Вернуться к разделу "Реализация проекта BookScanLib".
Бинаризация Mixture Modeling из проекта ImageJ применяется для преобразования серой (8-битной) растровой картинки в чёрно-белую (1-битная).
Алгоритм Mixture Modeling анализирует обрабатываемую картинку и автоматически вычисляет порог бинаризации - единый для всей картинки (т.е это глобальная бинаризация). Найденный порог подаётся на вход обыкновенной пороговой бинаризации (например, в библиотеке FreeImage для этого есть функция FreeImage_Threshold).
Я написал простейшую консольную программу для демонстрации работы Mixture Modeling Thresholding. На входе она принимает следующие параметры:
mix_mod_thres <input_file>
На выходе программа выдаёт этот же файл, обработанный этим алгоритмом.
Программа работает только с серыми изображениями.
Всё необходимое для тестирования этой программы (компиляционный проект, готовый экзешник, файл-пример и bat-файлы для тестирования программы) я оформил в небольшой пакет:
Скачать пакет mix_mod_thres (45 КБ)
(Для работы программы требуется 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 /* * Mixture Modeling algorithm * * Copyright (c) 2003 by Christopher Mei (christopher.mei@sophia.inria.fr) * and Maxime Dauphin * * This algorithm thresholds the image using a gray-level * histogram Gaussian characterisation. * **/ /* This algorithm separates the histogram of an image into two classes using a Gaussian model. It then calculates the image threshold as the intersection of these two Gaussians. This thresholding technique has the advantage of finding a threshold that is in certain cases closer to real world data. The Gaussian parameters can also be used to caracterise the two regions obtained. The plugin returns a histogram with the two Gaussians, the parameters obtained (average, standard deviation, threshold) and the thresholded image. */ // This algorithm was taken from the ImageJ sourcecodes // http://rsb.info.nih.gov/ij/plugins/mixture-modeling.html // and adopted for the FreeImage library // // Copyright (C) 2007-2008: // monday2000 monday2000@yandex.ru #include "FreeImage.h" #include "Utilities.h" #define INDEX_MAX 253 class MMClass { public: int histogram[256]; /** The index must vary between 1 and 253 C1 : [0;index] C2 : [index+1; 255] **/ int index; double mu1, mu2; double sigma2_1, sigma2_2; double mult1, mult2; double twoVariance1, twoVariance2; double max1, max2; int cardinal1, cardinal2; int cardinal; MMClass(FIBITMAP* src_dib); double calculateMax(int index); void setValues(); double gamma1(int i); double gamma2(int i); }; //////////////////////////////////////////////////////////////////////////////// // This class implements a MMClass. MMClass::MMClass(FIBITMAP* src_dib) { unsigned width = FreeImage_GetWidth(src_dib); unsigned height = FreeImage_GetHeight(src_dib); unsigned src_pitch = FreeImage_GetPitch(src_dib); BYTE* src_bits = (BYTE*)FreeImage_GetBits(src_dib); // The image raster BYTE* lines; cardinal = width*height; // build histogram first memset(histogram,0,256*sizeof(int)); // for each line for ( unsigned y = 0; y < height; y++ ) { lines = src_bits + y * src_pitch; // for each pixel for ( unsigned x = 0; x < width; x++) { histogram[lines[x]]++; } } index = 0; } //////////////////////////////////////////////////////////////////////////////// double MMClass::calculateMax(int index) { double sum = histogram[index]; double num = 1; if(index-1>=0) { sum += histogram[index-1]; num++; } if(index+1<255) { sum += histogram[index+1]; num++; } return sum/num; } //////////////////////////////////////////////////////////////////////////////// void MMClass::setValues() { mu1 = 0; mu2 = 0; sigma2_1 = 0; sigma2_2 = 0; max1 = 0; max2 = 0; cardinal1 = 0; cardinal2 = 0; int i = 0; for(i=0; i<=index ; i++) { cardinal1 += histogram[i]; mu1 += i*histogram[i]; } for(i=index+1; i<256 ; i++) { cardinal2 += histogram[i]; mu2 += i*histogram[i]; } if(cardinal1 == 0) { mu1 = 0; sigma2_1 = 0; } else mu1 /= (double)cardinal1; if(cardinal2 == 0) { mu2 = 0; sigma2_2 = 0; } else mu2 /= (double)cardinal2; if( mu1 != 0 ) { for(int i=0; i<=index ; i++) sigma2_1 += histogram[i]*pow(i-mu1,2); sigma2_1 /= (double)cardinal1; max1 = calculateMax((int) mu1); mult1 = (double) max1; twoVariance1 = 2*sigma2_1; } if( mu2 != 0 ) { for(i=index+1; i<256 ; i++) sigma2_2 += histogram[i]*pow(i-mu2,2); sigma2_2 /= (double)cardinal2; max2 = calculateMax((int) mu2); mult2 = (double) max2; twoVariance2 = 2*sigma2_2; } } //////////////////////////////////////////////////////////////////////////////// double MMClass::gamma1(int i) { if(sigma2_1 == 0) return 0; return (double)(mult1*exp(-(pow((double)i-mu1,2))/twoVariance1)); } //////////////////////////////////////////////////////////////////////////////// double MMClass::gamma2(int i) { if(sigma2_2 == 0) return 0; return (double)(mult2*exp(-(pow((double)i-mu2,2))/twoVariance2)); } //////////////////////////////////////////////////////////////////////////////// double findThreshold(int mu1, int mu2, MMClass& mmc) { double min = 9999999; int threshold = 0; int differenceGamma; for(int i=mu1; i<mu2; i++) { differenceGamma = mmc.gamma1(i) - mmc.gamma2(i); double val = (double)pow(differenceGamma,2.0); if(min>val) { min = val; threshold = i; } } return threshold; } //////////////////////////////////////////////////////////////////////////////// double calculateError(MMClass& mmc) { double error = 0; double gamma; for(int i=0; i<256; i++) { gamma = mmc.gamma1(i) + mmc.gamma2(i); error += pow(gamma - mmc.histogram[i],2); } return error/256; } //////////////////////////////////////////////////////////////////////////////// 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)); } //////////////////////////////////////////////////////////////////////////////// FIBITMAP* ProcessFilter(FIBITMAP* src_dib) { // get source image size 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); unsigned btpp = bpp/8; 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; MMClass mmc(src_dib); // initialize the helper class int threshold = 0; double error = 0; double errormin = 9999999; double mu1 = 0, mu2 = 0; // Start for (int i=0; i<INDEX_MAX; i++) // INDEX_MAX = 253 { mmc.index++; mmc.setValues(); error = calculateError(mmc); if(error<errormin) { errormin = error; threshold = mmc.index; mu1 = mmc.mu1; mu2 = mmc.mu2; } } mmc.index = threshold; mmc.setValues(); printf("Direct threshold = %d\n", threshold); threshold = (int)findThreshold((int)mu1, (int)mu2, mmc); printf("Real threshold = %d\n", threshold); BYTE val; unsigned x,y; // for each line for ( y = 0; y < height; y++ ) { lined = dst_bits + y * dst_pitch; lines = src_bits + y * src_pitch; // for all pixels for ( x = 0; x < width; x++) { 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)); 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 != 2) { printf("Usage : mix_mod_thres <input_file>\n"); return 0; } FIBITMAP *dib = GenericLoader(argv[1], 0); if (dib) { // bitmap is successfully loaded! if (FreeImage_GetImageType(dib) == FIT_BITMAP) { if (FreeImage_GetBPP(dib) == 8) { FIBITMAP* dst_dib = ProcessFilter(dib); 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 класса (до текущего индекса и после).
- Находим по сложным формулам всевозможные производные коэффициенты, характеризующие оба класса.
- Находим 2 интегральных коэффициента, характеризующих оба класса - на базе ранее полученных коэффициентов.
- Находим минимальную разницу между 2 интегральными коэффициентами и запоминаем текущий номер гистограммы и ещё 2 параметра ("стандартные отклонения") 2 классов.
Затем в цикле проходим от одного стандартного отклонения до второго, находим опять 2 интегральных коэффициента (по текущему номеру отклонения), подсчитываем разницу между ними - и запоминаем её наименьшее значение. Это и есть найденный порог.
Делаем обычную пороговую бинаризацию по найденному порогу.