前言
最近朋友神Q超人决定隐退不再发文,从第一篇文章到铁人赛都能看到对IT界的贡献,虽然有些遗憾,但我相信有天他还会回来继续发文,大家是否也会跟我一样期待呢?
这次要介绍几何变换,用一些技巧和数学公式来改变图像尺寸或图像位置转换,而改变尺寸在影像处理当中也占了相当重要的位置,可以让计算量减少许多,这次也一样依照[1]依序实作。
水平与垂直转换
对于水平转换可以快速想到左边和右边像素交换,而垂直转换也可以快速想到上边和下边像素交换,这实现并不困难,但其实它们都是通过三角函数
去作旋转运算(公式如下图),水平旋转180度,垂直旋转90度,但在这里可以直接用简易想法实现,除非有需要作特定角度转换才需要代入此公式做计算。
注:旋转还有分顺时针和逆时针,这里只实现顺时针。
来源:[2]
general.h
enum RotateType{HORIZONTAL,VERTICAL};
Library.h
/*Rotate8bit Parameter:src= source of imagepur= purpose of imagewidth= Image widthheight= Image heighttype= rotate type*/void Rotate8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, C_UINT32 type);
Library.cpp
void Library::Rotate8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, MNDT::RotateType type){switch (type){case MNDT::RotateType::HORIZONTAL:RotateHorizontal8bit(src, pur, width, height);break;case MNDT::RotateType::VERTICAL:RotateVertical8bit(src, pur, width, height);break;}}
水平
1. 将右边像素搬移到左边
Library.h
void RotateHorizontal8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height);
Library.cpp
void Library::RotateHorizontal8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height){Image srcImage(const_cast<UCHAE*>(src), width, height, MNDT::ImageType::GRAY_8BIT);Image purImage(pur, width, height, MNDT::ImageType::GRAY_8BIT);for (UINT32 row = 0; row < height; row++){UINT32 purIndex = 0;for (int32_t col = width - 1; col >= 0; col--){purImage.image[row][purIndex] = srcImage.image[row][col];purIndex++;}}}
垂直
1. 将下边像素搬移到上边
Library.h
void RotateVertical8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height);
Library.cpp
void Library::RotateVertical8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height){Image srcImage(const_cast<UCHAE*>(src), width, height, MNDT::ImageType::GRAY_8BIT);Image purImage(pur, width, height, MNDT::ImageType::GRAY_8BIT);C_UINT32 copySize = width * sizeof(UCHAE);UINT32 purIndex = 0;for (int32_t row = height - 1; row >= 0; row--){memcpy(purImage.image[purIndex], srcImage.image[row], copySize);purIndex++;}}
影像尺寸变换
这里介绍两种尺寸转换第一种邻近内插法
第二种双线插值法
,这两种都是常用来做尺寸转换。首先先加入转换型态函数。
general.h
enum RotateType{HORIZONTAL,VERTICAL};
Library.h
/*Resize Parameter:src= source of imagepur= purpose of imagewidth= Image's widthheight= Image's heightreWidth= new widthreHeight= new heighttype= resize type*/void Resize8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, C_UINT32 reWidth, C_UINT32 reHeight, C_UINT32 type);
Library.cpp
void Library::Resize8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, C_UINT32 reWidth, C_UINT32 reHeight, C_UINT32 type){switch (type){case MNDT::ResizeType::NEAREST:NearestResize8bit(src, pur, width, height, reWidth, reHeight);break;case MNDT::ResizeType::LINEAR:LinearResize8bit(src, pur, width, height, reWidth, reHeight);break;}}
邻近内插法
主要计算缩放倍率取得原先像素索引位置。
1. 计算x索引和y索引的倍率。
2. 走访reWidth * reHeight大小,取得原先像素的位置,位置为现在索引值乘上上述的倍率。
Library.h
void NearestResize8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, C_UINT32 reWidth, C_UINT32 reHeight);
Library.cpp
void Library::NearestResize8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, C_UINT32 reWidth, C_UINT32 reHeight){Image srcImage(const_cast<UCHAE*>(src), width, height, MNDT::ImageType::GRAY_8BIT);Image purImage(pur, reWidth, reHeight, MNDT::ImageType::GRAY_8BIT);float xBase = static_cast<float>(width - 1) / static_cast<float>(reWidth - 1);float yBase = static_cast<float>(height - 1) / static_cast<float>(reHeight - 1);for (UINT32 row = 0; row < reHeight; row++){C_UINT32 srcRow = static_cast<UINT32>(row * yBase); for (UINT32 col = 0; col < reWidth; col++){C_UINT32 srcCol = static_cast<UINT32>(col * xBase);purImage.image[row][col] = srcImage.image[srcRow][srcCol];}}}
双线插值法
一开始与邻近内插一样取得倍率,不同的地方在于是乘上倍率的索引都必须要向上和向下取得索引,也就是说x和y计算出来都会有2个索引位置,组合起来为4个点,再依据算出来的缩放比例乘上像素(如下图)即是结果。
来源:[3]
双线插值的概念即是距离越近影响越大,假设,k点计算出来的四个点为a、b、c和d。
a对k的影响:(0.75 * 0.75) * a。b对k的影响:(0.25 * 0.75) * b。c对k的影响:(0.75 * 0.25) * c。d对k的影响:(0.25 * 0.25) * d。上述例子可以知道距离和影响可知道,影响 = 1 - 距离。
1. 计算倍率,取小数点第四位。
2. 计算缩放比例取得四个点权重。
3. 走访reWidth * reHeight大小,取得四个原先像素的位置,位置为现在索引值乘上上述的倍率,最后在乘上权重。
Library.h
void LinearResize8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, C_UINT32 reWidth, C_UINT32 reHeight);
Library.cpp
void Library::LinearResize8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, C_UINT32 reWidth, C_UINT32 reHeight){Image srcImage(const_cast<UCHAE*>(src), width, height, MNDT::ImageType::GRAY_8BIT);Image purImage(pur, reWidth, reHeight, MNDT::ImageType::GRAY_8BIT);C_FLOAT xBase = static_cast<int>(floor(static_cast<float>(width - 1) / static_cast<float>(reWidth - 1) * 1000.0f)) / 1000.0f;C_FLOAT yBase = static_cast<int>(floor(static_cast<float>(height - 1) / static_cast<float>(reHeight - 1) * 1000.0f)) / 1000.0f;C_FLOAT xProportion = static_cast<float>(reWidth) / static_cast<float>(width);C_FLOAT yProportion = static_cast<float>(reHeight) / static_cast<float>(height);C_FLOAT xOffset = xProportion - floor(xProportion); // 左边权重比例C_FLOAT yOffset = yProportion - floor(yProportion); // 上边权重比例//(0, 0), (0, 1), (1, 0), (1, 1)C_FLOAT w1 = (1.0f - xOffset) * (1.0f - yOffset);C_FLOAT w2 = xOffset * (1.0f - yOffset);C_FLOAT w3 = (1.0f - xOffset) * yOffset;C_FLOAT w4 = xOffset * yOffset;for (UINT32 row = 0; row < reHeight; row++){float y = row * yBase;C_UINT32 y1 = static_cast<UINT32>(floor(y));C_UINT32 y2 = static_cast<UINT32>(ceil(y));for (UINT32 col = 0; col < reWidth; col++){float x = col * xBase;C_UINT32 x1 = static_cast<UINT32>(floor(x));C_UINT32 x2 = static_cast<UINT32>(ceil(x));float pix = static_cast<float>(srcImage.image[y1][x1]) * w1+ static_cast<float>(srcImage.image[y1][x2]) * w2+ static_cast<float>(srcImage.image[y2][x1]) * w3+ static_cast<float>(srcImage.image[y2][x2]) * w4;purImage.image[row][col] = static_cast<UINT32>(pix);}}}
金字塔影像转换
金字塔也是拿来做影像转换,只不过它转换方式较为单纯,主要将影像放大或缩小2倍。
缩小金字塔
[1]的做法为先将它用高斯模糊,取得偶数行列。
1. 高斯模糊。
2. 走访改为+2即可缩小。
Library.h
/*PyramidDown8bit Parameter:src= source of imagepur= purpose of imagewidth= Image's widthheight= Image's height*/void PyramidDown8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height);
Library.cpp
void Library::PyramidDown8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height){UCHAE* data = new UCHAE[width * height];BlurGauss8bit(src, data, width, height, 5, 1.0f);Image srcImage(const_cast<UCHAE*>(src), width, height, MNDT::ImageType::GRAY_8BIT);for (UINT32 srcRow = 0; srcRow < height; srcRow += 2){for (UINT32 srcCol = 0; srcCol < width; srcCol += 2){*pur = srcImage.image[srcRow][srcCol];pur++;}}delete[] data;data = nullptr;}
放大金字塔
[1]的做法为行列都间隔一个像素0,在做高斯模糊。但这边测试结果只需要将间隔填入相同的像素即可(可在做一次高斯模糊)。
1. 计算放大后的大小。
2. 走访改为+2,并将间隔元素也设置相同像素。
Library.h
/*PyramidUp8bit Parameter:src= source of imagepur= purpose of imagewidth= Image's widthheight= Image's height*/void PyramidUp8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height);
Library.cpp
void Library::PyramidUp8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height){C_UINT32 purWidth = width << 1;C_UINT32 purHeight = height << 1;Image dataImage(pur, purWidth, purHeight, MNDT::ImageType::GRAY_8BIT);for (UINT32 purRow = 0; purRow < purHeight; purRow += 2){for (UINT32 purCol = 0; purCol < purWidth; purCol += 2){dataImage.image[purRow][purCol] = *src;dataImage.image[purRow + 1][purCol] = *src;dataImage.image[purRow][purCol + 1] = *src;dataImage.image[purRow + 1][purCol + 1] = *src;src++;}}}
仿射变换
[1]提到主要是取得三个点取得公式去做转换,公式其实就是要求出方程式的係数。假设点(x, y)要转换为(x', y),则公式为ax + by + c = x',然而有三个点,这时候就有三个方程式,这边使用高斯消去法求a、b和c。最后再将原始图像的索引值代入求出的方程式即是访设变换。
注:对高斯消去法有兴趣可以去网路上看相关原理,这里略过。
取得方程式係数
affine为3 * 5的阵列,每一水平列资料为[x, y, 1(b係数都是1), 'x, 'y]。
1. 计算高斯消去法。
2. 将结果指派给x和y的係数。
Library.h
/*SetAffineTransform Parameter:affine= point input and outputbaseX= ouput x base(a.b.c)baseY= ouput y base(a.b.c)row= row sizecol= col size*/void SetAffineTransform(float** affine, float* baseX, float* baseY, C_UINT32 row, C_UINT32 col);
Library.cpp
void Library::SetAffineTransform(float** affine, float* baseX, float* baseY, C_UINT32 row, C_UINT32 col){GaussianElimination gauss(affine, row, col);gauss.Compute();for (UINT32 index = 0; index < row; index++){baseX[index] = affine[index][col - 2];baseY[index] = affine[index][col - 1];}}
仿射转换
1. 走访,代入x和y的方程式计算索引值。
Library.h
/*Affine8bit Parameter:src= source of imagepur= purpose of imagewidth= Image's widthheight= Image's heightbaseX= ouput x base(a.b.c)baseY= ouput y base(a.b.c)*/void Affine8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, C_FLOAT* baseX, C_FLOAT* baseY);
Library.cpp
void Library::Affine8bit(C_UCHAE* src, UCHAE* pur, C_UINT32 width, C_UINT32 height, C_FLOAT* baseX, C_FLOAT* baseY){Image srcImage(const_cast<UCHAE*>(src), width, height, MNDT::ImageType::GRAY_8BIT);Image purImage(pur, width, height, MNDT::ImageType::GRAY_8BIT);for (UINT32 row = 0; row < height; row++){for (UINT32 col = 0; col < width; col++){C_UINT32 newRow = static_cast<UINT32>(col * baseY[0] + row * baseY[1] + baseY[2]);C_UINT32 newCol = static_cast<UINT32>(col * baseX[0] + row * baseX[1] + baseX[2]);if (newRow >= 0 && newRow < height && newCol >= 0 && newCol < width){purImage.image[newRow][newCol] = srcImage.image[row][col];}}}}
C#视窗原始码
C++函数原始码
结语
这次写的这函数库几乎都在处理8位元,没有把24位元加进去,对于24位元只有另外提出来,主要是怕混淆,再加上会多判断影响到效能,但主要还是知道原理才是最重要的,若有问题或有观念错误欢迎提问纠正。
参考文献
[1]阿洲(2015). OpenCV教学 | 阿洲的程式教学 from: http://monkeycoding.com/?page_id=12 (2018.11.10).
[2]维基百科(2018). 旋转矩阵 from: https://zh.wikipedia.org/wiki/%E6%97%8B%E8%BD%AC%E7%9F%A9%E9%98%B5#%E4%BA%8C%E7%BB%B4%E7%A9%BA%E9%97%B4 (2018.11.10).
[3]维基百科(2018). 双线性插值 from: https://zh.wikipedia.org/wiki/%E5%8F%8C%E7%BA%BF%E6%80%A7%E6%8F%92%E5%80%BC (2018.11.10).