Вернуться к разделу "Реализация проекта BookScanLib".
Конволюционные фильтры - это алгоритмы, построенные на принципе конволюции (Convolution).
Конволюция - это замена обрабатываемого пикселя на средневзвешенное (с различными весовыми коэффициентами) значение его малой окрестности.
Конволюционные фильтры могут обрабатывать только серые и цветные изображения (чёрно-белые - нет).
Проще говоря:
1. Окошко 3х3 пикселя ("апертура")
пробегает по всем пикселям изображения. 2. Каждому из 9 соседних пикселей и центру назначается свой весовой коэффициент. 3. Номер цвета каждого пикселя умножается на соответствующий коэффициент и все 10 полученных значений складываются. 4. Найденная сумма делится на общий вес, к результату прибавляется добавка (иногда). 5. Результат вставляется на место обрабатываемого пикселя. |
Существует целое семейство конволюционных фильтров, отличающихся друг от друга лишь матрицей весовых коэффициентов (алгоритм обработки - один и тот же).
В этой статье мы рассмотрим следующие фильтры:
1. Blur (Размытие): 1 1 1 1 1 1 1 1 1 / 9 |
2. Gaussian Blur: (Гауссово размытие) 0 1 0 1 4 1 0 1 0 / 8 |
3. Sharpening (Резкость): 0 -1 0 -1 9 -1 0 -1 0 / 5 |
4. Laplasian (Лапласиан - выделение
границ) -1 -1 -1 -1 8 -1 -1 -1 -1 / 1 + 128 |
5. Emboss 135 degrees (Рельеф 135 град.) 1 0 0 0 0 0 0 0 -1 / 1 + 128 |
6. Emboss 90 degrees, 50% (Рельеф 90 град., 50% ) 0 1 0 0 0 0 0 -1 0 / 2 + 128 |
Я написал простейшую консольную программу для демонстрации работы семества конволюционных фильтров. На входе она принимает имя графического файла через командную строку и номер фильтра, а на выходе выдаёт этот же файл, обработанный выбранным фильтром.
Всё необходимое для тестирования этой программы (компиляционный проект, готовый экзешник, файл-пример и bat-файлы для тестирования программы) я оформил в небольшой пакет:
Скачать пакет conv_filters (51 КБ)
(Для работы программы требуется FreeImage dll-библиотека из пакета FreeImage DLL v3.9.2 - см. статью 1. Знакомство с FreeImage).
Рассмотрим исходные коды этой программы:
// This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // http://www.gnu.org/copyleft/gpl.html // Copyright (C) 2007-2008: // monday2000 monday2000@yandex.ru #include "FreeImage.h" #include "Utilities.h" //////////////////////////////////////////////////////////////////////////////// FIBITMAP* FI_filter(FIBITMAP* src_dib, int f[3][3], int weight, int add) { unsigned width = FreeImage_GetWidth(src_dib); unsigned height = FreeImage_GetHeight(src_dib); unsigned pitch = FreeImage_GetPitch(src_dib); unsigned bpp = FreeImage_GetBPP(src_dib); unsigned btpp = bpp/8; unsigned row, col, i, j, d; int k_index; FIBITMAP* dst_dib = FreeImage_Allocate(width, height, bpp); unsigned dst_pitch = FreeImage_GetPitch(dst_dib); BYTE* src_bits = (BYTE*)FreeImage_GetBits(src_dib); // The image raster BYTE* dst_bits = (BYTE*)FreeImage_GetBits(dst_dib); // The image raster BYTE* src_end_row = src_bits + (height-1) * pitch; int end_col = width - 1; BYTE* lines, *linek, *lined; int* pval = new int[btpp]; int* psum = new int[btpp]; for (row = 0; row < height; row++) { lines = src_bits + row * pitch; lined = dst_bits + row * dst_pitch; for (col = 0; col < width; col++) { memset(psum, 0, sizeof(int)*btpp); // kernel processing for (i = 0; i < 3; i++) for (j = 0; j < 3; j++) { linek = lines + (i-1) * pitch; if (linek < src_bits) linek = src_bits; if (linek > src_end_row) linek = src_end_row; for (d=0; d<btpp; d++) { k_index = col+j-1; if (k_index < 0) k_index = 0; if (k_index > end_col) k_index = end_col; psum[d] += linek[k_index * btpp + d] * f[i][j]; } } for (d=0; d<btpp; d++) { pval[d] = psum[d] / weight + add; // clamp and place result in destination pixel lined[col * btpp + d] = (BYTE)MIN(MAX((int)0, (int)(pval[d] + 0.5)), (int)255); } } } if(bpp == 8) { // copy the original palette to the destination bitmap RGBQUAD *src_pal = FreeImage_GetPalette(src_dib); RGBQUAD *dst_pal = FreeImage_GetPalette(dst_dib); memcpy(&dst_pal[0], &src_pal[0], 256 * sizeof(RGBQUAD)); } // Copying the DPI... FreeImage_SetDotsPerMeterX(dst_dib, FreeImage_GetDotsPerMeterX(src_dib)); FreeImage_SetDotsPerMeterY(dst_dib, FreeImage_GetDotsPerMeterY(src_dib)); delete [] pval; delete [] psum; return dst_dib; } //////////////////////////////////////////////////////////////////////////////// FIBITMAP* run_filter(FIBITMAP* dib, int choice) { int f[3][3]; // filter int weight = 1; // divider int add = 0; // value sometimes added to the divider switch (choice) { case 1: { //********************************************* // 1. Blur: f[0][0] = 1; f[0][1] = 1; f[0][2] = 1; f[1][0] = 1; f[1][1] = 1; f[1][2] = 1; f[2][0] = 1; f[2][1] = 1; f[2][2] = 1; weight = 9; add = 0; //********************************************* return FI_filter(dib, f, weight, add); } case 2: { //********************************************* // 2. Gaussian Blur: f[0][0] = 0; f[0][1] = 1; f[0][2] = 0; f[1][0] = 1; f[1][1] = 4; f[1][2] = 1; f[2][0] = 0; f[2][1] = 1; f[2][2] = 0; weight = 8; add = 0; //********************************************* return FI_filter(dib, f, weight, add); } case 3: { //********************************************* // 3. Sharpening: f[0][0] = 0; f[0][1] = -1; f[0][2] = 0; f[1][0] = -1; f[1][1] = 9; f[1][2] = -1; f[2][0] = 0; f[2][1] = -1; f[2][2] = 0; weight = 5; add = 0; //********************************************* return FI_filter(dib, f, weight, add); } case 4: { //********************************************* // 4. Laplasian: (contour selection) f[0][0] = -1; f[0][1] = -1; f[0][2] = -1; f[1][0] = -1; f[1][1] = 8; f[1][2] = -1; f[2][0] = -1; f[2][1] = -1; f[2][2] = -1; weight = 1; add = 128; //********************************************* return FI_filter(dib, f, weight, add); } case 5: { //********************************************* // 5. Emboss 135 degrees f[0][0] = 1; f[0][1] = 0; f[0][2] = 0; f[1][0] = 0; f[1][1] = 0; f[1][2] = 0; f[2][0] = 0; f[2][1] = 0; f[2][2] = -1; weight = 1; add = 128; //********************************************* return FI_filter(dib, f, weight, add); } case 6: { //********************************************* // 6. Emboss 90 degrees, 50% f[0][0] = 0; f[0][1] = 1; f[0][2] = 0; f[1][0] = 0; f[1][1] = 0; f[1][2] = 0; f[2][0] = 0; f[2][1] = -1; f[2][2] = 0; weight = 2; add = 128; //********************************************* return FI_filter(dib, f, weight, add); } } return NULL; } //////////////////////////////////////////////////////////////////////////////// // ---------------------------------------------------------- /** FreeImage error handler @param fif Format / Plugin responsible for the error @param message Error message */ void FreeImageErrorHandler(FREE_IMAGE_FORMAT fif, const char *message) { printf("\n*** "); printf("%s Format\n", FreeImage_GetFormatFromFIF(fif)); printf(message); printf(" ***\n"); } // ---------------------------------------------------------- /** Generic image loader @param lpszPathName Pointer to the full file name @param flag Optional load flag constant @return Returns the loaded dib if successful, returns NULL otherwise */ FIBITMAP* GenericLoader(const char* lpszPathName, int flag) { FREE_IMAGE_FORMAT fif = FIF_UNKNOWN; // check the file signature and deduce its format // (the second argument is currently not used by FreeImage) fif = FreeImage_GetFileType(lpszPathName, 0); FIBITMAP* dib; if(fif == FIF_UNKNOWN) { // no signature ? // try to guess the file format from the file extension fif = FreeImage_GetFIFFromFilename(lpszPathName); } // check that the plugin has reading capabilities ... if((fif != FIF_UNKNOWN) && FreeImage_FIFSupportsReading(fif)) { // ok, let's load the file dib = FreeImage_Load(fif, lpszPathName, flag); // unless a bad file format, we are done ! if (!dib) { printf("%s%s%s\n","File \"", lpszPathName, "\" not found."); return NULL; } } return dib; } //////////////////////////////////////////////////////////////////////////////// int main(int argc, char *argv[]) { // call this ONLY when linking with FreeImage as a static library #ifdef FREEIMAGE_LIB FreeImage_Initialise(); #endif // FREEIMAGE_LIB // initialize your own FreeImage error handler FreeImage_SetOutputMessage(FreeImageErrorHandler); if(argc != 3) { printf("Usage : filters <input_file> <filter_number>\n"); return 0; } FIBITMAP *dib = GenericLoader(argv[1], 0); int choice = atoi(argv[2]); if (dib) { // bitmap is successfully loaded! if (FreeImage_GetImageType(dib) == FIT_BITMAP) { if (FreeImage_GetBPP(dib) == 8 || FreeImage_GetBPP(dib) == 24) { FIBITMAP* dst_dib = run_filter(dib, choice); if (dst_dib) { // save the filtered bitmap const char *output_filename = "filtered.tif"; // first, check the output format from the file name or file extension FREE_IMAGE_FORMAT out_fif = FreeImage_GetFIFFromFilename(output_filename); if(out_fif != FIF_UNKNOWN) { // then save the file FreeImage_Save(out_fif, dst_dib, output_filename, 0); } // free the loaded FIBITMAP FreeImage_Unload(dst_dib); } } else printf("%s\n", "Unsupported color mode."); } else // non-FIT_BITMAP images are not supported. printf("%s\n", "Unsupported color mode."); FreeImage_Unload(dib); } // call this ONLY when linking with FreeImage as a static library #ifdef FREEIMAGE_LIB FreeImage_DeInitialise(); #endif // FREEIMAGE_LIB return 0; } |
Реализация этих алгоритмов взята из книги:
Фень Юань. Программирование графики для Windows. СПб.: Питер, 2002.
(Эта книга есть в Интернете в формате DjVu на русском языке и в формате CHM на английском языке)
Ниже я приведу выдержки из этой книги:
На рис. 12.4 после первого исходного рисунка показаны результаты применения трех пространственных фильтров: сглаживания, сглаживания по Гауссу и резкости (для наглядности масштаб равен 3:1).
Вверху слева изображена исходная картинка. Справа от нее показан результат применения сглаживающего фильтра. Его матрица 3x3 состоит из одних единиц, а вес равен 9. Следовательно, этот фильтр присваивает пикселу среднее значение пикселов в блоке 3x3. Сглаживающий фильтр называется низкочастотным фильтром, поскольку он сохраняет низкочастотные участки и отфильтровывает высокочастотные искажения. В частности, он может использоваться для сглаживания линий, фигур и растров, выводимых средствами GDI. На рисунке видно, как сглаживающий фильтр маскирует зазубренные края исходной картинки. После применения сглаживающего фильтра на границах глифа появляются серые пикселы.
Фильтр сглаживания по Гауссу также относится к категории низкочастотных фильтров. Вместо равномерного распределения этот фильтр назначает больший весовой коэффициент центральному пикселу. Фильтры этого типа могут определяться и для большего радиуса; на рисунке показан фильтр 3x3.
Фильтр резкости вычитает соседние пикселы из текущего, чтобы подчеркнуть переходы в изображении. Он относится к категории высокочастотных фильтров, которые выделяют высокочастотные компоненты изображения и оставляют низкочастотные участки без изменений. Регулируя весовой коэффициент центрального пиксела, можно менять степень резкости. Для монохромного изображения, показанного на рисунке, результат применения фильтра резкости практически незаметен.
На рис. 12.5 показаны результаты применения фильтра Лапласа и двух рельефных фильтров.
По виду матрицы фильтр Лапласа похож на высокочастотный фильтр, но он генерирует абсолютно другое изображение. Фильтр Лапласа относится к категории фильтров выделения границ с нулевой суммой коэффициентов матрицы. Фильтр выделения границ заменяет равномерно окрашенные области черным цветом, а области с изменениями — цветом, отличным от черного. В приведенном примере фильтр прибавляет к каждому каналу 128, чтобы отрицательный результат не заменялся нулем. В результате прибавления 128 равномерно окрашенные области становятся серыми.
Следующие два фильтра, называемые рельефными фильтрами, преобразуют цветное изображение в оттенки серого со своеобразными объемными эффектами. В одном углу матрицы рельефного фильтра стоит 1, а элемент в противоположном углу равен -1. Применение рельефного фильтра можно рассматривать как вычитание изображения, смещенного на определенную величину от оригинала. Результат увеличивается на 128, чтобы нулевая точка переместилась в середину шкалы оттенков серого. Относительные позиции пикселов со значениями 1 и -1 определяют направление рельефного выделения. В нашем примере продемонстрированы два направления. Во втором примере результат умножения делится на 2, поэтому степень рельефности изображения уменьшается.