C++图像处理 -- 平面几何变换

阅读提示

    《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。

    《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。

    尽可能保持二者内容一致,可相互对照。

    本文代码必须包括《C++图像处理 -- 数据类型及公用函数文章中的BmpData.h头文件以及《C++图像处理 -- 平面几何变换类》TransformMatrix.h文件。

 

    在《C++图像处理 -- 平面几何变换类》一文中,介绍了图像平面几何变换类TransformMatrix,并写了一个简单的临近插值法图像几何变换函数Transform,用于测试。很显然,Transform函数产生的变换图像不仅质量较差,而且也不具备通用性,只能作为一个实现图像几何变换的框架。

    本文拟采用临近插值法、双线性插值法和双立方插值法等三种插值方式,来实现较完整、通用的图形图像平面几何变换。下面是除TransformMatrix类外的全部代码(TransformMatrix类代码在《C++图像处理 -- 平面几何变换类》中)。

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

#define GetPixel4096(data, x, y)	\
	(PARGBQuad)((LPBYTE)data->Scan0 + (y >> 12) * data->Stride + ((x >> 12) << 2))
//---------------------------------------------------------------------------

// 获取临近插值颜色
FORCEINLINE
ARGBQuad GetNearColor(CONST BitmapData *data, UINT x, UINT y)
{
	return *GetPixel4096(data, x, y);
}
//---------------------------------------------------------------------------

// 获取线性插值颜色
FORCEINLINE
ARGBQuad GetBilinearColor(CONST BitmapData *data, UINT x, UINT y)
{
	UINT u = (x & 0xfff) >> 4;	// u = (x % 0x1000) / 16
	UINT v = (y & 0xfff) >> 4;	// v = (y % 0x1000) / 16
	UINT u0 = u ^ 255;			// u0 = 255 - u
	UINT v0 = v ^ 255;			// v0 = 255 - v
	UINT m0 = v0 * u0;
	UINT m1 = v * u0;
	UINT m2 = v0 * u;
	UINT m3 = v * u;
	PARGBQuad p0 = GetPixel4096(data, x, y);
	PARGBQuad p1 = (PARGBQuad)((LPBYTE)p0 + data->Stride);
	PARGBQuad p2 = p0 + 1;
	PARGBQuad p3 = p1 + 1;
	ARGBQuad color;
	// 如果不要求很高精度,/ (255 * 255)可改为 >> 16,能提高速度
	color.Blue = (p0->Blue * m0 + p1->Blue * m1 + p2->Blue * m2 + p3->Blue * m3) / (255 * 255);
	color.Green = (p0->Green * m0 + p1->Green * m1 + p2->Green * m2 + p3->Green * m3) / (255 * 255);
	color.Red = (p0->Red * m0 + p1->Red * m1 + p2->Red * m2 + p3->Red * m3) / (255 * 255);
	color.Alpha = (p0->Alpha * m0 + p1->Alpha * m1 + p2->Alpha * m2 + p3->Alpha * m3) / (255 * 255);
	return color;
}
//---------------------------------------------------------------------------

static INT uvTable[513];

// 获取双立方插值颜色
FORCEINLINE
ARGBQuad GetBicubicColor(CONST BitmapData *data, UINT x, UINT y)
{
	INT us[4], vs[4];
	UINT u = (x & 0xfff) >> 4;	// u = (x % 0x1000) / 16
	UINT v = (y & 0xfff) >> 4;	// v = (y % 0x1000) / 16
	us[0] = uvTable[256 + u];
	us[1] = uvTable[u];
	us[2] = uvTable[256 - u];
	us[3] = uvTable[512 - u];
	vs[0] = uvTable[256 + v];
	vs[1] = uvTable[v];
	vs[2] = uvTable[256 - v];
	vs[3] = uvTable[512 - v];

	PARGBQuad p = GetPixel4096(data, x, y);
	INT pixOffset = data->Stride >> 2;
	INT sA, sR, sG, sB;
	sA = sR = sG = sB = 0;

	for (INT i = 0; i < 4; i ++, p += pixOffset)
	{
		sB += ((us[0] * p[0].Blue + us[1] * p[1].Blue +
				us[2] * p[2].Blue + us[3] * p[3].Blue) * vs[i]);
		sG += ((us[0] * p[0].Green + us[1] * p[1].Green +
				us[2] * p[2].Green + us[3] * p[3].Green) * vs[i]);
		sR += ((us[0] * p[0].Red + us[1] * p[1].Red +
				us[2] * p[2].Red + us[3] * p[3].Red) * vs[i]);
		sA += ((us[0] * p[0].Alpha + us[1] * p[1].Alpha +
				us[2] * p[2].Alpha + us[3] * p[3].Alpha) * vs[i]);
	}
	sB >>= 16;
	sG >>= 16;
	sR >>= 16;
	sA >>= 16;
	ARGBQuad color;
	sA = sA < 0? 0 : sA > 255? 255 : sA;
	// 因像素格式为PARGB,上限必须为sA(Alpha)而非255
	color.Blue = sB < 0? 0 : sB > sA? sA : sB;
	color.Green = sG < 0? 0 : sG > sA? sA : sG;
	color.Red = sR < 0? 0 : sR > sA? sA : sR;
	color.Alpha = sA;
	return color;
}
//---------------------------------------------------------------------------

VOID InitBicubicUVTable(FLOAT slope = -0.75)
{
	static FLOAT _slope = 0;
	DOUBLE x, x2, x3;
	if (!(slope < 0)) slope = -0.75;
	if (_slope != slope)
	{
		_slope = slope;
		for (INT i = 0; i <= 512; i ++)
		{
			x = i * (1.0 / 256);
			x2 = x * x;
			x3 = x * x2;
			if (x > 2)
				uvTable[i] = 0;
			else if (x > 1)
				uvTable[i] = (INT)(slope * (x3 - 5 * x2 + 8 * x - 4) * 256);
			else
				uvTable[i] = (INT)(((slope + 2) * x3 - (slope + 3) * x2 + 1) * 256);
		}
	}
}
//---------------------------------------------------------------------------

// 目标像素pd和颜色cs合成函数。
FORCEINLINE
VOID MixerColor(PARGBQuad pd, ARGBQuad cs)
{
	if (cs.Alpha == 255)		// 如果源像素不透明度为255,直接拷贝
		pd->Color = cs.Color;
	else if (cs.Alpha != 0)		// 否则,如果源像素不透明度大于0
	{
		if (pd->Alpha == 255)	// 如果目标像素不透明度为255,ARGB合成
		{
			pd->Blue += (cs.Blue - (pd->Blue * cs.Alpha + 127) / 255);
			pd->Green += (cs.Green - (pd->Green * cs.Alpha + 127) / 255);
			pd->Red += (cs.Red - (pd->Red * cs.Alpha + 127) / 255);
		}
		else  					// 否则,PARGB合成
		{
			// pd转换为PARGB,cs已经是PARGB格式
			pd->Blue = (pd->Blue * pd->Alpha + 127) / 255;
			pd->Green = (pd->Green * pd->Alpha + 127) / 255;
			pd->Red = (pd->Red * pd->Alpha + 127) / 255;
			// pd与cs合成
			pd->Blue += (cs.Blue - (pd->Blue * cs.Alpha + 127) / 255);
			pd->Green += (cs.Green - (pd->Green * cs.Alpha + 127) / 255);
			pd->Red += (cs.Red - (pd->Red * cs.Alpha + 127) / 255);
			pd->Alpha += (cs.Alpha - (pd->Alpha * cs.Alpha + 127) / 255);
			// 重新转换为ARGB
			pd->Blue = pd->Blue * 255 / pd->Alpha;
			pd->Green = pd->Green * 255 / pd->Alpha;
			pd->Red = pd->Red * 255 / pd->Alpha;
		}
	}
}
//---------------------------------------------------------------------------

typedef ARGBQuad (*InterpolateProc)(CONST BitmapData*, UINT, UINT);

// 获取插值过程和扩展半径
INT GetInterpolateProc(InterpolateMode mode, InterpolateProc &proc)
{
	INT radius[] = {2, 1, 2, 4};
	InterpolateProc procs[] = {GetBilinearColor, GetNearColor,
							   GetBilinearColor, GetBicubicColor};
	proc = procs[mode];
	return radius[mode];
}
//---------------------------------------------------------------------------

VOID CopyInterpolateData(BitmapData *dest, CONST BitmapData *source, INT alpha)
{
//	SetAlphaFlag(dest, HasAlphaFlag(source));
	PARGBQuad pd, ps;
	UINT width, height;
	INT dstOffset, srcOffset;
	GetDataCopyParams(dest, source, width, height, pd, ps, dstOffset, srcOffset);
	UINT x, y;
	// 如果alpha < 255或者源数据含Alpha,转换为PARGB像素格式
	if (alpha < 255 || HasAlphaFlag(source))
	{
		for (y = 0; y < height; y ++, pd += dstOffset, ps += srcOffset)
		{
			for (x = 0; x < width; x ++, pd ++, ps ++)
			{
				pd->Alpha = (alpha * ps->Alpha + 127) / 255;
				pd->Blue = (ps->Blue * pd->Alpha + 127) / 255;
				pd->Green = (ps->Green * pd->Alpha + 127) / 255;
				pd->Red = (ps->Red * pd->Alpha + 127) / 255;
			}
		}
	}
	// 否则, 直接像素拷贝
	else
	{
		for (y = 0; y < height; y ++, pd += dstOffset, ps += srcOffset)
		{
			for (x = 0; x < width; *pd ++ = *ps ++, x ++);
		}
	}
}
//---------------------------------------------------------------------------

VOID FillBorder(BitmapData *data, UINT radius, BOOL fillX, BOOL fillY)
{
	UINT height = data->Height - (radius << 1);
	UINT x, y;
	PARGBQuad pd, ps;
	if (fillX)
	{
		UINT width = data->Width - (radius << 1);
		pd = (PARGBQuad)data->Scan0 + radius * data->Width;
		for (y = 0; y < height; y ++)
		{
			for (x = 0, ps = pd + radius; x < radius; *pd ++ = *ps, x ++);
			for (x = 0, pd += width, ps = pd - 1; x < radius; *pd ++ = *ps, x ++);
        }
	}
	if (fillY)
	{
		pd = (PARGBQuad)data->Scan0;
		ps = pd + radius * data->Width;
		PARGBQuad pd2 = ps + height * data->Width;
		PARGBQuad ps2 = pd2 - data->Width;
		for (y = 0; y < radius; y ++)
		{
			for (x = 0; x < data->Width; *pd ++ = ps[x], *pd2 ++ = ps2[x], x ++);
		}
    }
}
//---------------------------------------------------------------------------

BOOL CanTransform(INT width, INT height, RECT &r)
{
	r.right += r.left;
	r.bottom += r.top;
	if (r.right > width) r.right = width;
	if (r.bottom > height) r.bottom = height;
	if (r.left > 0) r.right -= r.left; else r.left = 0;
	if (r.top > 0) r.bottom -= r.top; else r.top = 0;
	return r.right > 0 && r.bottom > 0;
}

BOOL GetTransformParams(INT dstWidth, INT dstHeight,
	INT srcWidth, INT srcHeight, TransformMatrix &matrix, RECT &dst, RECT &src)
{
	FLOAT fx, fy, fwidth, fheight;
	matrix.GetTransformSize(srcWidth, srcHeight, fx, fy, fwidth, fheight);
	matrix.Invert();
	dst.left = (LONG)fx;
	dst.top = (LONG)fy;
	dst.right = (LONG)(fwidth + fx + 0.999999f);
	dst.bottom = (LONG)(fheight + fy + 0.999999f);
	if (!CanTransform(dstWidth, dstHeight, dst))
		return FALSE;
	if (fx > 0 || fy > 0)
	{
		if (fx < 0) fx = 0;
		else if (fy < 0) fy = 0;
		matrix.Translate(fx, fy);
	}
	matrix.GetTransformSize(dst.right, dst.bottom, fx, fy, fwidth, fheight);
	src.left = (LONG)fx;
	src.top = (LONG)fy;
	src.right = (LONG)(fwidth + fx + 0.999999f);
	src.bottom = (LONG)(fheight + fy + 0.999999f);
	if (!CanTransform(srcWidth, srcHeight, src))
		return FALSE;
	if (fx > 0) matrix.GetElements().dx -= fx;
	if (fy > 0) matrix.GetElements().dy -= fy;
	return TRUE;
}
//---------------------------------------------------------------------------

// 执行图像数据几何变换
VOID ImageTransform(BitmapData *dest, INT x, INT y,
	CONST BitmapData *source, TransformMatrix *matrix, FLOAT alpha = 1.0f)
{
	INT alphaI = (INT)(alpha * 255);
	if (alphaI <= 0) return;
	if (alphaI > 255) alphaI = 255;

	// 复制几何变换矩阵对象
	TransformMatrix m(matrix);

	// 几何变换矩阵绝对增加平移量x, y
	m.GetElements().dx += x;
	m.GetElements().dy += y;

	// 逆转几何变换矩阵,计算并分别返回目标和源图像实际大小dstR和srcR
	RECT dstR, srcR;
	if (GetTransformParams(dest->Width, dest->Height,
		source->Width, source->Height, m, dstR, srcR) == FALSE)
		return;

	// 将浮点数扩大4096倍,采用定点数运算
	INT im11 = (INT)(m.GetElements().m11 * 4096.0f);
	INT im12 = (INT)(m.GetElements().m12 * 4096.0f);
	INT im21 = (INT)(m.GetElements().m21 * 4096.0f);
	INT im22 = (INT)(m.GetElements().m22 * 4096.0f);

	// 根据mode获取插值过程及边框扩展半径
	InterpolateMode mode = GetInterpolateMode(source);
	InterpolateProc ColorProc;
	INT radius = GetInterpolateProc(mode, ColorProc);

	BitmapData dst, src, exp, tmp;
	// 按dstR和srcR分别获取目标和源图像子图到dst和src
	GetBitmapData(dest, dstR.left, dstR.top, dstR.right, dstR.bottom, &dst);
	GetBitmapData(source, srcR.left, srcR.top, srcR.right, srcR.bottom, &src);
	// 建立扩展半径为radius新的图像数据对象exp
	GetBitmapData(src.Width + radius * 2, src.Height + radius * 2, &exp);
	// src图像数据拷贝到exp
	GetBitmapData(&exp, radius, radius, src.Width, src.Height, &tmp);
	CopyInterpolateData(&tmp, &src, alphaI);
	// 填充exp边框像素。如果im21或im12的12位尾数不为0,说明x或y向为斜边,不填充
	BOOL fillX = (im21 & 0xfff) == 0;
	BOOL fillY = (im12 & 0xfff) == 0;
	FillBorder(&exp, radius, fillX, fillY);
	if (fillX && fillY && alphaI == 255 && !HasAlphaFlag(source) &&
		dest->Width == dst.Width && dest->Height == dst.Height)
		SetAlphaFlag(dest, FALSE);
	// 确定源图像边界界限
	INT up = radius * 0x800;
	INT xDown = (exp.Width - radius) * 0x1000;
	INT yDown = (exp.Height - radius) * 0x1000;
	// 几何变换逆矩阵的平移量为与子图原点对应的源图起始坐标点
	INT xs = (INT)(m.GetElements().dx * 4096.0f) + up + 0x800;
	INT ys = (INT)(m.GetElements().dy * 4096.0f) + up + 0x800;
	INT width = (INT)dst.Width;
	INT height = (INT)dst.Height;

	PARGBQuad pd = (PARGBQuad)dst.Scan0;
	INT dstOffset = (dst.Stride >> 2) - dst.Width;
	// 如果插值方式为双立方卷积,初始化UV表
	if (mode == InterpolateModeBicubic)
		InitBicubicUVTable(-0.75);

	// 按目标子图逐点复制源子图几何变换后的数据
	for (y = 0; y < height; y ++, ys += im22, xs += im21, pd += dstOffset)
	{
		INT y0 = ys;
		INT x0 = xs;
		for (x = 0; x < width; x ++, x0 += im11, y0 += im12, pd ++)
		{
			if (y0 >= up && y0 < yDown && x0 >= up && x0 < xDown)
			{
				MixerColor(pd, ColorProc(&exp, x0, y0));
            }
		}
	}
	FreeBitmapData(&exp);
}
//---------------------------------------------------------------------------

    同《C++图像处理 -- 平面几何变换类》的临近插值图形图像平面几何变换函数相比,本文代码有以下特点:

    1、实现了邻近插值、双线性插值和双立方插值三种插值方式,具有很强的通用性和实用性。

    2、插值过程采用了定点数运算,比浮点数运算速度快。

    3、较好的实现了边界处理。边界处理是图形图像平面几何变换的一个难点,处理不好会出现难看的锯齿或者半透明的图像边缘。本文代码采用了扩展图像边框像素的方法,较好的解决了这个问题。当边界发生倾斜(变形)时,超出图像边界的像素值设置为0,通过插值后的半透明像素点能较好地解决边界锯齿;而边界不变形时,超出图像边界的像素值用临近的边界像素值替代,这样就不会出现一些不该出现的半透明像素,避免难看的半透明的图像边缘。下面是用本文代码和GDI+几何变换函数分别作1.2倍缩放处理加0.3剪切的双线性插值处理效果图:

    4、可处理含Alpha信息的图像,同时增加了图像数据不透明度的处理。对含Alpha信息的图像数据或者不透明度小于1的几何变换,对图像源作了自乘预处理,减少了原像素与变换后的像素值得差异,保证了较好的视觉效果。下面的PNG图片几何变换处理效果图中,左边是经过自乘预处理后的,而右边是未处理的:

    5、限于文章篇幅,本文代码以通用性和清晰度为主,没作过多的优化处理,有兴趣的朋友可根据自己需要进行改进。例如,可将缩放处理过程独立,以提高缩放处理处理速度。

    下面是用BCB2010和GDI+运用本文图形图像平面几何变换代码处理Alpha像素格式图像的例子代码:

void __fastcall TForm1::Button1Click(TObject *Sender)
{
	// 获取源图像扫描线数据
	Gdiplus::Bitmap *bmp =  new Gdiplus::Bitmap(L"..\\..\\media\\IMG_9440_mf.jpg");
	BitmapData source, dest;
	LockBitmap(bmp, &source);

	// 设置几何变换
	TransformMatrix matrix;
	matrix.Scale(1.2, 1.2);
	matrix.Shear(0.3, 0.3);

	// 建立目标位图并获取其扫描线数据
	RECT r;
	matrix.GetTransformRect(source.Width, source.Height, r);
	Gdiplus::Bitmap *newBmp = new Gdiplus::Bitmap(
		r.right - r.left, r.bottom - r.top, PixelFormat32bppARGB);
	LockBitmap(newBmp, &dest);

	// 设置双立方插值方式
	SetInterpolateMode(&source, InterpolateModeBicubic);
	// 执行图像几何变换。
	// 注意这里使用-r.left, -r.top为坐标,使得变换后的图像完全可见
	ImageTransform(&dest, -r.left, -r.top, &source, &matrix, 1);

	// 释放图像扫描线数据(位图解锁)
	UnlockBitmap(newBmp, &dest);
	UnlockBitmap(bmp, &source);

	// 画几何变换后的图像
	Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle);
	g->DrawImage(newBmp, 0, 0);

	delete g;
	delete newBmp;
	delete bmp;

}
//---------------------------------------------------------------------------

    因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:maozefa@hotmail.com

    这里可访问《C++图像处理 -- 文章索引

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值