Вернуться к разделу "Реализация проекта BookScanLib ".
При сканировании книга обычно кладётся на предметное стекло сканера по сути дела с небольшим перекосом относительно сторон сканера. Алгоритм 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 - хорошая статья с исходниками.