Смешайте два изображения с помощью графического процессора

Мне нужно очень быстро смешать тысячи пар изображений.

В настоящее время мой код делает следующее: _apply — это указатель на функцию, подобную Blend. Это одна из многих функций, которые мы можем передать, но не единственная. Любая функция принимает два значения и выводит третье, и это делается на каждом канале для каждого пикселя. Я бы предпочел решение, которое является общим для любой такой функции, а не конкретное решение для смешивания.

typedef byte (*Transform)(byte src1,byte src2); 
Transform _apply;

for (int i=0 ; i< _frameSize ; i++) 
{
    source[i] = _apply(blend[i]);
}


byte Blend(byte src, byte blend)
{
    int resultPixel = (src + blend)/2;

    return (byte)resultPixel;
}

Я делал это на процессоре, но производительность ужасна. Насколько я понимаю, делать это на GPU очень быстро. Моя программа должна работать на компьютерах с графическими процессорами Nvidia или Intel, поэтому любое решение, которое я использую, должно быть независимым от поставщика. Если я использую графический процессор, он также должен быть OpenGL, чтобы быть независимым от платформы.

Я думаю, что использование пиксельного шейдера GLSL помогло бы, но я не знаком с пиксельными шейдерами или с тем, как использовать их для 2D-объектов (например, моих изображений).

Это разумное решение? Если да, то как это сделать в 2D? Если есть библиотека, которая уже делает это, тоже полезно знать.

РЕДАКТИРОВАТЬ: я получаю пары изображений из разных источников. Один всегда исходит из компонента 3D-графики в opengl (то есть изначально в GPU). Другой исходит из системной памяти либо из сокета (в сжатом видеопотоке), либо из файла с отображением памяти. «Стоком» результирующего изображения является экран. Ожидается, что я покажу изображения на экране, поэтому для их отображения можно использовать GPU или что-то вроде SDL.

Функция смешивания, которая будет выполняться чаще всего, это

byte Patch(byte delta, byte lo)
{
    int resultPixel = (2 * (delta - 127)) + lo;

    if (resultPixel > 255)
       resultPixel = 255;

    if (resultPixel < 0)
       resultPixel = 0;

    return (byte)resultPixel;
}

РЕДАКТИРОВАТЬ 2: Изображение, поступающее с земли GPU, происходит таким образом. От FBO к PBO к системной памяти

glBindFramebuffer(GL_FRAMEBUFFER,fbo);
glReadBuffer( GL_COLOR_ATTACHMENT0 );
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
glReadPixels(0,0,width,height,GL_BGR,GL_UNSIGNED_BYTE,0); 
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo); 
void* mappedRegion = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);

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

Редактировать 3: одно из моих изображений будет получено из D3D, а другое - из OpenGL. Кажется, что-то вроде Thrust или OpenCL — лучший вариант


person cloudraven    schedule 06.04.2014    source источник
comment
Как вы получаете данные изображения в первую очередь? Я согласен с ответом SchighSchagh в том, что накладные расходы ввода-вывода при передаче каждого изображения в GPU и обратно могут испортить все преимущества обработки GPU, но неясно, так ли это, без более широкой картины того, где ваши исходящие изображения и что делать с результатами.   -  person derhass    schedule 06.04.2014
comment
Производительность на ЦП ужасна, потому что вы не оптимизировали для ЦП. Например, наивная реализация матричного умножения получает около 1% пиковых флопов, но если вы знаете, что делаете, не так уж сложно сделать это более чем в 50 раз быстрее на ЦП. Может быть, вы могли бы опубликовать код своей функции смешивания.   -  person Z boson    schedule 06.04.2014
comment
Спасибо за ваши комментарии. Я добавил больше информации, чтобы сделать проблему более ясной. Моя реализация очень наивна. Я нахожусь на этапе настройки производительности, когда мне нужно перейти от того, что работает, к чему-то быстрому, и это одно из основных узких мест, которые у нас есть.   -  person cloudraven    schedule 08.04.2014
comment
Хорошо, если одно из ваших изображений начинается на графическом процессоре, а финальное изображение заканчивается на графическом процессоре, то во что бы то ни стало выполняйте вычисления на графическом процессоре. Но вы, вероятно, все еще будете ограничены пропускной способностью памяти с новой функцией смешивания, которую вы показали. Можете ли вы опубликовать больше кода, показывающего, как организована ваша программа? Что-то, что показывает, например, какие FBO задействованы, текстуры и т. д.   -  person Nicu Stiurca    schedule 09.04.2014
comment
Я добавил еще немного контекста, чтобы показать используемый FBO. У нас нет текстур. Мы могли бы изменить код для рендеринга в текстуру вместо FBO, если это лучше подходит для этой цели. Подходит ли тяга для этого сценария? Спасибо за вашу помощь   -  person cloudraven    schedule 11.04.2014
comment
Получается, что одно изображение должно исходить из OpenGL, а другое из DirectX. Вероятно, opencl или тяга будет выходом   -  person cloudraven    schedule 18.04.2014


Ответы (1)


Судя по вашей функции Blend, это операция, полностью ограниченная памятью. Кэши на ЦП, вероятно, могут хранить только очень небольшую часть из тысяч изображений, которые у вас есть. Это означает, что большая часть вашего времени тратится на ожидание, пока ОЗУ выполнит запросы на загрузку/сохранение, а ЦП будет много простаивать.

Вы НЕ получите никакого ускорения, если вам придется копировать ваши изображения из ОЗУ в графический процессор, заставлять арифметические блоки графического процессора простаивать, пока они ждут, пока ОЗУ графического процессора передаст им данные, ждать, пока ОЗУ графического процессора снова записывает результаты, а затем копировать все это обратно в основной ОЗУ. Использование GPU для этого может существенно замедлить работу.


Но я могу ошибаться, и вы, возможно, еще не насыщаете свою шину памяти. Вам придется попробовать его в своей системе и профилировать. Вот несколько простых вещей, которые вы можете попытаться оптимизировать.

1. Многопоточность

Я бы сосредоточился на оптимизации алгоритма непосредственно на процессоре. Самое простое — использовать многопоточность, для чего достаточно включить OpenMP в компиляторе и обновить цикл for:

#include <omp.h> // add this along with enabling OpenMP support in your compiler
...
#pragma omp parallel for // <--- compiler magic happens here
for (int i=0 ; i< _frameSize ; i++) 
{
    source[i] = _apply(blend[i]);
}

Если пропускная способность вашей памяти не перегружена, это, скорее всего, ускорит смешивание, сколько бы ядер ни было в вашей системе.

2. Микрооптимизации

Еще одна вещь, которую вы можете попробовать, это реализовать Blend с помощью SIMD-инструкций, которые в настоящее время есть в большинстве процессоров. Я не могу помочь вам с этим, не зная, на какой процессор вы ориентируетесь.

Вы также можете попробовать развернуть цикл for, чтобы уменьшить некоторые накладные расходы цикла.

Одним из простых способов добиться того и другого является использование библиотеки собственных матриц, обернув ваш данные в своих структурах данных.

// initialize your data and result buffer
byte *source = ...
byte *blend = ...
byte *result = ...

// tell Eigen where you data/buffer are, and to treat it like a dynamic vectory of bytes
// this is a cheap shallow copy
Map<Matrix<byte, Dynamic,1> > sourceMap(source, _frameSize);
Map<Matrix<byte, Dynamic,1> > blendMap(blend, _frameSize);
Map<Matrix<byte, Dynamic,1> > resultMap(result, _frameSize);

// perform blend using all manner of insane optimization voodoo under the covers
resultMap = (sourceMap + blendMap)/2;

3. Используйте графический процессор

Наконец, я дам прямой ответ на ваш вопрос с простым способом использования графического процессора без особых знаний о программировании графического процессора. Проще всего попробовать библиотеку Thrust. Вам придется переписать свои алгоритмы как алгоритмы в стиле STL, но в вашем случае это довольно просто.

// functor for blending
struct blend_functor
{
  template <typename Tuple>
  __host__ __device__
  void operator()(Tuple t)
  {
    // C[i] = (A[i] + B[i])/2;
    thrust::get<2>(t) = (thrust::get<0>(t) + thrust::get<1>(t))/2;
  }
};

// initialize your data and result buffer
byte *source = ...
byte *blend = ...
byte *result = NULL;

// copy the data to the vectors on the GPU
thrust::device_vector<byte> A(source, source + _frameSize);
thrust::device_vector<byte> B(blend, blend + _frameSize);
// allocate result vector on the GPU
thrust::device_vector<byte> C(_frameSize);

// process the data on the GPU device
thrust::for_each(thrust::make_zip_iterator(thrust::make_tuple(
                                  A.begin(), B.begin(), C.begin())),
                 thrust::make_zip_iterator(thrust::make_tuple(
                                  A.end(), B.end(), C.end())),
                 blend_functor());

// copy the data back to main RAM
thrust::host_vector<byte> resultVec = C;
result = resultVec.data();

Отличительной особенностью тяги является то, что после того, как вы написали алгоритмы в общем виде, они могут автоматически использовать разные концы для выполнения вычислений. CUDA — это серверная часть по умолчанию, но вы также можете настроить ее во время компиляции для использования OpenMP или TBB (библиотека потоков Intel).

person Nicu Stiurca    schedule 06.04.2014
comment
Спасибо! Это очень полезно. Я добавил некоторые дополнительные детали, которые могут повлиять на вашу рекомендацию. Если это так, я был бы признателен за любые комментарии. Я беру одно из двух изображений с графического процессора, а получателем полученного изображения является экран. Я думал о шейдере или GPGPU, поскольку одно из изображений поступает из памяти графического процессора, но вы правы, мне все равно нужно скопировать другое из системы в память графического процессора. Поэтому я не уверен, какой из них имеет больше смысла. Я вижу, что любое из ваших предложений улучшит производительность - person cloudraven; 08.04.2014