Вернуться к разделу "Реализация проекта BookScanLib".
Бинаризация Sauvola из проекта OCRopus применяется для преобразования серой (8-битной) растровой картинки в чёрно-белую (1-битная).
Алгоритм Sauvola Thresholding анализирует обрабатываемую картинку и автоматически вычисляет порог бинаризации - индивидуальный для каждого пикселя (т.е это локальная или адаптивная бинаризация). Найденный порог используется для обыкновенной пороговой бинаризации в отношении текущего пикселя.
Главный недостаток этого алгоритма в том, что он идёт под Apache License - что меня совершенно не устраивает - т.к. мне нужно только GPL 2 и выше. Поэтому прийдётся искать такой же алгоритм, но под лицензией GPL 2 и выше.
Я написал простейшую консольную программу для демонстрации работы Sauvola Thresholding. На входе она принимает следующие параметры:
sauvola_ocropus <input_file> <size (int)> <weight (double)>
Size - это размер обрабатывающего окна.
Weight - вес фильтра. Точность - до одной десятой.
На выходе программа выдаёт этот же файл, обработанный этим алгоритмом.
Программа работает только с серыми изображениями.
Всё необходимое для тестирования этой программы (компиляционный проект, готовый экзешник, файл-пример и bat-файлы для тестирования программы) я оформил в небольшой пакет:
Скачать пакет sauvola_ocropus (41 КБ)
(Для работы программы требуется FreeImage dll-библиотека из пакета FreeImage DLL v3.9.2 - см. статью 1. Знакомство с FreeImage).
Рассмотрим исходные коды этой программы:
// -*- C++ -*-
// Copyright 2006-2008 Deutsches Forschungszentrum fuer Kuenstliche Intelligenz
// or its licensors, as applicable.
//
// You may not use this file except under the terms of the accompanying license.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you
// may not use this file except in compliance with the License. You may
// obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Project: OCRopus
// File: ocr-binarize-sauvola.cc
// Purpose: An efficient implementation of the Sauvola's document binarization
// algorithm based on integral images as described in
// F. Shafait, D. Keysers, T.M. Breuel. "Efficient Implementation of
// Local Adaptive Thresholding Techniques Using Integral Images".
// Document Recognition and Retrieval XV, San Jose.
//
// Responsible: Faisal Shafait (faisal.shafait@dfki.de)
// Reviewer:
// Primary Repository:
// Web Sites: www.iupr.org, www.dfki.de
// An efficient implementation of the Sauvola's document
// binarization algorithm based on integral images.
// This algorithm was taken from the OCRopus sourcecodes
// and adopted for the FreeImage library
//
// Copyright (C) 2007-2008:
// monday2000 monday2000@yandex.ru
#include "FreeImage.h"
#include "Utilities.h"
#define MAXVAL 256
// float k = 0.3,"Weighting factor"
// int w = 40,"Local window size. Should always be positive"
// k>=0.05 && k<=0.95;
// w>0 && k<1000;
////////////////////////////////////////////////////////////////////////////////
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, int w, double k)
{
const unsigned width = FreeImage_GetWidth(src_dib);
const 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 whalf = w>>1; // Half of window size
// Calculate the integral image, and integral of the squared image
int int_size = height*width*sizeof(unsigned)*sizeof(unsigned);
int ws = width*sizeof(unsigned);
unsigned* p_integral_image = (unsigned*)malloc(int_size);
unsigned* p_rowsum_image = (unsigned*)malloc(int_size);
unsigned* p_integral_sqimg = (unsigned*)malloc(int_size);
unsigned* p_rowsum_sqimg = (unsigned*)malloc(int_size);
memset(p_integral_image,0,int_size);
memset(p_rowsum_image,0,int_size);
memset(p_integral_sqimg,0,int_size);
memset(p_rowsum_sqimg,0,int_size);
int xmin,ymin,xmax,ymax;
double diagsum,idiagsum,diff,sqdiagsum,sqidiagsum,sqdiff,area;
double mean,std,threshold;
unsigned i,j;
for(j=0; j<height; j++)
{
lines = src_bits + j * src_pitch;
p_rowsum_image[j*ws] = lines[0];
p_rowsum_sqimg[j*ws] = lines[0]*lines[0];
}
for(j=0; j<height; j++)
{
lines = src_bits + j * src_pitch;
for(i=1; i<width; i++)
{
p_rowsum_image[j*ws+i] = p_rowsum_image[(j-1)*ws+i] + lines[i];
p_rowsum_sqimg[j*ws+i] = p_rowsum_sqimg[(j-1)*ws+i] + lines[i]*lines[i];
}
}
for(i=0; i<width; i++)
{
p_integral_image[i] = p_rowsum_image[i];
p_integral_sqimg[i] = p_rowsum_sqimg[i];
}
for(j=1; j<height; j++)
{
for(i=0; i<width; i++)
{
p_integral_image[j*ws+i] = p_integral_image[j*ws+(i-1)] + p_rowsum_image[j*ws+i];
p_integral_sqimg[j*ws+i] = p_integral_sqimg[j*ws+(i-1)] + p_rowsum_sqimg[j*ws+i];
}
}
//Calculate the mean and standard deviation using the integral image
BYTE val;
for(j=0; j<height; j++)
{
lined = dst_bits + j * dst_pitch;
lines = src_bits + j * src_pitch;
for(i=0; i<width; i++)
{
xmin = MAX(0,int(i-whalf));
xmax = MIN(width-1,i+whalf);
ymin = MAX(0,int(j-whalf));
ymax = MIN(height-1,j+whalf);
area = (xmax-xmin+1)*(ymax-ymin+1);
// area can't be 0 here
// proof (assuming whalf >= 0):
// we'll prove that (xmax-xmin+1) > 0,
// (ymax-ymin+1) is analogous
// It's the same as to prove: xmax >= xmin
// width - 1 >= 0 since width > i >= 0
// i + whalf >= 0 since i >= 0, whalf >= 0
// i + whalf >= i - whalf since whalf >= 0
// width - 1 >= i - whalf since width > i
// --IM
if(!xmin && !ymin)
{ // Point at origin
diff = p_integral_image[ymax*ws+xmax];
sqdiff = p_integral_sqimg[ymax*ws+xmax];
}
else if(!xmin && ymin)
{ // first column
//printf("ymin=%d\n",ymin);
diff = p_integral_image[ymax*ws+xmax] - p_integral_image[(ymin-1)*ws+xmax];
sqdiff = p_integral_sqimg[ymax*ws+xmax] - p_integral_sqimg[(ymin-1)*ws+xmax];
}
else if(xmin && !ymin)
{ // first row
diff = p_integral_image[ymax*ws+xmax] - p_integral_image[ymax*ws+(xmin-1)];
sqdiff = p_integral_sqimg[ymax*ws+xmax] - p_integral_sqimg[ymax*ws+(xmin-1)];
}
else
{ // rest of the image
diagsum = p_integral_image[ymax*ws+xmax] + p_integral_image[(ymin-1)*ws+(xmin-1)];
idiagsum = p_integral_image[ymax*ws+(xmin-1)] + p_integral_image[(ymin-1)*ws+xmax];
diff = diagsum - idiagsum;
sqdiagsum = p_integral_sqimg[ymax*ws+xmax] + p_integral_sqimg[(ymin-1)*ws+(xmin-1)];
sqidiagsum = p_integral_sqimg[ymax*ws+(xmin-1)] + p_integral_sqimg[(ymin-1)*ws+xmax];
sqdiff = sqdiagsum - sqidiagsum;
}
mean = diff/area;
std = sqrt((sqdiff - diff*diff/area)/(area-1));
threshold = mean*(1+k*((std/128)-1));
val = (BYTE) ( ( lines[i] >= threshold ) ? 255 : 0 );
SetPixel(lined, i, &val);
}
}
// Copying the DPI...
FreeImage_SetDotsPerMeterX(dst_dib, FreeImage_GetDotsPerMeterX(src_dib));
FreeImage_SetDotsPerMeterY(dst_dib, FreeImage_GetDotsPerMeterY(src_dib));
free(p_integral_image);
free(p_rowsum_image);
free(p_integral_sqimg);
free(p_rowsum_sqimg);
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 != 4) {
printf("Usage : sauvola_ocropus <input_file> <size (int)> <weight (double)>\n");
return 0;
}
FIBITMAP *dib = GenericLoader(argv[1], 0);
unsigned size = atoi(argv[2]); // Local window size. Should always be positive
// default = 40; w>0 && k<1000
double weight = atof(argv[3]); //Weighting factor
// default = 0.3; k>=0.05 && k<=0.95;
if (dib)
{
// bitmap is successfully loaded!
if (FreeImage_GetImageType(dib) == FIT_BITMAP)
{
if (FreeImage_GetBPP(dib) == 8)
{
FIBITMAP* dst_dib = ProcessFilter(dib, size, weight);
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 "габаритные" (по диагонали) разницы апертуры - по к.суммам и к.кв.сумм. Находим площадь апертуры. Находим среднее по к.сумме. С помощью извлечения корня и составления пропорции пир участии веса находим порог бинаризации для текущего пикселя и преобразуем его в чёрно-белый.
1. OCRopus.
2. Описание алгоритма (PDF ENG 386 КБ)