文章目录
- 前言
- 1.图像平移
- 1.1 图像平移定义
- 1.2 图像平移编程实例
- 2.图像镜像
- 2.1 图像镜像定义
- 2.2 图像镜像编程实例
- 3.图像缩放
- 3.1 图像缩放定义
- 3.2 灰度插值法
- 3.3 图像缩放编程实例
- 4.图像旋转
- 4.1 图像旋转定义
- 4.2 图像旋转编程实例
前言
在某种意义上来说,图像的几何运算是与点运算相对立的。它改变了像素之间的空间位置和空间关系,但它没有改变灰度等级值。几何运算需要两个独立的算法:空间变换和灰度值插值。
在本章所介绍的几何运算中,应用空间变换算法对图像进行平移、镜像处理,应用空间变换和灰度值插值算法对图像进行缩放和旋转处理。
需要说明的是,在这里,以图像的几何中心作为坐标原点,x轴由左向右递增,y轴由上至下递增。
因此,在进行图像旋转时,是以图像的几何中心为基准进行旋转的:在进行图像缩放时,也是以图像的几何中心为基准,其上下左右均等地向内收缩或向外扩大的。这种坐标转换会使图像变换更自然。另外,在进行几何运算的时候,保持原图像的尺寸大小不变,如果变换后的图像超出该尺寸,超出部分会被截断,而不足部分会以白色像素填充。
1.图像平移
1.1 图像平移定义
1.2 图像平移编程实例
该实例通过设置横向和纵向的平移量,实现了图像的平移。
分别为该窗体内的2个Button控件添加Click事件,为了和主窗体之间传递数据,再添加2个get属性访问器,代码如下:
public partial class translation : Form
{
public translation()
{
InitializeComponent();
}
private void start_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.OK;
}
private void close_Click(object sender, EventArgs e)
{
this.Close();
}
public string GetXOffset
{
get
{
//横向平移量
return xOffset.Text;
}
}
public string GetYOffset
{
get
{
//纵向平移量
return yOffset.Text;
}
}
}
回到主窗体,为“图像平移”按钮添加Click事件,代码如下:
/// <summary>
/// 图像平移
/// </summary>
private void translation_Click(object sender, EventArgs e)
{
if (curBitmap!=null)
{
translation traForm = new translation();
if (traForm.ShowDialog()==DialogResult.OK)
{
Rectangle rect = new Rectangle(0, 0, curBitmap.Width, curBitmap.Height);
BitmapData bmpData = curBitmap.LockBits(rect, ImageLockMode.ReadWrite, curBitmap.PixelFormat);
IntPtr ptr = bmpData.Scan0;
int bytes = bmpData.Stride * bmpData.Height;
byte[] grayValues = new byte[bytes];
Marshal.Copy(ptr, grayValues, 0, bytes);
//得到两个方向的图像平移量
int x = Convert.ToInt32(traForm.GetXOFFset);
int y = Convert.ToInt32(traForm.GetYOffset);
byte[] tempArray = new byte[bytes];
//临时初始化为白色(255)像素
for (int i = 0; i < bytes; i++)
{
tempArray[i] = 255;
}
for (int j = 0; j < curBitmap.Height; j++)
{//保证纵向平移不出界
if ((j + y) < curBitmap.Height && (j + y) > 0)
{
for (int i = 0; i < curBitmap.Width * 3; i += 3)
{
if ((i + x * 3) < curBitmap.Width * 3 && (i + x * 3) > 0)
{//保证横向平移不出界
tempArray[(i + x * 3) + 0 + (j + y) * bmpData.Stride] = grayValues[i + 0 + j * bmpData.Stride];
tempArray[i + x * 3 + 1 + (j + y) * bmpData.Stride] = grayValues[i + 1 + j * bmpData.Stride];
tempArray[i + x * 3 + 2 + (j + y) * bmpData.Stride] = grayValues[i + 2 + j * bmpData.Stride];
}
}
}
}
//数组复制,返回平移图像
grayValues = (byte[])tempArray.Clone();
Marshal.Copy(grayValues, 0, ptr, bytes);
curBitmap.UnlockBits(bmpData);
}
Invalidate();
}
要注意像素格式PixelFormat,如24位灰度图像是1440万色,但是我们书上给的算法是对8为进行处理,可以采用分割的思想将24位拆开成3个8位,由这三个8为所保存的数据组合为24位,在处理的时候就将他们分开处理但是要整体观看。
2.图像镜像
2.1 图像镜像定义
镜像是一个物体相对于一个镜面的复制品。图像镜像分为水平镜像和垂直镜像两种。
水平镜像就是将图像左半部分和右半部分以图像垂直中轴线为中心镜像进行对换;
垂直镜像就是将图像上半部分和下半部分以图像水平中轴线为中心镜像进行对换。
2.2 图像镜像编程实例
该实例实现了图像的水平镜像和垂直镜像。
分别为该窗体内的2个Button控件添加Click事件,并再添加1个get属性访问器,代码如下:
private void startMirror_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.OK;
}
private void close_Click(object sender, EventArgs e)
{
this.Close();
}
public bool GetMirror
{
get
{
return horMirror.Checked;
}
}
}
回到主窗体,为“图像镜像”按钮添加Click事件,代码如下:
private void mirror_Click(object sender, EventArgs e)
{
if (curBitmap!=null)
{
mirror mirForm = new mirror();
if (mirForm.ShowDialog()==DialogResult.OK)
{
Rectangle rect = new Rectangle(0, 0, curBitmap.Width, curBitmap.Height);
BitmapData bmpData = curBitmap.LockBits(rect, ImageLockMode.ReadWrite, curBitmap.PixelFormat);
IntPtr ptr = bmpData.Scan0;
int bytes = 0;
判断是灰度色图像还是彩色图像,给相应的大小
if (curBitmap.PixelFormat==PixelFormat.Format8bppIndexed)
{
bytes= curBitmap.Width * curBitmap.Height;
}
else if (curBitmap.PixelFormat == PixelFormat.Format24bppRgb)
{
bytes = curBitmap.Width * curBitmap.Height * 3;
}
byte[] pixelValues = new byte[bytes];
Marshal.Copy(ptr, pixelValues, 0, bytes);
//水平中轴
int halfWidth = curBitmap.Width / 2;
//垂直中轴
int halfHeight = curBitmap.Height / 2;
byte temp;
byte temp1;
byte temp2;
byte temp3;
if (curBitmap.PixelFormat == PixelFormat.Format8bppIndexed)
{
if (mirForm.GetMirror)
{
for (int i = 0; i < curBitmap.Height; i++)
for (int j = 0; j < halfWidth; j++)
{
temp = pixelValues[i * curBitmap.Width + j];
pixelValues[i * curBitmap.Width + j] = pixelValues[(i + 1) * curBitmap.Width - j - 1];
pixelValues[(i + 1) * curBitmap.Width - j - 1] = temp;
}
}
else
{
for (int j = 0; j < curBitmap.Width; j++)
{
for (int i = 0; i < halfHeight; i++)
{
temp = pixelValues[i * curBitmap.Width + j];
pixelValues[i * curBitmap.Width + j] = pixelValues[(curBitmap.Height - i - 1) * curBitmap.Width + j];
pixelValues[(curBitmap.Height - i - 1) * curBitmap.Width + j] = temp;
}
}
}
}
else if (curBitmap.PixelFormat == PixelFormat.Format24bppRgb)
{
if (mirForm.GetMirror)
{
//水平镜像处理
for (int i = 0; i < curBitmap.Height; i++)
{
//每个像素的三个字节在水平镜像时顺序不能变,所以这个方法不能用
//for (int j = 0; j < halfWidth; j++)
//{
// //以水平中轴线为对称轴,两边像素值交换
// temp = pixelValues[i * curBitmap.Width * 3 + j * 3];
// pixelValues[i * curBitmap.Width * 3 + j * 3] = pixelValues[(i + 1) * curBitmap.Width * 3 - 1 - j * 3];
// pixelValues[(i + 1) * curBitmap.Width * 3 - 1 - j * 3] = temp;
//}
for (int j = 0; j < halfWidth; j++)
{//每三个字节组成一个像素,顺序不能乱
temp = pixelValues[0 + i * curBitmap.Width * 3 + j * 3];
temp1 = pixelValues[1 + i * curBitmap.Width * 3 + j * 3];
temp2 = pixelValues[2 + i * curBitmap.Width * 3 + j * 3];
pixelValues[0 + i * curBitmap.Width * 3 + j * 3] = pixelValues[0 + (i + 1) * curBitmap.Width * 3 - (j + 1) * 3];
pixelValues[1 + i * curBitmap.Width * 3 + j * 3] = pixelValues[1 + (i + 1) * curBitmap.Width * 3 - (j + 1) * 3];
pixelValues[2 + i * curBitmap.Width * 3 + j * 3] = pixelValues[2 + (i + 1) * curBitmap.Width * 3 - (j + 1) * 3];
pixelValues[0 + (i + 1) * curBitmap.Width * 3 - (j + 1) * 3] = temp;
pixelValues[1 + (i + 1) * curBitmap.Width * 3 - (j + 1) * 3] = temp1;
pixelValues[2 + (i + 1) * curBitmap.Width * 3 - (j + 1) * 3] = temp2;
}
}
}
else
{
//垂直镜像处理
for (int i = 0; i < curBitmap.Width * 3; i++)
{
for (int j = 0; j < halfHeight; j++)
{
//以垂直中轴线为对称轴。两边像素值互换
temp = pixelValues[j * curBitmap.Width * 3 + i];
pixelValues[j * curBitmap.Width * 3 + i] = pixelValues[(curBitmap.Height - j - 1) * curBitmap.Width * 3 + i];
pixelValues[(curBitmap.Height - j - 1) * curBitmap.Width * 3 + i] = temp;
}
}
}
}
Marshal.Copy(pixelValues, 0, ptr, bytes);
curBitmap.UnlockBits(bmpData);
}
Invalidate();
}
}
3.图像缩放
在图像缩放运算和图像旋转运算中,要用到灰度插值算法,因此这里给出灰度插值的两种算法。
3.1 图像缩放定义
3.2 灰度插值法
应用公式(4.5)所产生的图像中的像素有可能在原图像中找不到相应的像素点,因为数字图像中的坐标总是整数。这样就必须进行近似处理。一般是应用灰度插值法。它包括最近邻插值和双线性插值。
最近邻插值也称零阶插值,是最简单的插值方法。其做法是令输出像素的灰度值等于离它所映射到的位置最近的输入像素的灰度值。该插值计算虽然十分简单,但它会带来锯齿形的边,图像中也会出现孔洞和重叠。
双线性插值也称一阶插值,该方法是求到相邻的4个方格上点的距离之比,用这个比率和4个邻点像素的灰度值进行灰度插值,具体方法如下。
双线性插值法计算量大,但缩放后图像质量高,不会出现像素值不连续的情况。由于双线性插值具有低通滤波器的性质,使高频分量受损,所以可能会使图像轮廓在一定程度上变得模糊。
3.3 图像缩放编程实例
该实例应用最近邻插值法和双线性插值法实现图像的缩放。
在主窗体内添加1个Button控件,其属性修改如表4.5所示。
创建1个名为zoom 的 Windows窗体,该窗体用于选择缩放量及用何种灰度插值法。为该窗体添加2个Button控件、1个GroupBox控件、2个RadioButton 控件、2个Label 控件和2个TextBox控件,其属性修改如表4.6所示。
分别为该窗体内的2个Button控件添加Click事件,并再添加3个get属性访问器,代码如下:
private void startZoom_Click(object sender, EventArgs e)
{
if (xZoom.Text == "0" || yZoom.Text == "0")
{
MessageBox.Show("缩放量不能为0!\n请重新正确填写。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
else
{
this.DialogResult = DialogResult.OK;
}
}
private void close_Click(object sender, EventArgs e)
{
this.Close();
}
public bool GetNearOrBil
{
get
{
//判断是最近邻插值法还是双线性插值法
return nearestNeigh.Checked;
}
}
public string GetXZoom
{
get
{
//得到横向缩放量
return xZoom.Text;
}
}
public string GetYZoom
{
get
{
//得到纵向缩放量
return yZoom.Text;
}
}
回到主窗体,为“图像缩放”按钮控件添加Click事件代码,代码如下:
private void zoom_Click(object sender, EventArgs e)
{
if (curBitmap != null)
{
zoom zoomForm = new zoom();
if (zoomForm.ShowDialog() == DialogResult.OK)
{
Rectangle rect = new Rectangle(0, 0, curBitmap.Width, curBitmap.Height);
System.Drawing.Imaging.BitmapData bmpData = curBitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmap.PixelFormat);
IntPtr ptr = bmpData.Scan0;
int bytes = curBitmap.Width * curBitmap.Height;
byte[] grayValues = new byte[bytes];
System.Runtime.InteropServices.Marshal.Copy(ptr, grayValues, 0, bytes);
double x = Convert.ToDouble(zoomForm.GetXZoom);
double y = Convert.ToDouble(zoomForm.GetYZoom);
int halfWidth = (int)(curBitmap.Width / 2);
int halfHeight = (int)(curBitmap.Height / 2);
int xz = 0;
int yz = 0;
int tempWidth = 0;
int tempHeight = 0;
byte[] tempArray = new byte[bytes];
if (zoomForm.GetNearOrBil == true)
{
for (int i = 0; i < curBitmap.Height; i++)
{
for (int j = 0; j < curBitmap.Width; j++)
{
tempHeight = i - halfHeight;
tempWidth = j - halfWidth;
if (tempWidth > 0)
{
xz = (int)(tempWidth / x + 0.5);
}
else
{
xz = (int)(tempWidth / x - 0.5);
}
if (tempHeight > 0)
{
yz = (int)(tempHeight / y + 0.5);
}
else
{
yz = (int)(tempHeight / y - 0.5);
}
tempWidth = xz + halfWidth;
tempHeight = yz + halfHeight;
if (tempWidth < 0 || tempWidth >= curBitmap.Width || tempHeight < 0 || tempHeight >= curBitmap.Height)
{
tempArray[i * curBitmap.Width + j] = 255;
}
else
{
tempArray[i * curBitmap.Width + j] = grayValues[tempHeight * curBitmap.Width + tempWidth];
}
}
}
}
else
{
double tempX, tempY, p, q;
for (int i = 0; i < curBitmap.Height; i++)
{
for (int j = 0; j < curBitmap.Width; j++)
{
tempHeight = i - halfHeight;
tempWidth = j - halfWidth;
tempX = tempWidth / x;
tempY = tempHeight / y;
if (tempWidth > 0)
{
xz = (int)tempX;
}
else
{
xz = (int)(tempX - 1);
}
if (tempHeight > 0)
{
yz = (int)tempY;
}
else
{
yz = (int)(tempY - 1);
}
p = tempX - xz;
q = tempY - yz;
tempWidth = xz + halfWidth;
tempHeight = yz + halfHeight;
if (tempWidth < 0 || (tempWidth + 1) >= curBitmap.Width || tempHeight < 0 || (tempHeight + 1) >= curBitmap.Height)
{
tempArray[i * curBitmap.Width + j] = 255;
}
else
{
tempArray[i * curBitmap.Width + j] = (byte)((1.0 - q) * ((1.0 - p) * grayValues[tempHeight * curBitmap.Width + tempWidth] + p * grayValues[tempHeight * curBitmap.Width + tempWidth + 1]) +
q * ((1.0 - p) * grayValues[(tempHeight + 1) * curBitmap.Width + tempWidth] + p * grayValues[(tempHeight + 1) * curBitmap.Width + 1 + tempWidth]));
}
}
}
}
grayValues = (byte[])tempArray.Clone();
System.Runtime.InteropServices.Marshal.Copy(grayValues, 0, ptr, bytes);
curBitmap.UnlockBits(bmpData);
}
Invalidate();
}
}
4.图像旋转
4.1 图像旋转定义
同理,旋转后得到的图像像素也有可能在原图像中找不到相应的像素点,因此旋转处理也要用到插值法。由于双线性插值法在图像处理性能上要好过最近邻插值,因此,我们只应用双线性插值这一种方法对图像进行旋转处理。
4.2 图像旋转编程实例
该实例实现了任意角度的图像旋转。
1).在主窗体内添加1个Button 控件,其属性修改如表4.7所示。
2).创建1个名为rotation的 Windows窗体,该窗体用于选择旋转的角度。为该窗体添加2个Button控件、1个Label控件和1个TextBox控件,其属性修改如表4.8所示。
分别为该窗体内的2个Button控件添加Cick事件,并再添加1个get属性访问器,代码如下:
private void startRot_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.OK;
}
private void close_Click(object sender, EventArgs e)
{
this.Close();
}
public string GetDegree
{
get
{
//得到所要旋转的角度
return degree.Text;
}
}
回到主窗体,为“图像旋转”按钮控件添加Click事件,代码如下:
private void rotation_Click(object sender, EventArgs e)
{
if (curBitmap != null)
{
rotation rotForm = new rotation();
if (rotForm.ShowDialog() == DialogResult.OK)
{
Rectangle rect = new Rectangle(0, 0, curBitmap.Width, curBitmap.Height);
System.Drawing.Imaging.BitmapData bmpData = curBitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmap.PixelFormat);
IntPtr ptr = bmpData.Scan0;
int bytes = curBitmap.Width * curBitmap.Height;
byte[] grayValues = new byte[bytes];
System.Runtime.InteropServices.Marshal.Copy(ptr, grayValues, 0, bytes);
int degree = Convert.ToInt32(rotForm.GetDegree);
double radian = degree * Math.PI / 180.0;
double mySin = Math.Sin(radian);
double myCos = Math.Cos(radian);
int halfWidth = (int)(curBitmap.Width / 2);
int halfHeight = (int)(curBitmap.Height / 2);
int xr = 0;
int yr = 0;
int tempWidth = 0;
int tempHeight = 0;
byte[] tempArray = new byte[bytes];
double tempX, tempY, p, q;
for (int i = 0; i < curBitmap.Height; i++)
{
for (int j = 0; j < curBitmap.Width; j++)
{
tempHeight = i - halfHeight;
tempWidth = j - halfWidth;
tempX = tempWidth * myCos - tempHeight * mySin;
tempY = tempHeight * myCos + tempWidth * mySin;
if (tempWidth > 0)
{
xr = (int)tempX;
}
else
{
xr = (int)(tempX - 1);
}
if (tempHeight > 0)
{
yr = (int)tempY;
}
else
{
yr = (int)(tempY - 1);
}
p = tempX - xr;
q = tempY - yr;
tempWidth = xr + halfWidth;
tempHeight = yr + halfHeight;
if (tempWidth < 0 || (tempWidth + 1) >= curBitmap.Width || tempHeight < 0 || (tempHeight + 1) >= curBitmap.Height)
{
tempArray[i * curBitmap.Width + j] = 255;
}
else
{
tempArray[i * curBitmap.Width + j] = (byte)((1.0 - q) * ((1.0 - p) * grayValues[tempHeight * curBitmap.Width + tempWidth] + p * grayValues[tempHeight * curBitmap.Width + tempWidth + 1]) +
q * ((1.0 - p) * grayValues[(tempHeight + 1) * curBitmap.Width + tempWidth] + p * grayValues[(tempHeight + 1) * curBitmap.Width + 1 + tempWidth]));
}
}
}
grayValues = (byte[])tempArray.Clone();
System.Runtime.InteropServices.Marshal.Copy(grayValues, 0, ptr, bytes);
curBitmap.UnlockBits(bmpData);
}
Invalidate();
}
}