Вернуться к разделу "Реализация проекта BookScanLib".
Фильтр Unsharp Mask (Нерезкое Маскирование) - это качественное средство для придания резкости обрабатываемому изображению.
Любая реализация фильтра unsharp mask построена на следующем принципе:
1. Исходное
изображение размывается (через Blur).
2. Вычисляется разница между исходным и
размытым изображением.
Вопрос лишь в том, какой алгоритм избрать для размывания исходного изображения.
Я написал простейшую консольную программу для демонстрации работы фильтра Unsharp Mask. Для размытия исходного изображения она использует алгоритм Gaussian Blur. На входе она принимает следующие параметры:
unsharp <input_file> <radius (double)> <amount (double)> <threshold (int)> <only_blur (bool)>
Если указать последний параметр = 1, то программа будет делать только размывание (без нерезкого маскирования).
По умолчанию рекомендуются следующие значения:
Radius = 5.0 (обратите внимание - это дробное, а не целое число)
Amount = 0.5 (дробное)
Threshold = 0 (целое)
Точность дробных чисел - до одной десятой.
На выходе программа выдаёт этот же файл, обработанный выбранным фильтром.
Программа работает только с серыми и цветными изображениями.
Всё необходимое для тестирования этой программы (компиляционный проект, готовый экзешник, файл-пример и bat-файлы для тестирования программы) я оформил в небольшой пакет:
Скачать пакет unsharp (55 КБ)
(Для работы программы требуется FreeImage dll-библиотека из пакета FreeImage DLL v3.9.2 - см. статью 1. Знакомство с FreeImage).
Рассмотрим исходные коды этой программы:
/*
* Copyright (C) 1999 Winston Chang
* <winstonc@cs.wisc.edu>
* <winston@stdout.org>
*
* 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.
*/
// This algorithm was taken from the GIMP v2.4.5 sourcecodes
// and adopted for the FreeImage library
//
// Copyright (C) 2007-2008:
// monday2000 monday2000@yandex.ru
#include "FreeImage.h"
#include "Utilities.h"
/////////////////////////////////////////////////////////////////////////////////////////////
// clamp and place result in destination pixel
inline BYTE FI_Clamp(double value)
{
return (BYTE)MIN(MAX((int)0, (int)(value + 0.5)), (int)255);
}
/////////////////////////////////////////////////////////////////////////////////////////////
inline double FI_Sqr(double param)
{
return param * param;
}
/////////////////////////////////////////////////////////////////////////////////////////////
inline int FI_Round(double param)
{
return (int)floor(param);
}
/////////////////////////////////////////////////////////////////////////////////////////////
// This function is written as if it is blurring a column at a time,
// even though it can operate on rows, too. There is no difference
// in the processing of the lines, at least to the blur_line function.
static void blur_line (const double *ctable,
const double *cmatrix,
const int cmatrix_length,
const BYTE *src,
BYTE *dest,
const int len,
const int bytes)
{
const double *cmatrix_p;
const double *ctable_p;
const BYTE *src_p;
const BYTE *src_p1;
const int cmatrix_middle = cmatrix_length / 2;
int row;
int i, j;
// This first block is the same as the optimized version --
// it is only used for very small pictures, so speed isn't a
// big concern.
if (cmatrix_length > len)
{
for (row = 0; row < len; row++)
{
// find the scale factor
double scale = 0;
for (j = 0; j < len; j++)
{
// if the index is in bounds, add it to the scale counter
if (j + cmatrix_middle - row >= 0 &&
j + cmatrix_middle - row < cmatrix_length)
scale += cmatrix[j];
}
src_p = src;
for (i = 0; i < bytes; i++)
{
double sum = 0;
src_p1 = src_p++;
for (j = 0; j < len; j++)
{
if (j + cmatrix_middle - row >= 0 &&
j + cmatrix_middle - row < cmatrix_length)
sum += *src_p1 * cmatrix[j];
src_p1 += bytes;
}
*dest++ = (BYTE) FI_Round (sum / scale);
}
}
}
else
{
// for the edge condition, we only use available info and scale to one
for (row = 0; row < cmatrix_middle; row++)
{
// find scale factor
double scale = 0;
for (j = cmatrix_middle - row; j < cmatrix_length; j++)
scale += cmatrix[j];
src_p = src;
for (i = 0; i < bytes; i++)
{
double sum = 0;
src_p1 = src_p++;
for (j = cmatrix_middle - row; j < cmatrix_length; j++)
{
sum += *src_p1 * cmatrix[j];
src_p1 += bytes;
}
*dest++ = (BYTE) FI_Round (sum / scale);
}
}
// go through each pixel in each col
for (; row < len - cmatrix_middle; row++)
{
src_p = src + (row - cmatrix_middle) * bytes;
for (i = 0; i < bytes; i++)
{
double sum = 0;
cmatrix_p = cmatrix;
src_p1 = src_p;
ctable_p = ctable;
for (j = 0; j < cmatrix_length; j++)
{
sum += cmatrix[j] * *src_p1;
src_p1 += bytes;
ctable_p += 256;
}
src_p++;
*dest++ = (BYTE) FI_Round (sum);
}
}
// for the edge condition, we only use available info and scale to one
for (; row < len; row++)
{
// find scale factor
double scale = 0;
for (j = 0; j < len - row + cmatrix_middle; j++)
scale += cmatrix[j];
src_p = src + (row - cmatrix_middle) * bytes;
for (i = 0; i < bytes; i++)
{
double sum = 0;
src_p1 = src_p++;
for (j = 0; j < len - row + cmatrix_middle; j++)
{
sum += *src_p1 * cmatrix[j];
src_p1 += bytes;
}
*dest++ = (BYTE) FI_Round (sum / scale);
}
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////
// generates a 1-D convolution matrix to be used for each pass of
// a two-pass gaussian blur. Returns the length of the matrix.
//
static int gen_convolve_matrix (double radius, double **cmatrix_p)
{
double *cmatrix;
double std_dev;
double sum;
int matrix_length;
int i, j;
// we want to generate a matrix that goes out a certain radius
// from the center, so we have to go out ceil(rad-0.5) pixels,
// inlcuding the center pixel. Of course, that's only in one direction,
// so we have to go the same amount in the other direction, but not count
// the center pixel again. So we double the previous result and subtract
// one.
// The radius parameter that is passed to this function is used as
// the standard deviation, and the radius of effect is the
// standard deviation * 2. It's a little confusing.
//
radius = fabs (radius) + 1.0;
std_dev = radius;
radius = std_dev * 2;
// go out 'radius' in each direction
matrix_length = 2 * ceil (radius - 0.5) + 1;
if (matrix_length <= 0)
matrix_length = 1;
*cmatrix_p = (double*) calloc (matrix_length, sizeof(double));
cmatrix = *cmatrix_p;
// Now we fill the matrix by doing a numeric integration approximation
// from -2*std_dev to 2*std_dev, sampling 50 points per pixel.
// We do the bottom half, mirror it to the top half, then compute the
// center point. Otherwise asymmetric quantization errors will occur.
// The formula to integrate is e^-(x^2/2s^2).
//
// first we do the top (right) half of matrix
for (i = matrix_length / 2 + 1; i < matrix_length; i++)
{
double base_x = i - (matrix_length / 2) - 0.5;
sum = 0;
for (j = 1; j <= 50; j++)
{
if (base_x + 0.02 * j <= radius)
sum += exp (- FI_Sqr (base_x + 0.02 * j) / (2 * FI_Sqr (std_dev)));
}
cmatrix[i] = sum / 50;
}
// mirror the thing to the bottom half
for (i = 0; i <= matrix_length / 2; i++)
cmatrix[i] = cmatrix[matrix_length - 1 - i];
// find center val -- calculate an odd number of quanta to make it symmetric,
// even if the center point is weighted slightly higher than others.
sum = 0;
for (j = 0; j <= 50; j++)
sum += exp (- FI_Sqr (0.5 + 0.02 * j) / (2 * FI_Sqr (std_dev)));
cmatrix[matrix_length / 2] = sum / 51;
// normalize the distribution by scaling the total sum to one
sum = 0;
for (i = 0; i < matrix_length; i++)
sum += cmatrix[i];
for (i = 0; i < matrix_length; i++)
cmatrix[i] = cmatrix[i] / sum;
return matrix_length;
}
//////////////////////////////////////////////////////////////////////////////////////////////
// ----------------------- gen_lookup_table -----------------------
// generates a lookup table for every possible product of 0-255 and
// each value in the convolution matrix. The returned array is
// indexed first by matrix position, then by input multiplicand (?)
// value.
//
static double* gen_lookup_table (const double *cmatrix, int cmatrix_length)
{
double *lookup_table = (double*) calloc (cmatrix_length * 256, sizeof(double));
double *lookup_table_p = lookup_table;
const double *cmatrix_p = cmatrix;
int i, j;
for (i = 0; i < cmatrix_length; i++)
{
for (j = 0; j < 256; j++)
*(lookup_table_p++) = *cmatrix_p * (double) j;
cmatrix_p++;
}
return lookup_table;
}
//////////////////////////////////////////////////////////////////////////////////////////////
void FI_pixel_rgn_get_row (FIBITMAP* dib, BYTE* buf, unsigned x, unsigned y, unsigned width, int bytes)
{
// Get several pixels of a region in a row.
// This function fills the buffer buf with the
// values of the pixels from (x, y) to (x+width-1, y).
// buf should be large enough to hold all these values.
//dib :
// a previously initialized FIBITMAP*.
//buf :
// a pointer to an array of BYTE
//x :
// the x coordinate of the first pixel
//y :
// the y coordinate of the first pixel
//width :
// the number of pixels to get.
unsigned dw = FreeImage_GetWidth(dib);
unsigned dh = FreeImage_GetHeight(dib);
unsigned pitch = FreeImage_GetPitch(dib);
unsigned c, i;
BYTE* bits = (BYTE*)FreeImage_GetBits(dib); // The image raster
BYTE* lines = bits + y * pitch;
int index = 0;
for (c = x; c < x+width; c++)
for (i = 0; i < bytes; i++)
buf[index++] = lines[c * bytes + i];
}
/////////////////////////////////////////////////////////////////////////////////////////////
void FI_pixel_rgn_get_col (FIBITMAP* dib, BYTE* buf, unsigned x, unsigned y, unsigned height, int bytes)
{
// Get several pixels of a region's column.
// This function fills the buffer buf with the values
// of the pixels from (x, y) to (x, y+height-1).
// buf should be large enough to hold all these values.
//dib :
// a previously initialized FIBITMAP*.
//buf :
// a pointer to an array of BYTE
//x :
// the x coordinate of the first pixel
//y :
// the y coordinate of the first pixel
//height :
// the number of pixels to get.
unsigned dw = FreeImage_GetWidth(dib);
unsigned dh = FreeImage_GetHeight(dib);
unsigned pitch = FreeImage_GetPitch(dib);
unsigned r, i;
BYTE* bits = (BYTE*)FreeImage_GetBits(dib); // The image raster
BYTE* lines;
int index = 0;
for (r = y; r < y+height; r++)
{
lines = bits + r * pitch;
for (i = 0; i < bytes; i++)
buf[index++] = lines[x * bytes + i];
}
}
/////////////////////////////////////////////////////////////////////////////////////////////
void FI_pixel_rgn_set_row (FIBITMAP* dib, BYTE* buf, unsigned x, unsigned y, unsigned width, int bytes)
{
// Set several pixels of a region in a row.
// This function draws the pixels from (x, y)
// to (x+width-1, y) using the values of the buffer buf.
// buf should be large enough to hold all these values.
//dib :
// a previously initialized FIBITMAP*.
//buf :
// a pointer to an array of BYTE
//x :
// the x coordinate of the first pixel
//y :
// the y coordinate of the first pixel
//width :
// the number of pixels to set.
unsigned dw = FreeImage_GetWidth(dib);
unsigned dh = FreeImage_GetHeight(dib);
unsigned pitch = FreeImage_GetPitch(dib);
unsigned c, i;
BYTE* bits = (BYTE*)FreeImage_GetBits(dib); // The image raster
BYTE* lines = bits + y * pitch;
int index = 0;
for (c = x; c < x+width; c++)
for (i = 0; i < bytes; i++)
lines[c * bytes + i] = buf[index++];
}
/////////////////////////////////////////////////////////////////////////////////////////////
void FI_pixel_rgn_set_col (FIBITMAP* dib, BYTE* buf, unsigned x, unsigned y, unsigned height, int bytes)
{
// Set several pixels of a region's column.
// This function draws the pixels from (x, y)
// to (x, y+height-1) using the values from the buffer buf.
// buf should be large enough to hold all these values.
//dib :
// a previously initialized FIBITMAP*.
//buf :
// a pointer to an array of BYTE
//x :
// the x coordinate of the first pixel
//y :
// the y coordinate of the first pixel
//height :
// the number of pixels to set.
unsigned dw = FreeImage_GetWidth(dib);
unsigned dh = FreeImage_GetHeight(dib);
unsigned pitch = FreeImage_GetPitch(dib);
unsigned r, i;
BYTE* bits = (BYTE*)FreeImage_GetBits(dib); // The image raster
BYTE* lines;
int index = 0;
for (r = y; r < y+height; r++)
{
lines = bits + r * pitch;
for (i = 0; i < bytes; i++)
lines[x * bytes + i] = buf[index++];
}
}
/////////////////////////////////////////////////////////////////////////////////////////////
static FIBITMAP* unsharp_mask (FIBITMAP* src_dib, double radius, double amount, int threshold, int only_blur)
{
double *cmatrix = NULL;
int cmatrix_length;
double *ctable;
BYTE* src;
BYTE* dest;
unsigned bpp = FreeImage_GetBPP(src_dib);
unsigned bytes = bpp/8;
unsigned width = FreeImage_GetWidth(src_dib);
unsigned height = FreeImage_GetHeight(src_dib);
unsigned pitch = FreeImage_GetPitch(src_dib);
unsigned row, col;
FIBITMAP* dst_dib = FreeImage_Allocate(width, height, bpp);
// generate convolution matrix
// and make sure it's smaller than each dimension
cmatrix_length = gen_convolve_matrix (radius, &cmatrix);
// generate lookup table
ctable = gen_lookup_table (cmatrix, cmatrix_length);
// allocate buffers
src = (BYTE*) calloc (MAX (width, height) * bytes, sizeof(BYTE));
dest = (BYTE*) calloc (MAX (width, height) * bytes, sizeof(BYTE));
// blur the rows
for (row = 0; row < height; row++)
{
FI_pixel_rgn_get_row (src_dib, src, 0, row, width, bytes);
blur_line (ctable, cmatrix, cmatrix_length, src, dest, width, bytes);
FI_pixel_rgn_set_row (dst_dib, dest, 0, row, width, bytes);
}
// blur the cols
for (col = 0; col < width; col++)
{
FI_pixel_rgn_get_col (dst_dib, src, col, 0, height, bytes);
blur_line (ctable, cmatrix, cmatrix_length, src, dest, height, bytes);
FI_pixel_rgn_set_col (dst_dib, dest, col, 0, height, bytes);
}
if (!only_blur)
{
// merge the source and destination (which currently contains
// the blurred version) images
for (row = 0; row < height; row++)
{
const BYTE *s = src;
BYTE *d = dest;
int u, v;
// get source row
FI_pixel_rgn_get_row (src_dib, src, 0, row, width, bytes);
// get dest row
FI_pixel_rgn_get_row (dst_dib, dest, 0, row, width, bytes);
// combine the two
for (u = 0; u < width; u++)
{
for (v = 0; v < bytes; v++)
{
int value;
int diff = *s - *d;
// do tresholding
if (abs (2 * diff) < threshold)
diff = 0;
value = *s++ + amount * diff;
*d++ = FI_Clamp (value);
}
}
FI_pixel_rgn_set_row (dst_dib, dest, 0, row, width, bytes);
}
}
free (dest);
free (src);
free (ctable);
free (cmatrix);
if(bpp == 8)
{
// copy the original palette to the destination bitmap
RGBQUAD *src_pal = FreeImage_GetPalette(src_dib);
RGBQUAD *dst_pal = FreeImage_GetPalette(dst_dib);
memcpy(&dst_pal[0], &src_pal[0], 256 * sizeof(RGBQUAD));
}
// 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;
}
/////////////////////////////////////////////////////////////////////////////////////////////
/*
The most widely useful method for sharpening an image,
The unsharp mask is a sharpening filter that works
by comparing using the difference of the image and
a blurred version of the image. It is commonly
used on photographic images, and is provides a much
more pleasing result than the standard sharpen
filter.
Winston Chang <winstonc@cs.wisc.edu>, 1999
double radius; // Radius of gaussian blur (in pixels > 1.0)
double amount; // Strength of effect
int threshold; // Threshold (0-255)
5.0, // default radius
0.5, // default amount
0 // default threshold
*/
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 < 5) {
printf("Usage : unsharp <input_file> <radius (double)> <amount (double)> <threshold (int)> <only_blur (bool)>\n");
return 0;
}
FIBITMAP *dib = GenericLoader(argv[1], 0);
double radius = atof(argv[2]);
double amount = atof(argv[3]);
int threshold = atof(argv[4]);
int only_blur = 0;
if(argc == 6) only_blur = atof(argv[5]);
if (dib)
{
// bitmap is successfully loaded!
if (FreeImage_GetImageType(dib) == FIT_BITMAP)
{
if (FreeImage_GetBPP(dib) == 8 || FreeImage_GetBPP(dib) == 24)
{
FIBITMAP* dst_dib = unsharp_mask (dib, radius, amount, threshold, only_blur);
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;
}
/////////////////////////////////////////////////////////////////////////////////////////////
|
Реализация данного алгоритма позаимствована из графического редактора GIMP v2.4.5.
Размывание осуществляется посредством гауссового размытия (Gaussian Blur). Однако, это гораздо более качественный Gaussian Blur, чем тот, который был описан в 12. Морфологические фильтры. Этот Gaussian Blur - 2-x-проходный, т.е. сначала размывает по строкам, а потом - по столбцам. При этом он заранее строит т.н. конволюционную матрицу и таблицу сверки - которые задают точность размытия на каждом из 2-х проходов алгоритма (я не слишком разбирался в этом алгоритме - подробнее рассказать не могу).
Как видно из исходных кодов, параметр Radius влияет на формирование конволюционной матрицы при работе Gaussian Blur. Параметры Amount и Threshold влияют только на работу Unsharp Mask (и не влияют на Gaussian Blur). Amount - это постоянный множитель, на который всякий раз умножается разница между исходным и размытым пикселем, а параметр Threshold задаёт итоговое обнуление слишком маленьких значений разницы между исходным и размытым пикселем.
Вместо Gaussian Blur можно было бы взять любой иной вид размытия (Blur).