Измените размер 8-битного изображения на 2 с помощью ARM NEON

У меня есть 8-битное изображение 640x480, которое я хотел бы уменьшить до изображения 320x240:

void reducebytwo(uint8_t *dst, uint8_t *src)
//src is 640x480, dst is 320x240

Как лучше всего это сделать с помощью ARM SIMD NEON? Где-нибудь есть пример кода?

В качестве отправной точки я просто хотел бы сделать эквивалент:

for (int h = 0; h < 240; h++)
    for (int w = 0; w < 320; w++)
        dst[h * 320 + w] = (src[640 * h * 2 + w * 2] + src[640 * h * 2 + w * 2 + 1] + src[640 * h * 2 + 640 + w * 2] + src[640 * h * 2 + 640 + w * 2 + 1]) / 4; 

person gregoiregentil    schedule 23.07.2013    source источник
comment
Необходимо определить лучшее. Самый быстрый, самый качественный, минимальный размер и т.д.? Для самого высокого качества существуют различные компромиссы при уменьшении изображения. В одних случаях важно сохранить низкочастотный контент, а в других — высокочастотный. Что такое 8-битный? Шкала серого, цветовая карта или что-то еще?   -  person artless noise    schedule 23.07.2013
comment
Это ввод в градациях серого. Лучший = самый быстрый.   -  person gregoiregentil    schedule 23.07.2013


Ответы (3)


Вот версия asm для reduce_line, предложенная @Nils Pipenbrinck

static void reduce2_neon_line(uint8_t* __restrict src1, uint8_t* __restrict src2, uint8_t* __restrict dest, int width) {
    for(int i=0; i<width; i+=16) {
        asm (
             "pld [%[line1], #0xc00]     \n"
             "pld [%[line2], #0xc00]     \n"
             "vldm %[line1]!, {d0,d1}  \n"
             "vldm %[line2]!, {d2,d3}  \n"
             "vpaddl.u8 q0, q0         \n"
             "vpaddl.u8 q1, q1         \n"
             "vadd.u16  q0, q1         \n"
             "vshrn.u16 d0, q0, #2     \n"

             "vst1.8 {d0}, [%[dst]]! \n"

             :
             : [line1] "r"(src1), [line2] "r"(src2), [dst] "r"(dest)
             : "q0", "q1", "memory"
             );
    }
}

Это примерно в 4 раза быстрее, чем версия C (проверено на iPhone 5).

person Max    schedule 08.10.2013

Это перевод вашего кода на встроенные функции NEON один к одному:

#include <arm_neon.h>
#include <stdint.h>

static void resize_line (uint8_t * __restrict src1, uint8_t * __restrict src2, uint8_t * __restrict dest)
{
  int i;
  for (i=0; i<640; i+=16)
  {
    // load upper line and add neighbor pixels:
    uint16x8_t a = vpaddlq_u8 (vld1q_u8 (src1));

    // load lower line and add neighbor pixels:
    uint16x8_t b = vpaddlq_u8 (vld1q_u8 (src2));

    // sum of upper and lower line: 
    uint16x8_t c = vaddq_u16 (a,b);

    // divide by 4, convert to char and store:
    vst1_u8 (dest, vshrn_n_u16 (c, 2));

    // move pointers to next chunk of data
    src1+=16;
    src2+=16;
    dest+=8;
   }
}   

void resize_image (uint8_t * src, uint8_t * dest)
{
  int h;    
  for (h = 0; h < 240 - 1; h++)
  {
    resize_line (src+640*(h*2+0), 
                 src+640*(h*2+1), 
                 dest+320*h);
  }
}

Он обрабатывает 32 исходных пикселя и генерирует 8 выходных пикселей за итерацию.

Я быстро просмотрел вывод ассемблера, и он выглядит нормально. Вы можете повысить производительность, если напишете функцию resize_line на ассемблере, развернете цикл и устраните остановки конвейера. Это даст вам ориентировочный прирост производительности в три раза.

Это должно быть намного быстрее, чем ваша реализация без изменений ассемблера.

Примечание. Я не тестировал код...

person Nils Pipenbrinck    schedule 24.07.2013
comment
Большой! Как вы думаете, иметь всю функцию resize_image на ассемблере было бы намного быстрее, или вы думаете, что с вашим предложением я уже сэкономил 90% времени? - person gregoiregentil; 24.07.2013
comment
Это было бы быстрее .. в этом нет сомнений. - person Nils Pipenbrinck; 24.07.2013

Если вас не слишком заботит точность, то этот внутренний цикл должен дать вам вдвое большую вычислительную производительность по сравнению с более точным алгоритмом:

for (i=0; i<640; i+= 32)
{
    uint8x16x2_t a, b;
    uint8x16_t c, d;

    /* load upper row, splitting even and odd pixels into a.val[0]
     * and a.val[1] respectively. */
    a = vld2q_u8(src1);

    /* as above, but for lower row */
    b = vld2q_u8(src2);

    /* compute average of even and odd pixel pairs for upper row */
    c = vrhaddq_u8(a.val[0], a.val[1]);
    /* compute average of even and odd pixel pairs for lower row */
    d = vrhaddq_u8(b.val[0], b.val[1]);

    /* compute average of upper and lower rows, and store result */
    vst1q_u8(dest, vrhaddq_u8(c, d));

    src1+=32;
    src2+=32;
    dest+=16;
}

Он работает с использованием операции vhadd, результат которой имеет тот же размер, что и ввод. Таким образом, вам не нужно смещать окончательную сумму обратно к 8-битной, а вся арифметика выполняется восьмибитной, что означает, что вы можете выполнять в два раза больше операций за инструкцию.

Однако он менее точен, потому что промежуточная сумма квантуется, а GCC 4.7 ужасно справляется с генерацией кода. GCC 4.8 работает нормально.

Однако вся операция имеет хорошие шансы быть привязанной к вводу-выводу. Цикл следует развернуть, чтобы максимизировать разделение между загрузками и арифметикой, а __builtin_prefetch() (или PLD) следует использовать для подъема входящих данных в кэш до того, как они потребуются.

person sh1    schedule 07.08.2013