Вернуться к разделу "Реализация проекта BookScanLib ".


8. Программная реализация алгоритма Deskew

При сканировании книга обычно кладётся на предметное стекло сканера по сути дела с небольшим перекосом относительно сторон сканера. Алгоритм Deskew ("de-skew" - от англ. "skew" - наклон, склон, скос, уклон) автоматически устраняет подобный перекос страниц. Рассмотрим пример программной реализации данного алгоритма на базе библиотеки FreeImage.

Есть несколько принципиальных подходов к реализации алгоритма Deskew, отличающиеся по скорости и точности работы, а также по типу основного применяемого алгоритма.

Я взял уже готовую реализацию алгоритма Deskew из проекта PageTools и просто "перебил" её под библиотеку FreeImage. В этой реализации используется т.н. быстрое преобразование Радона. Я пока ещё не разбирался с сутью и принципом работы всего этого - оставим это на будущее.

Я написал простейшую консольную программу для демонстрации работы алгоритма Deskew. На входе она принимает имя графического файла через командную строку, а на выходе выдаёт этот же файл, обработанный алгоритмом Deskew.

Всё необходимое для тестирования этой программы (компиляционный проект, готовый экзешник, файлы-примеры и bat-файлы для тестирования программы) я оформил в небольшой пакет:

Скачать пакет deskew1 (742 КБ)

(Для работы программы требуется FreeImage dll-библиотека из пакета FreeImage DLL v3.9.2 - см. статью 1. Знакомство с FreeImage).

ВАЖНО: Данная реализация алгоритма Deskew работает пока только с чёрно-белыми изображениями текста. Не пытайтесь подать на вход алгоритма серые или цветные картинки текста. Над устранением этого ограничения подумаем позже.

Рассмотрим исходные коды этой программы:


//	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
// ==========================================================
// The PageTools Deskew algorithm adapted for the FreeImage 3.92
//
// This deskew algorithm is based on the fast Radon transform algorithm
//
// PageTools Project: http://pagetools.sourceforge.net/
//
// ==========================================================
#include <math.h>
#include <memory.h>
#include <stdio.h>

#include "FreeImage.h"

extern FIBITMAP *DLL_CALLCONV BSL_RotateClassic(FIBITMAP *dib, double angle);

// ----------------------------------------------------------

/**
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);
	
	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
		FIBITMAP *dib = FreeImage_Load(fif, lpszPathName, flag);
		
		// unless a bad file format, we are done !
		return dib;
	}
	
	return NULL;
}

// ----------------------------------------------------------

unsigned int PageTools_Next_Pow2(unsigned int n)
{
    unsigned int retval=1;
    while(retval<n){
        retval<<=1;
    }
    return retval;
}

// ----------------------------------------------------------

void PageTools_Radon(FIBITMAP *dib, int sign, unsigned int sharpness[])
{
    unsigned char* bitcount=new unsigned char[256];
	
	unsigned short int *p1, *p2; // Stored columnwise
    
	unsigned int w2=PageTools_Next_Pow2(FreeImage_GetPitch(dib));

	// FreeImage_GetPitch - Returns the width of the bitmap in bytes, rounded
	// to the next 32-bit boundary, also known as pitch or stride or scan
	
	unsigned int w=FreeImage_GetPitch(dib);
	
	unsigned int h=FreeImage_GetHeight(dib);

	// FreeImage_GetHeight - Returns the height of the bitmap in pixel units.
	
    unsigned int s=h*w2;
    p1=new unsigned short int[s];
    p2=new unsigned short int[s];
    // Fill in the first table
    memset(p1, 0, sizeof(unsigned short int)*s);
    unsigned int ir, ic;

	unsigned int i, j, cnt;
	
    for(i=0; i<256; i++)
	{
		j=i, cnt=0;
        do cnt+=j&1; while(j>>=1);
        bitcount[i]=cnt;		
	}
	
    for(ir=0; ir<h; ir++)
	{        
		unsigned char const *scl= FreeImage_GetScanLine(dib, ir);
		
		// DLL_API BYTE *DLL_CALLCONV FreeImage_GetScanLine(FIBITMAP *dib, int);
		//
		// Returns a pointer to the start of the given scanline in the bitmap’s data-bits.
		// It is up to you to interpret these bytes correctly, according to the results of
		// FreeImage_GetBPP and FreeImage_GetImageType

		unsigned char val;
		
        for(ic=0; ic<w; ic++)
		{
            if(sign>0)
			{
				val = scl[w-1-ic];

                p1[h*ic+ir]=bitcount[val];
				
            }
			else
			{
				val = scl[ic];				

                p1[h*ic+ir]=bitcount[val];
            }
        }
    }    
	
    // Iterate
    unsigned short int *x1=p1;
    unsigned short int *x2=p2;
    unsigned int step=1;
    for(;;){
        for(i=0; i<w2; i+=2*step){
            for(j=0; j<step; j++){
                // Columns-sources:
                unsigned short int *s1=x1+h*(i+j);
                unsigned short int *s2=x1+h*(i+j+step);
                
                // Columns-targets:
                unsigned short int *t1=x2+h*(i+2*j);
                unsigned short int *t2=x2+h*(i+2*j+1);
                unsigned int m;
                for(m=0; m<h; m++){
                    t1[m]=s1[m];
                    t2[m]=s1[m];
                    if(m+j<h){
                        t1[m]+=s2[m+j];
                    }
                    if(m+j+1<h){
                        t2[m]+=s2[m+j+1];
                    }
                }
            }
        }
        // Swap the tables:
        unsigned short int *aux=x1;
        x1=x2;
        x2=aux;
        // Increase the step:
        step*=2;
        if(step>=w2) break;
    }
    // Now, compute the sum of squared finite differences:
    for(ic=0; ic<w2; ic++){
        unsigned int acc=0;
        unsigned short int *col=x1+h*ic;
        for(ir=0; ir+1<h; ir++){
            int diff=(int)(col[ir])-(int)(col[ir+1]);
            acc+=diff*diff;
        }
        sharpness[w2-1+sign*ic]=acc;
        
    }
    delete[] p1;
    delete[] p2;
    delete[] bitcount;    
}

// ----------------------------------------------------------

double PageTools_FindSkew(FIBITMAP *dib)
{
	unsigned int w2=PageTools_Next_Pow2(FreeImage_GetPitch(dib));
	
	// FreeImage_GetPitch - Returns the width of the bitmap in bytes, rounded
	// to the next 32-bit boundary, also known as pitch or stride or scan
	
    unsigned int ssize=2*w2-1; // Size of sharpness table
    unsigned int *sharpness=new unsigned int[ssize];
    PageTools_Radon(dib, 1, sharpness);
    PageTools_Radon(dib, -1, sharpness);
    unsigned int i, imax=0;
    unsigned int vmax=0;
    double sum=0.;
    for(i=0; i<ssize; i++)
	{
        unsigned int s=sharpness[i];

        if(s>vmax)
		{
            imax=i;
            vmax=s;
        }
        sum+=s;
    }
	
	unsigned int h=FreeImage_GetHeight(dib); 
	
	// FreeImage_GetHeight - Returns the height of the bitmap in pixel units.
	
    if(vmax<=3*sum/h){  // Heuristics !!!
        return 0;
    }
    int iskew= (int)imax-int(w2)+1;
    delete[] sharpness;
    return atan((double)iskew/(8*w2))*FreeImage_GetBPP(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 : deskew2 <input file name>\n");
		return 1;
	}
	
	FIBITMAP *dib = GenericLoader(argv[1], 0);
	
	if (dib)
	{		
		// bitmap successfully loaded!
		
		double angle = PageTools_FindSkew(dib);		
		
        // Convert radians to degrees and change the sign:
		
		angle = -57.295779513082320876798154814105*angle;
		
		printf("Skew angle = %f\n", angle);
		
		// Rotate the monochrome image by the found skew angle
		
		FIBITMAP *rotated;		

		rotated = BSL_RotateClassic(dib, -angle);
		
		// save the deskewed bitmap
		const char *output_filename = "deskewed.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, rotated, output_filename, 0);
		}
		
		// free the loaded FIBITMAP
		FreeImage_Unload(dib);
		
		// free the loaded FIBITMAP
		FreeImage_Unload(rotated);		
	}
	
	// call this ONLY when linking with FreeImage as a static library
#ifdef FREEIMAGE_LIB
	FreeImage_DeInitialise();
#endif // FREEIMAGE_LIB
	
	return 0;
}

Выводы:

Алгоритм Deskew состоит из двух функций: FindSkew (определить угол перекоса) и Rotate (повернуть изображение на найденный угол).

Данная реализация функции FindSkew (это то, что я взял из проекта PageTools) достаточно хороша. Основные её достоинства - неплохая точность (до десятых долей градуса) и довольно высокая скорость работы. Можете убедиться в этом самостоятельно (в консольном окне выдаётся вычисленное значение угла перекоса).

Я немного изменил алгоритм FindSkew, взятый из проекта PageTools - а именно, найденный угол перекоса умножается на глубину цвета картинки (в битах). Это позволило применять данный алгоритм не только к чёрно-белым изображениям (как в проекте PageTools), но также и к серым, и к цветным изображениям.

Реализация функции Rotate взята из предыдущего пункта 7. Улучшенный алгоритм RotateClassic.

Данная реализация алгоритма Deskew полностью готова к использованию в сторонних программах. Правда, я пока даже и не пытался оптимизировать её по скорости - не исключено, что это возможно.

И ещё: неизвестно, будет ли данный алгоритм сбиваться с толку областями скана, занимаемыми картинками (как СканКромсаторный Deskew) - нужно проверять.


Полезные ссылки

В этом разделе собраны все исходники алгоритмов Deskew, которые мне удалось найти в Интернете после длительных и тщательных поисков. Если у Вас есть какие-то исходники алгоритмов Deskew - пришлите мне, пожалуйста.

1. PageTools - аналогичный российский проект (оттуда и были взяты исходники для данной статьи).

2. Определение угла поворота страницы - статья с теоретическим описанием алгоритма Deskew. К сожалению, эта статья явна неудачна - содержит опечатки в математических выкладках, написана туманно и заумно-наукообразно без нужды. Смысл же изложенного там алгоритма понять вообще невозможно.

3. Leptonica - наиболее известный учебный проект по сканобработке. Содержит программную реализацию алгоритма Deskew с открытыми исходниками.

4. unpaper 0.2 - Немецкий проект. Работает под Linux и на базе формата PBM (используя штатные средства Linux по работе с PBM). Содержит программную реализацию алгоритма Deskew с открытыми исходниками.

5. durendal TWiki.Deskew - Англоязычный проект. Содержит в основном исследования по алгоритму deskew (очень ценные).

6. Алгоритм deskew (ОС Linux)  (5 КБ)     Источник: snake4d   (2004 год).   Лицензия: GPL.

7. Статья  Measuring document image skew and orientation  (174 КБ)     Источник: Cptn_Cook     Формат - PDF.     Язык - английский. Авторы:  Dan S. Bloomberg, Gary E. Kopec and Lakshmi Dasari.
Presented at IS&T/SPIE EI’95, Conference 2422: Document Recognition II pp. 302-316, Feb 6-7, 1995, San Jose, CA.

8. How to Deskew an Image - хорошая статья с исходниками.

Hosted by uCoz