Работа с изображениями - достаточно типичная задача для видекорарт и OpenCL. Ну точно более типичная, чем перемножение матриц. Я долго искал наглядный пример, чтобы с изображением и чтобы все работало и без лишних телодвижений типа установки каких-то непонятных или очень больших библиотек и в итоге так ничего не нашел, но потом наткнулся на пример работы с bmp-файлом, который подходит мне идеально: небольшой размер библиотеки (по сути все укладывается водин небольшой заголовочный файл - "BMP.h") и не требует никаких дополнительных действий. А еще я решил изучить, насколько удобно использовать cmake, встроенный в Visual Studio 2019 и править конфигурацию через CMakeLists.txt. Оказывается, что довольно удобно - удобнее, чем например руками каждый раз отдельно создавать Release и Debug конфигурации и для каждой из них создавать x86 и x64 варианты (итого 4 конфигурации как минимум). Так вот, у нас есть вот такой CMakeLists.txt:
- #
- cmake_minimum_required (VERSION 3.8)
- # create project
- project ("OclFilterImg")
- # Add source to this project's executable.
- add_executable (OclFilterImg "OpenCL-filter-img.cpp" "BMP.h")
- # OpencCL headers
- target_include_directories(OclFilterImg PRIVATE "$ENV{CUDA_PATH}/include")
- # OpenCL library
- set(opencl_lib_folder "$ENV{CUDA_PATH}/lib")
- # check 32 or 64 bits
- if(CMAKE_SIZEOF_VOID_P EQUAL 8)
- # 64 bits
- set(opencl_lib_folder "${opencl_lib_folder}/x64")
- elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
- # 32 bits
- set(opencl_lib_folder "${opencl_lib_folder}/Win32")
- endif()
- # link OpenCL library
- target_link_libraries(OclFilterImg ${opencl_lib_folder}/OpenCL.lib)
Нам надо наложить на него сначала один фильтр, а потом на результат наложить еще один фильтр и потом еще один. Каждый пиксел это 3 байта RGB цвета + альфа-канал прозрачности, итого 32 бита на пиксел. Причем альфа-канала может и не быть, тогда получается 24 бита на пиксел. Библиотека поддерживает оба варианта и для удобства хранит картинку в виде массива байтов (unsigned char). Именно так мы и передаем изображение в код ядра, где уже через высоту и ширину картинки и количества байтов на пиксел ( 3 или 4 байта) вычисляем позицию пикселя. Первый фильтр у нас делает из цветного изображения монохромное путем усреднения цвета по всем трем RGB-компонентам:
- // converts an RBG image to grayscale
- __kernel void rgbToGray(
- __global unsigned char* inImg,
- __global unsigned char* outImg,
- const int bytesPerPix)
- {
- // Get work-item identifiers.
- int x = get_global_id(0);
- int y = get_global_id(1);
- int imgW = get_global_size(0);
- int offset = ((y * imgW) + x) * bytesPerPix;
- unsigned char B = inImg[offset];
- unsigned char G = inImg[offset + 1];
- unsigned char R = inImg[offset + 2];
- unsigned char gray = (B + G + R) / 3;
- outImg[offset] = gray;
- outImg[offset + 1] = gray;
- outImg[offset + 2] = gray;
- if (bytesPerPix == 4) // if we have alfa-channel
- outImg[offset + 3] = inImg[offset + 3];
- }
- // Create a low-pass filter mask.
- const int lpMaskSize = 5;
- float lpMask[lpMaskSize][lpMaskSize] =
- {
- {.04,.04,.04,.04,.04},
- {.04,.04,.04,.04,.04},
- {.04,.04,.04,.04,.04},
- {.04,.04,.04,.04,.04},
- {.04,.04,.04,.04,.04},
- };
- int MinMaxVal(int min, int max, int val)
- {
- if (val < min)
- {
- return min;
- }
- else if (val > max)
- {
- return max;
- }
- return val;
- }
- // This kernel function convolves an image input_image[imgWidth, imgHeight]
- // with a mask of size maskSize.
- __kernel void filterImage(
- __global unsigned char* inImg,
- __global unsigned char* outImg,
- const int bytesPerPix,
- const unsigned int maskSize,
- __constant float* mask)
- {
- // Get work-item identifiers.
- int x = get_global_id(0);
- int y = get_global_id(1);
- int imgW = get_global_size(0);
- int imgH = get_global_size(1);
- int offset = ((y * imgW) + x) * bytesPerPix;
- // Check if the mask cannot be applied to the current pixel
- if (x < maskSize / 2
- || y < maskSize / 2
- || x >= imgW - maskSize / 2
- || y >= imgH - maskSize / 2)
- {
- outImg[offset] = 0;
- outImg[offset + 1] = 0;
- outImg[offset + 2] = 0;
- if (bytesPerPix == 4) // if we have alfa-channel
- outImg[offset + 3] = inImg[offset + 3];
- return;
- }
- // Apply mask based on the neighborhood of pixel inputImg.
- int outSumB = 0;
- int outSumG = 0;
- int outSumR = 0;
- for (size_t k = 0; k < maskSize; k++)
- {
- for (size_t l = 0; l < maskSize; l++)
- {
- // Calculate the current mask index.
- size_t maskIdx = (maskSize - 1 - k) + (maskSize - 1 - l) * maskSize;
- // Compute output pixel.
- size_t xM = x - maskSize / 2 + k;
- size_t yM = y - maskSize / 2 + l;
- int offsetM = ((yM * imgW) + xM) * bytesPerPix;
- outSumB += inImg[offsetM] * mask[maskIdx];
- outSumG += inImg[offsetM + 1] * mask[maskIdx];
- outSumR += inImg[offsetM + 2] * mask[maskIdx];
- }
- }
- // Write output pixel.
- outImg[offset] = MinMaxVal(0, 255, outSumB);
- outImg[offset + 1] = MinMaxVal(0, 255, outSumG);
- outImg[offset + 2] = MinMaxVal(0, 255, outSumR);
- if (bytesPerPix == 4) // if we have alfa-channel
- outImg[offset + 3] = inImg[offset + 3];
- }
- // Create a high-pass filter mask.
- const int hpMaskSize = 5;
- float hpMask[hpMaskSize][hpMaskSize] =
- {
- {-1,-1,-1,-1,-1},
- {-1,-1,-1,-1,-1},
- {-1,-1,24,-1,-1},
- {-1,-1,-1,-1,-1},
- {-1,-1,-1,-1,-1},
- };
Интересный эффект, не правда ли? Если посмотреть на коэффициенты этого фильтра, то все можно обьяснить: если пиксел окружен такими же точно пикселями, то суммарное значение после прохождения такого фильтра будет равно нулю (24 пикселя с отрицательными коэффициентами плюс один этот пиксел с коэффициентом +24 дают в сумме ноль). А если есть вокруг отличные от этого пикселя, то значение будет больше нуля. То есть такой фильтр по сути будет давать значения больше нуля только на границах, например, какой-то заполненной геометрической фигуры.
- void gpuProcess(const BMP& bmpIn, BMP& bmpOut)
- {
- const auto imgWidth = bmpIn.bmp_info_header.width;
- const auto imgHeight = bmpIn.bmp_info_header.height;
- const uint32_t bytesPP = bmpIn.bmp_info_header.bit_count / 8;
- cl::Buffer inImg(context, CL_MEM_READ_WRITE | CL_MEM_HOST_READ_ONLY | CL_MEM_COPY_HOST_PTR, imgWidth * imgHeight * bytesPP, (void*) bmpIn.data.data());
- cl::Buffer grayImg(context, CL_MEM_READ_WRITE | CL_MEM_HOST_READ_ONLY, imgWidth * imgHeight * bytesPP);
- cl::Buffer lpfImg(context, CL_MEM_READ_WRITE | CL_MEM_HOST_READ_ONLY, imgWidth * imgHeight * bytesPP);
- cl::Buffer lpMaskBuf(context, CL_MEM_READ_ONLY | CL_MEM_HOST_NO_ACCESS | CL_MEM_COPY_HOST_PTR, lpMaskSize * lpMaskSize * sizeof(lpMask[0]), lpMask);
- cl::Buffer hpfImg(context, CL_MEM_READ_WRITE | CL_MEM_HOST_READ_ONLY, imgWidth * imgHeight * bytesPP);
- cl::Buffer hpMaskBuf(context, CL_MEM_READ_ONLY | CL_MEM_HOST_NO_ACCESS | CL_MEM_COPY_HOST_PTR, hpMaskSize * hpMaskSize * sizeof(hpMask[0]), hpMask);
- cl::Kernel grayKernel(program, "rgbToGray");
- grayKernel.setArg(0, inImg);
- grayKernel.setArg(1, grayImg);
- grayKernel.setArg(2, bytesPP);
- cl::Kernel lpfKernel(program, "filterImage");
- lpfKernel.setArg(0, grayImg);
- lpfKernel.setArg(1, lpfImg);
- lpfKernel.setArg(2, bytesPP);
- lpfKernel.setArg(3, lpMaskSize);
- lpfKernel.setArg(4, lpMaskBuf);
- cl::Kernel hpfKernel(program, "filterImage");
- hpfKernel.setArg(0, lpfImg);
- hpfKernel.setArg(1, hpfImg);
- hpfKernel.setArg(2, bytesPP);
- hpfKernel.setArg(3, hpMaskSize);
- hpfKernel.setArg(4, hpMaskBuf);
- cl::CommandQueue queue(context, device);
- queue.enqueueNDRangeKernel(grayKernel, cl::NullRange, cl::NDRange(imgWidth, imgHeight));
- queue.enqueueNDRangeKernel(lpfKernel, cl::NullRange, cl::NDRange(imgWidth, imgHeight));
- queue.enqueueNDRangeKernel(hpfKernel, cl::NullRange, cl::NDRange(imgWidth, imgHeight));
- queue.enqueueReadBuffer(hpfImg, CL_TRUE, 0, bmpOut.data.size() * sizeof(bmpOut.data[0]), &bmpOut.data[0]);
- }
Комментариев нет:
Отправить комментарий