Вернуться к разделу "Реализация проекта BookScanLib".
Бинаризация Sauvola из проекта gamera применяется для преобразования серой (8-битной) растровой картинки в чёрно-белую (1-битная).
Алгоритм Sauvola Thresholding анализирует обрабатываемую картинку и автоматически вычисляет порог бинаризации - индивидуальный для каждого пикселя (т.е это локальная или адаптивная бинаризация). Найденный порог используется для обыкновенной пороговой бинаризации в отношении текущего пикселя.
Я написал простейшую консольную программу для демонстрации работы Sauvola Thresholding. На входе она принимает следующие параметры:
sauvola <input_file> <region_size (int)> <sensitivity (double)> <dynamic_range (int)> <lower_bound (int)> <upper_bound (int)>
region_size - это размер обрабатывающего окна . 15 по умолчанию.
sensitivity - чувствительность. 0.5 по умолчанию.
dynamic_range - динамический диапазон отклонения. 128 по умолчанию.
lower_bound - нижний глобальный порог. 20 по умолчанию.
upper_bound - верхний глобальный порог. 150 по умолчанию.
Точность дробных - до одной десятой.
На выходе программа выдаёт этот же файл, обработанный этим алгоритмом.
Программа работает только с серыми изображениями.
Всё необходимое для тестирования этой программы (компиляционный проект, готовый экзешник, файл-пример и bat-файлы для тестирования программы) я оформил в небольшой пакет:
Скачать пакет sauvola (40 КБ)
(Для работы программы требуется FreeImage dll-библиотека из пакета FreeImage DLL v3.9.2 - см. статью 1. Знакомство с FreeImage).
Рассмотрим исходные коды этой программы:
/* * * Copyright (C) 2005 John Ashley Burgoyne and Ichiro Fujinaga * 2007 Uma Kompella and Christoph Dalitz * * 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. */ /* Creates a binary image using Sauvola's adaptive algorithm. Sauvola, J. and M. Pietikainen. 2000. Adaptive document image binarization. *Pattern Recognition* 33: 225--236. Like the QGAR library, there are two extra global thresholds for the lightest and darkest regions. int *region_size* : default=15 The size of the region in which to calculate a threshold. double *sensitivity* : default=0.5 The sensitivity weight on the adjusted variance. int *dynamic_range* : range=(1,255), default=128 The dynamic range of the variance. int *lower bound* : range=(0,255), default=20 A global threshold beneath which all pixels are considered black. int *upper bound* : range=(0,255), default=150 A global threshold above which all pixels are considered white. */ // This algorithm was taken from the gamera.sf.net sourcecodes // and adopted for the FreeImage library // // Copyright (C) 2007-2008: // monday2000 monday2000@yandex.ru #include "FreeImage.h" #include "Utilities.h" //////////////////////////////////////////////////////////////////////////////// inline void SetPixel(BYTE *bits, unsigned x, BYTE* value) { // this function is simplified from FreeImage_SetPixelIndex *value ? bits[x >> 3] |= (0x80 >> (x & 0x7)) : bits[x >> 3] &= (0xFF7F >> (x & 0x7)); } //////////////////////////////////////////////////////////////////////////////// void mean_filter(FIBITMAP* src_dib, int region_size, double* p_mean) { 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 x, y, i, j; int k_index; BYTE* src_bits = (BYTE*)FreeImage_GetBits(src_dib); // The image raster BYTE* src_end_row = src_bits + (height-1) * pitch; int end_col = width - 1; BYTE* lines, *linek; int sum; unsigned area = region_size * region_size; for (y = 0; y < height; y++) { lines = src_bits + y * pitch; for (x = 0; x < width; x++) { sum = 0; // kernel processing for (i = 0; i < region_size; i++) for (j = 0; j < region_size; j++) { linek = lines + (i-1) * pitch; if (linek < src_bits) linek = src_bits; if (linek > src_end_row) linek = src_end_row; k_index = x+j-1; if (k_index < 0) k_index = 0; if (k_index > end_col) k_index = end_col; sum += linek[k_index]; } p_mean[y*width+x] = (double)sum / area; } } } //////////////////////////////////////////////////////////////////////////////// void variance_filter(FIBITMAP* src_dib, int region_size, double* p_mean, double* p_var) { 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 y, x, i, j; int k_index; BYTE* src_bits = (BYTE*)FreeImage_GetBits(src_dib); // The image raster BYTE* src_end_row = src_bits + (height-1) * pitch; int end_col = width - 1; BYTE* lines, *linek; int sum; unsigned area = region_size * region_size; for (y = 0; y < height; y++) { lines = src_bits + y * pitch; for (x = 0; x < width; x++) { sum = 0; // kernel processing for (i = 0; i < region_size; i++) for (j = 0; j < region_size; j++) { linek = lines + (i-1) * pitch; if (linek < src_bits) linek = src_bits; if (linek > src_end_row) linek = src_end_row; k_index = x+j-1; if (k_index < 0) k_index = 0; if (k_index > end_col) k_index = end_col; sum += linek[k_index]*linek[k_index]; } p_var[y*width+x] = (double)sum / area - p_mean[y*width+x] * p_mean[y*width+x]; } } } //////////////////////////////////////////////////////////////////////////////// FIBITMAP* ProcessFilter(FIBITMAP* src_dib, int region_size, double sensitivity, int dynamic_range, int lower_bound, int upper_bound) { unsigned width = FreeImage_GetWidth(src_dib); unsigned height = FreeImage_GetHeight(src_dib); unsigned src_pitch = FreeImage_GetPitch(src_dib); unsigned bpp = FreeImage_GetBPP(src_dib); FIBITMAP* dst_dib = FreeImage_Allocate(width, height, 1); // Build a monochrome palette RGBQUAD *pal = FreeImage_GetPalette(dst_dib); pal[0].rgbRed = pal[0].rgbGreen = pal[0].rgbBlue = 0; pal[1].rgbRed = pal[1].rgbGreen = pal[1].rgbBlue = 255; 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* lines, *lined; unsigned x, y; BYTE mean; double deviation; double adjusted_deviation; int threshold; BYTE val; int int_size = height*width*sizeof(double); double* p_mean = (double*)malloc(int_size); double* p_var = (double*)malloc(int_size); memset(p_mean,0,int_size); memset(p_var,0,int_size); mean_filter(src_dib, region_size, p_mean); variance_filter(src_dib, region_size, p_mean, p_var); for (y = 0; y < height; y++) { lines = src_bits + y * src_pitch; lined = dst_bits + y * dst_pitch; for (x = 0; x < width; x++) { mean = p_mean[y*width+x]; deviation = sqrt(p_var[y*width+x]); adjusted_deviation = 1.0 - deviation / (double)dynamic_range; if (lines[x] < lower_bound) val = 0; else if (lines[x] > upper_bound) val = 255; else { threshold = (int)mean + (1.0 - sensitivity * adjusted_deviation); val = (BYTE) ( ( lines[x] >= threshold ) ? 255 : 0 ); } SetPixel(lined, x, &val); } } // Copying the DPI... FreeImage_SetDotsPerMeterX(dst_dib, FreeImage_GetDotsPerMeterX(src_dib)); FreeImage_SetDotsPerMeterY(dst_dib, FreeImage_GetDotsPerMeterY(src_dib)); free(p_mean); free(p_var); return dst_dib; } //////////////////////////////////////////////////////////////////////////////// /** 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 != 7) { printf("Usage : sauvola <input_file> <region_size (int)> <sensitivity (double)> <dynamic_range (int)> <lower_bound (int)> <upper_bound (int)>\n"); return 0; } FIBITMAP *dib = GenericLoader(argv[1], 0); int region_size = atoi(argv[2]); double sensitivity = atof(argv[3]); int dynamic_range = atoi(argv[4]); int lower_bound = atoi(argv[5]); int upper_bound = atoi(argv[6]); if (dib) { // bitmap is successfully loaded! if (FreeImage_GetImageType(dib) == FIT_BITMAP) { if (FreeImage_GetBPP(dib) == 8) { FIBITMAP* dst_dib = ProcessFilter(dib, region_size, sensitivity, dynamic_range, lower_bound, upper_bound); 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; } |
Составляются 2 вспомогательных т.н. "интегральных" изображения. Первое заполняется так: квадратная апертура пробегает по всем пикселям, суммирует апертуру, делит на площадь и вставляет в результат. Получается усреднённое изображение.
Второе заполняется так: квадратная апертура пробегает по всем пикселям, суммирует квадраты в апертуре, делит на площадь и вычитает квадрат соответствующего пикселя из первого вспомогательного изображения и вставляет в результат. Получается отклонённое изображение.
Затем в основном цикле проходим по всем пикселям. Если цвет меньше глобального минимума - ставим чёрный, если больше глобального максимума - ставим белый. Если между глобальными порогами - делаем собственно адаптивную бинаризацию: при помощи соответствующих пикселей обоих вспомогательных изображений, чувствительности и динамического диапазона вычисляем по формуле порог для текущего пикселя. Бинаризуем текущий пиксель по этому порогу и вставляем его в результат.