Изменение размера изображения, хранящегося в виде массива с шагом: можно ли ускорить эту билинейную интерполяцию?

У меня есть фрагмент кода C, который является частью общедоступного репозитория (Darknet), который должен изменять размер изображение с помощью билинейной интерполяции. Из-за того, как остальная часть кода работает с изображениями, изображение сохраняется в виде одномерного массива, где значения пикселей из исходного 3-канального изображения считываются шаг за шагом. Таким образом, значение, соответствующее пикселю (x, y, k) (x: столбец, y: строка, k: канал), сохраняется в ячейке x + w.h + w.h.c в одномерном массиве.

Функция изменения размера, которая на самом деле является частью Darknet, занимает значительное количество времени на этапе предварительной обработки, возможно, из-за вложенных циклов for, которые перебирают строки и столбцы и пытаются получить доступ к соответствующим значениям, а также, возможно, к типу преобразования: поэтому я пытаюсь создать более оптимизированную версию. Исходный код для изменения размера выглядит следующим образом. im — исходное изображение, поэтому im.w и im.h — исходная ширина и высота. w и h — целевые ширина и высота.

image resize_image(image im, int w, int h)
{
    image resized = make_image(w, h, im.c);   
    image part = make_image(w, im.h, im.c);
    int r, c, k;
    float w_scale = (float)(im.w - 1) / (w - 1);
    float h_scale = (float)(im.h - 1) / (h - 1);
    for(k = 0; k < im.c; ++k){
        for(r = 0; r < im.h; ++r){
            for(c = 0; c < w; ++c){
                float val = 0;
                if(c == w-1 || im.w == 1){
                    val = get_pixel(im, im.w-1, r, k);
                } else {
                    float sx = c*w_scale;
                    int ix = (int) sx;
                    float dx = sx - ix;
                    val = (1 - dx) * get_pixel(im, ix, r, k) + dx * get_pixel(im, ix+1, r, k);
                }
                set_pixel(part, c, r, k, val);
            }
        }
    }
    for(k = 0; k < im.c; ++k){
        for(r = 0; r < h; ++r){
            float sy = r*h_scale;
            int iy = (int) sy;
            float dy = sy - iy;
            for(c = 0; c < w; ++c){
                float val = (1-dy) * get_pixel(part, c, iy, k);
                set_pixel(resized, c, r, k, val);
            }
            if(r == h-1 || im.h == 1) continue;
            for(c = 0; c < w; ++c){
                float val = dy * get_pixel(part, c, iy+1, k);
                add_pixel(resized, c, r, k, val);
            }
        }
    }
    free_image(part);
    return resized;
}

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

  1. Размеры исходного изображения и изображения с измененным размером будут фиксированными, поэтому моя «пользовательская» функция изменения размера не обязательно должна быть независимой от размера. Я перехожу от 640x360 к размерам 626x352.

  2. Целевая платформа — NVIDIA Jetson с процессором ARM, поэтому такие инструкции, как AVX2, в моем случае неприменимы. Но у меня есть доступ к CUDA.

Здесь я должен отметить, что из-за требований моего проекта эта функция изменения размера на самом деле является частью библиотеки (.so), которая вызывается из Python. Таким образом, я не могу хранить что-либо «в памяти» как таковое, например, объекты текстур CUDA и т. д., поэтому их повторное создание может фактически создать дополнительные накладные расходы на стороне CUDA.

Любые предложения по улучшению этой рутины были бы очень полезны.


person HighVoltage    schedule 26.04.2018    source источник
comment
Вместо этого попробуйте выполнить повторную выборку?   -  person Stan    schedule 26.04.2018
comment
Вы делаете новые изображения больше или меньше?   -  person AShelly    schedule 26.04.2018
comment
@AShelly Smaller: я перехожу с 640x360 на 626x352.   -  person HighVoltage    schedule 26.04.2018
comment
get_pixel и co не очень эффективны, если вы можете, вам следует избегать их использования и обращаться к image->data только с одной переменной, которую вы увеличиваете. Структура изображения также должна использовать FAM для увеличения производительности.   -  person Stargateur    schedule 26.04.2018


Ответы (1)


[Как упомянул Stargateur] get_pixel et. все расточительны. Большинство обращений к пикселям можно обрабатывать с помощью указателя. Это довольно стандартная вещь при обработке изображения, где требуется скорость.

Большинство обращений происходит по измерению x, поэтому мы можем просто увеличивать указатели.

Из get_pixel создайте эту функцию:

static float *loc_pixel(image m, int x, int y, int c)
{
    return &m.data[(c * m.h * m.w) + (y * m.w) + x];
}

if в resize_image можно переместить из первого внутреннего цикла for с помощью некоторой реструктуризации.

Во всех циклах for мы можем удалить все функции *_pixel из внутреннего цикла, используя loc_pixel и указатели.

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

Вы могли бы добавить одну вещь, которую я не сделал, это чтобы loc_pixel брал указатель на изображение (т.е. image *m) вместо передачи всей структуры.

Кроме того, вы можете поэкспериментировать с заменой src[0] на src[c] и *dst на dst[c]. Это устранит некоторые ++src и ++dst и может работать быстрее. Это также может позволить компилятору лучше понять циклы, чтобы он мог использовать любые векторные инструкции руки, и может сделать его более податливым для CUDA. YMMV.

image
resize_image(image im, int w, int h)
{
    image resized = make_image(w, h, im.c);
    image part = make_image(w, im.h, im.c);
    int r,
     c,
     k;
    float w_scale = (float) (im.w - 1) / (w - 1);
    float h_scale = (float) (im.h - 1) / (h - 1);
    int wm1 = w - 1;
    float val;
    float marg;
    float *src;
    float *dst;

    for (k = 0; k < im.c; ++k) {
        for (r = 0; r < im.h; ++r) {
            src = loc_pixel(im, 0, r, k);
            dst = loc_pixel(part, 0, r, k);
            marg = get_pixel(im, im.w - 1, r, k);

            if (im.w == 1) {
                for (c = 0; c < w; ++c, ++dst)
                    *dst = marg;
                continue;
            }

            for (c = 0; c < wm1; ++c, ++src, ++dst) {
                float sx = c * w_scale;
                int ix = (int) sx;
                float dx = sx - ix;
                val = (1 - dx) * src[0] + dx * src[1];
                *dst = val;
            }

            // handle c == w - 1 case
            *dst = marg;
        }
    }

    for (k = 0; k < im.c; ++k) {
        for (r = 0; r < h; ++r) {
            float sy = r * h_scale;
            int iy = (int) sy;
            float dy = sy - iy;

            src = loc_pixel(part, 0, iy, k);
            dst = loc_pixel(resized, 0, r, k);
            for (c = 0; c < w; ++c, ++src, ++dst) {
                val = (1 - dy) * src[0];
                *dst = val;
            }

            if (r == h - 1 || im.h == 1)
                continue;

            src = loc_pixel(part, 0, iy + 1, k);
            dst = loc_pixel(resized, 0, r, k, val);
            for (c = 0; c < w; ++c, ++src, ++dst) {
                val = dy * src[0];
                *dst += val;
            }
        }
    }

    free_image(part);
    return resized;
}
person Craig Estey    schedule 27.04.2018
comment
Если вы используете эту технику, мне было бы интересно узнать, как производительность сравнивается с оригиналом. - person Craig Estey; 27.04.2018
comment
Очень интересно, спасибо! Подход с указателями определенно очень помогает. У меня сейчас нет доступа к Jetson, поэтому я попробовал это в модульном тесте на своем рабочем столе. В среднем на более чем 100 прогонов ваша версия занимала 2,6 мс, тогда как исходная версия занимала 6,1 мс на операцию изменения размера. Это значительное ускорение! - person HighVoltage; 27.04.2018
comment
Я изучаю использование указателей при вызове loc_pixel. Быстрый вопрос: есть ли причина, по которой вы оставили один вызов get_pixel в цикле for (marg = get_pixel..) вместо loc_pixel? Кроме того, как вы думаете, поможет ли жесткое кодирование исходного и целевого измерений в этой функции? - person HighVoltage; 27.04.2018