Вернуться к разделу "Реализация проекта BookScanLib".
Фильтр B-Spline Smooth (сглаживание кубическим Б-сплайном 4-го порядка) применяется в библиотеке FreeImage для сглаживания растровой картинки в функции FreeImage_Rescale (изменение размера).
Этот алгоритм является двухпроходным. Это самый "сглаживающий" по качеству алгоритм - из всех остальных сглаживающих фильтров - Box, Bilinear, Mitchell and Netravali's Bicubic filter, Catmull-Rom, Lanczos3.
Я взял исходные коды фунции FreeImage_Rescale, немного упростил их - для случая неизменного размера на входе и выходе - и объединил в один файл. Это нужно как своеобразная "заготовка" для дальнейших экспериментов.
Дело в том, что у меня есть идея попробовать придать данному алгоритму свойство селективности (адаптивности) - чтобы он сглаживал только фон, не трогая буквы текста.
Таким образом, я надеюсь попытаться повторить алгоритм Smooth из программы Corel PHOTO-PAINT.
Можно поппробовать "взять" свойство селективности из таких алгоритмов, как 14. Фильтр Selective Gaussian Blur, 15. Простейший фильтр Adaptive Smoothing, 16. Простейший фильтр Illuminance Correction, и "скрестить" это свойство селективности с данным алгоритмом.
Однако тут есть одна проблема: данный алгоритм - двухпроходный, а все перечисленные "селективные" - однопроходные.
Я написал простейшую консольную программу для демонстрации работы B-Spline Smooth. На входе она принимает только имя файла и никаких параметров:
bspline <input_file>
На выходе программа выдаёт этот же файл, обработанный этим алгоритмом.
Программа работает только с серыми изображениями и цветными изображениями.
Всё необходимое для тестирования этой программы (компиляционный проект, готовый экзешник, файл-пример и bat-файлы для тестирования программы) я оформил в небольшой пакет:
Скачать пакет bspline (56 КБ)
(Для работы программы требуется FreeImage dll-библиотека из пакета FreeImage DLL v3.9.2 - см. статью 1. Знакомство с FreeImage).
Рассмотрим исходные коды этой программы:
// ========================================================== // 4th order (cubic) b-spline smoothing // // Design and implementation by // - Hervй Drolon (drolon@infonie.fr) // - Detlev Vendt (detlev.vendt@brillit.de) // // This file is part of FreeImage 3 // // COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY // OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES // THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE // OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED // CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT // THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY // SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL // PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER // THIS DISCLAIMER. // // Use at your own risk! // ========================================================== // This algorithm was taken from the FreeImage sourcecodes // and simplified for a non-rescaling. // // Copyright (C) 2007-2008: // monday2000 monday2000@yandex.ru // Main reference: // Paul Heckbert, C code to zoom raster images up or down, with nice filtering. // UC Berkeley, August 1989. [online] http://www-2.cs.cmu.edu/afs/cs.cmu.edu/Web/People/ph/heckbert.html // // Heckbert references: // // Oppenheim A.V., Schafer R.W., Digital Signal Processing, Prentice-Hall, 1975 // Hamming R.W., Digital Filters, Prentice-Hall, Englewood Cliffs, NJ, 1983 // Pratt W.K., Digital Image Processing, John Wiley and Sons, 1978 // Hou H.S., Andrews H.C., "Cubic Splines for Image Interpolation and Digital Filtering", // IEEE Trans. Acoustics, Speech, and Signal Proc., vol. ASSP-26, no. 6, pp. 508-517, Dec. 1978. // #include "FreeImage.h" #include "Utilities.h" ///////////////////////////////////////////////////////////////////////////////////////////// // // 4th order (cubic) b-spline // double CBSplineFilter(double dVal) { // Default fixed width = 2 dVal = fabs(dVal); if(dVal < 1) return (4 + dVal*dVal*(-6 + 3*dVal)) / 6; if(dVal < 2) { double t = 2 - dVal; return (t*t*t / 6); } return 0; } ///////////////////////////////////////////////////////////////////////////////////////////// // // Filter weights table. // This class stores contribution information for an entire line (row or column). // class CWeightsTable { // // Sampled filter weight table. // Contribution information for a single pixel // typedef struct { /// Normalized weights of neighboring pixels double *Weights; /// Bounds of source pixels window int Left, Right; } Contribution; private: /// Row (or column) of contribution weights Contribution *m_WeightTable; /// Filter window size (of affecting source pixels) DWORD m_WindowSize; /// Length of line (no. of rows / cols) DWORD m_LineLength; public: /** Constructor Allocate and compute the weights table @param uSize Length (in pixels) of the line buffer */ CWeightsTable(DWORD uSize); /** Destructor Destroy the weights table */ ~CWeightsTable(); /** Retrieve a filter weight, given source and destination positions @param dst_pos Pixel position in destination line buffer @param src_pos Pixel position in source line buffer @return Returns the filter weight */ double getWeight(int dst_pos, int src_pos) { return m_WeightTable[dst_pos].Weights[src_pos]; } /** Retrieve left boundary of source line buffer @param dst_pos Pixel position in destination line buffer @return Returns the left boundary of source line buffer */ int getLeftBoundary(int dst_pos) { return m_WeightTable[dst_pos].Left; } /** Retrieve right boundary of source line buffer @param dst_pos Pixel position in destination line buffer @return Returns the right boundary of source line buffer */ int getRightBoundary(int dst_pos) { return m_WeightTable[dst_pos].Right; } }; //////////////////////////////////////////////////////////////////////////////////////// CWeightsTable::CWeightsTable(DWORD uSize) { DWORD u; double dWidth = 2; // Default fixed width = 2 - for 4th order (cubic) b-spline // allocate a new line contributions structure // // window size is the number of sampled pixels m_WindowSize = 2 * (int)ceil(dWidth) + 1; m_LineLength = uSize; // allocate list of contributions m_WeightTable = (Contribution*)malloc(m_LineLength * sizeof(Contribution)); for(u = 0 ; u < m_LineLength ; u++) { // allocate contributions for every pixel m_WeightTable[u].Weights = (double*)malloc(m_WindowSize * sizeof(double)); } for(u = 0; u < m_LineLength; u++) { // scan through line of contributions double dCenter = (double)u; // reverse mapping // find the significant edge points that affect the pixel int iLeft = MAX (0, (int)floor (dCenter - dWidth)); int iRight = MIN ((int)ceil (dCenter + dWidth), int(uSize) - 1); // cut edge points to fit in filter window in case of spill-off if((iRight - iLeft + 1) > int(m_WindowSize)) { if(iLeft < (int(uSize) - 1 / 2)) { iLeft++; } else { iRight--; } } m_WeightTable[u].Left = iLeft; m_WeightTable[u].Right = iRight; int iSrc = 0; double dTotalWeight = 0; // zero sum of weights for(iSrc = iLeft; iSrc <= iRight; iSrc++) { // calculate weights double weight = CBSplineFilter(dCenter - (double)iSrc); m_WeightTable[u].Weights[iSrc-iLeft] = weight; dTotalWeight += weight; } if((dTotalWeight > 0) && (dTotalWeight != 1)) { // normalize weight of neighbouring points for(iSrc = iLeft; iSrc <= iRight; iSrc++) { // normalize point m_WeightTable[u].Weights[iSrc-iLeft] /= dTotalWeight; } // simplify the filter, discarding null weights at the right iSrc = iRight - iLeft; while(m_WeightTable[u].Weights[iSrc] == 0){ m_WeightTable[u].Right--; iSrc--; if(m_WeightTable[u].Right == m_WeightTable[u].Left) break; } } } } ///////////////////////////////////////////////////////////////////////////////////////////// CWeightsTable::~CWeightsTable() { for(DWORD u = 0; u < m_LineLength; u++) { // free contributions for every pixel free(m_WeightTable[u].Weights); } // free list of pixels contributions free(m_WeightTable); } ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// /// Performs horizontal image filtering void horizontalFilter(FIBITMAP *src, unsigned width, unsigned height, FIBITMAP *dst) { // allocate and calculate the contributions CWeightsTable weightsTable(width); BYTE* src_bits = (BYTE*)FreeImage_GetBits(src); // The image raster BYTE* dst_bits = (BYTE*)FreeImage_GetBits(dst); // The image raster BYTE* lines, *lined; unsigned pitch = FreeImage_GetPitch(src); // Calculate the number of bytes per pixel (1 for 8-bit, 3 for 24-bit) unsigned btpp = FreeImage_GetBPP(src) / 8; // step through rows for(unsigned y = 0; y < height; y++) { lined = dst_bits + y * pitch; lines = src_bits + y * pitch; // process each row for(unsigned x = 0; x < width; x++) { // loop through row double value[3] = {0, 0, 0}; // 3 = 24bpp max int iLeft = weightsTable.getLeftBoundary(x); // retrieve left boundary int iRight = weightsTable.getRightBoundary(x); // retrieve right boundary for(int i = iLeft; i <= iRight; i++) { // scan between boundaries // accumulate weighted effect of each neighboring pixel double weight = weightsTable.getWeight(x, i-iLeft); for (unsigned j = 0; j < btpp; j++) value[j] += (weight * (double)lines[i * btpp + j]); } // clamp and place result in destination pixel for (unsigned j = 0; j < btpp; j++) lined[x * btpp + j] = (BYTE)MIN(MAX((int)0, (int)(value[j] + 0.5)), (int)255); } } } ///////////////////////////////////////////////////////////////////////////////////////////// /// Performs vertical image filtering void verticalFilter(FIBITMAP *src, unsigned width, unsigned height, FIBITMAP *dst) { // allocate and calculate the contributions CWeightsTable weightsTable(height); BYTE* src_bits = (BYTE*)FreeImage_GetBits(src); // The image raster BYTE* dst_bits = (BYTE*)FreeImage_GetBits(dst); // The image raster BYTE* lines, *lined; unsigned pitch = FreeImage_GetPitch(src); // Calculate the number of bytes per pixel (1 for 8-bit, 3 for 24-bit or 4 for 32-bit) unsigned btpp = FreeImage_GetBPP(src) / 8; // step through columns for(unsigned x = 0; x < width; x++) { // process each column for(unsigned y = 0; y < height; y++) { // loop through column double value[3] = {0, 0, 0}; // 3 = 24bpp max int iLeft = weightsTable.getLeftBoundary(y); // retrieve left boundary int iRight = weightsTable.getRightBoundary(y); // retrieve right boundary lined = dst_bits + y * pitch; for(int i = iLeft; i <= iRight; i++) { // scan between boundaries // accumulate weighted effect of each neighboring pixel double weight = weightsTable.getWeight(y, i-iLeft); lines = src_bits + i * pitch; for (unsigned j = 0; j < btpp; j++) value[j] += (weight * (double)lines[x * btpp + j]); } // clamp and place result in destination pixel for (unsigned j = 0; j < btpp; j++) lined[x * btpp + j] = (BYTE)MIN(MAX((int)0, (int)(value[j] + 0.5)), (int)255); } } } ///////////////////////////////////////////////////////////////////////////////////////////// // // This function performs B-Spline smoothing. It works with 8- and 24- buffers. // // References : // [1] Paul Heckbert, C code to zoom raster images up or down, with nice filtering. // UC Berkeley, August 1989. [online] http://www-2.cs.cmu.edu/afs/cs.cmu.edu/Web/People/ph/heckbert.html // [2] Eran Yariv, Two Pass Scaling using Filters. The Code Project, December 1999. // [online] http://www.codeproject.com/bitmap/2_pass_scaling.asp // FIBITMAP* ProcessFilter(FIBITMAP *src) { DWORD width = FreeImage_GetWidth(src); DWORD height = FreeImage_GetHeight(src); unsigned bpp = FreeImage_GetBPP(src); FREE_IMAGE_TYPE image_type = FreeImage_GetImageType(src); // allocate the dst image FIBITMAP *dst = FreeImage_Allocate(width, height, bpp); if(!dst) return NULL; if(bpp == 8) { if(FreeImage_GetColorType(src) == FIC_MINISWHITE) { // build an inverted greyscale palette RGBQUAD *dst_pal = FreeImage_GetPalette(dst); for(int i = 0; i < 256; i++) { dst_pal[i].rgbRed = dst_pal[i].rgbGreen = dst_pal[i].rgbBlue = (BYTE)(255 - i); } } else { // build a greyscale palette RGBQUAD *dst_pal = FreeImage_GetPalette(dst); for(int i = 0; i < 256; i++) { dst_pal[i].rgbRed = dst_pal[i].rgbGreen = dst_pal[i].rgbBlue = (BYTE)i; } } } // allocate a temporary image FIBITMAP *tmp = FreeImage_Allocate(width, height, bpp); if(!tmp) { FreeImage_Unload(dst); return NULL; } // smooth source image horizontally into temporary image horizontalFilter(src, width, height, tmp); // smooth temporary image vertically into result image verticalFilter(tmp, width, height, dst); // free temporary image FreeImage_Unload(tmp); // Copying the DPI... FreeImage_SetDotsPerMeterX(dst, FreeImage_GetDotsPerMeterX(src)); FreeImage_SetDotsPerMeterY(dst, FreeImage_GetDotsPerMeterY(src)); return dst; } //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /** 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 : bspline <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 || FreeImage_GetBPP(dib) == 24) { 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; } |
Как видно, алгоритм довольно сложный - с рассчитываемой таблицей весовых коэффициентов, двухпроходный (т.е. сначала преобразование идёт по колонкам, а потом - по рядам).
Но сглаживает он действительно здорово - даже и без свойства селективности. Попробуйте этот алгоритм самостоятельно на разных сканах.
http://en.wikipedia.org/wiki/B-spline
Статьи о B-Spline на английском языке (3,65 MB)