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


13. Фильтр Unsharp Mask (на базе Gaussian Blur)

Фильтр 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).

Hosted by uCoz