【图像处理】图像错切变换
一、图像错切原理
图像错切变换在图像几何形变方面非常有用,常见的错切变换分为X方向(水平) 与Y方向(垂直) 的错切变换。对应的数学矩阵分别如下:
X方向错切矩阵(y坐标不变,x坐标随y变化):
[1k0010001]\begin{bmatrix} 1 & k & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}100k10001
Y方向错切矩阵(x坐标不变,y坐标随x变化):
[100k10001]\begin{bmatrix} 1 & 0 & 0 \\ k & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}1k0010001
假设 P(x1,y1)P(x_1, y_1)P(x1,y1) 为错切变换前的像素点,P′(x2,y2)P'(x_2, y_2)P′(x2,y2) 为变换后像素点:
- X方向错切变换:x2=x1+k⋅y1x_2 = x_1 + k \cdot y_1x2=x1+k⋅y1,y2=y1y_2 = y_1y2=y1
- Y方向错切变换:y2=y1+k⋅x1y_2 = y_1 + k \cdot x_1y2=y1+k⋅x1,x2=x1x_2 = x_1x2=x1
其中 k=tan(θ)k = \tan(\theta)k=tan(θ),θ\thetaθ 为错切角度(通常取0~45度)。
二、opencv实现错切
2.1 实现步骤
基于C++ OpenCV实现错切变换,实现步骤如下:
- 计算输出图像尺寸:错切后图像会在水平或垂直方向扩展,需根据错切角度计算新的宽高(超出原图像部分填充背景色)。
- 像素坐标映射:遍历输出图像的每个像素,反向计算其在原图像中对应的源像素坐标(避免空洞问题)。
- 线性插值:由于源像素坐标可能为小数,通过线性插值计算最终像素值(原Java代码使用线性插值,此处保持一致)。
- 背景色填充:若源像素坐标超出原图像范围,填充预设背景色。
2.2 计算错切后图像的宽与高
根据错切方向(垂直/水平)和角度,通过正切函数计算扩展后的尺寸。
- 水平错切(X方向):输出宽度 = 原宽度 + 原高度 × tan(θ)\tan(\theta)tan(θ),高度不变。
- 垂直错切(Y方向):输出高度 = 原高度 + 原宽度 × tan(θ)\tan(\theta)tan(θ),宽度不变。
// 角度转弧度(OpenCV中CV_PI表示π)
double angle_rad = angle * CV_PI / 180.0;
// 计算输出图像宽高
if (vertical) {out_h = static_cast<int>(src_h + src_w * tan(angle_rad));out_w = src_w;
} else {out_w = static_cast<int>(src_w + src_h * tan(angle_rad));out_h = src_h;
}
2.3 目标像素坐标映射
反向映射(从输出像素找原像素)可避免变换后图像出现空洞。根据错切方向调整映射公式:
- 水平错切(vertical=false):srcx=dstx−tan(θ)×(dsty−srch)src_x = dst_x - \tan(\theta) \times (dst_y - src_h)srcx=dstx−tan(θ)×(dsty−srch),srcy=dstysrc_y = dst_ysrcy=dsty
- 垂直错切(vertical=true):srcy=dsty−tan(θ)×(dstx−srcw)src_y = dst_y - \tan(\theta) \times (dst_x - src_w)srcy=dsty−tan(θ)×(dstx−srcw),srcx=dstxsrc_x = dst_xsrcx=dstx
// dst_y:输出图像的行(对应y坐标),dst_x:输出图像的列(对应x坐标)
double src_y, src_x;
if (vertical) {src_y = dst_y - tan(angle_rad) * (dst_x - src_w);src_x = dst_x;
} else {src_x = dst_x - tan(angle_rad) * (dst_y - src_h);src_y = dst_y;
}
2.4 线性插值计算像素值
处理源像素坐标为小数的情况,通过相邻像素的线性加权(权重 uuu)计算最终像素值,同时处理边界(超出原图像范围填充背景色)。
// 计算整数坐标(向下取整)
int y0 = static_cast<int>(floor(src_y));
int x0 = static_cast<int>(floor(src_x));
// 计算插值权重 u(小数部分)
double u_y = src_y - y0; // 垂直方向权重(仅垂直错切时有效)
double u_x = src_x - x0; // 水平方向权重(仅水平错切时有效)
double u = vertical ? u_y : u_x;// 边界检查:超出原图像范围返回背景色
if (y0 < 0 || y0 >= src_h || x0 < 0 || x0 >= src_w) {return bg_color;
}// 相邻像素坐标(处理边界,避免越界)
int y1 = (y0 + 1 >= src_h) ? y0 : y0 + 1;
int x1 = (x0 + 1 >= src_w) ? x0 : x0 + 1;// 获取相邻像素的BGR值(OpenCV默认BGR通道)
Vec3b p0 = src.at<Vec3b>(y0, x0); // 左上角像素
Vec3b p1 = vertical ? src.at<Vec3b>(y1, x0) : src.at<Vec3b>(y0, x1); // 相邻像素// 线性插值:p = p0*(1-u) + p1*u
Vec3b result;
for (int c = 0; c < 3; ++c) { // 遍历B、G、R三个通道result[c] = static_cast<uchar>(p0[c] * (1 - u) + p1[c] * u);
}
return result;
2.5 完整代码
完整实现代码如下:
#include <opencv2/opencv.hpp>
#include <cmath>
#include <iostream>using namespace cv;
using namespace std;class ShearFilter {
private:double angle_; // 错切角度(度)Scalar bg_color_; // 背景色(默认黑色,BGR格式)bool vertical_; // 是否垂直错切(true=Y方向,false=X方向)int out_w_; // 输出图像宽度int out_h_; // 输出图像高度/*** @brief 计算源像素的线性插值结果* @param src 原图像* @param src_y 源图像y坐标(行)* @param src_x 源图像x坐标(列)* @return 插值后的像素值(BGR)*/Vec3b getPixel(const Mat& src, double src_y, double src_x) const {int src_h = src.rows;int src_w = src.cols;double angle_rad = angle_ * CV_PI / 180.0;// 边界检查:超出原图像范围返回背景色if (src_y < 0 || src_y >= src_h || src_x < 0 || src_x >= src_w) {return Vec3b(static_cast<uchar>(bg_color_[0]),static_cast<uchar>(bg_color_[1]),static_cast<uchar>(bg_color_[2]));}// 计算整数坐标(向下取整)int y0 = static_cast<int>(floor(src_y));int x0 = static_cast<int>(floor(src_x));// 计算插值权重(小数部分)double u = vertical_ ? (src_y - y0) : (src_x - x0);// 相邻像素坐标(处理边界,避免越界)int y1 = (y0 + 1 >= src_h) ? y0 : y0 + 1;int x1 = (x0 + 1 >= src_w) ? x0 : x0 + 1;// 获取相邻像素的BGR值Vec3b p0 = src.at<Vec3b>(y0, x0);Vec3b p1 = vertical_ ? src.at<Vec3b>(y1, x0) : src.at<Vec3b>(y0, x1);// 线性插值计算每个通道Vec3b result;for (int c = 0; c < 3; ++c) {result[c] = static_cast<uchar>(round(p0[c] * (1 - u) + p1[c] * u));}return result;}public:// 构造函数(默认:20度、黑色背景、水平错切)ShearFilter() : angle_(20.0), bg_color_(0, 0, 0), vertical_(false), out_w_(0), out_h_(0) {}// Setter方法void setAngle(double angle) { angle_ = angle; }void setBgColor(const Scalar& color) { bg_color_ = color; }void setVertical(bool vertical) { vertical_ = vertical; }// Getter方法(获取输出图像尺寸)int getOutWidth() const { return out_w_; }int getOutHeight() const { return out_h_; }/*** @brief 执行错切变换* @param src 输入图像(CV_8UC3)* @param dst 输出图像(自动创建)*/void filter(const Mat& src, Mat& dst) {if (src.empty() || src.type() != CV_8UC3) {cerr << "输入图像为空或格式错误(需CV_8UC3)!" << endl;return;}int src_h = src.rows;int src_w = src.cols;double angle_rad = angle_ * CV_PI / 180.0;double k = tan(angle_rad); // 错切系数// 1. 计算输出图像尺寸(保持不变,但用cvRound确保精度)if (vertical_) {out_h_ = cvRound(src_h + src_w * k); // 垂直错切:高度 = 原高 + 原宽×kout_w_ = src_w;} else {out_w_ = cvRound(src_w + src_h * k); // 水平错切:宽度 = 原宽 + 原高×kout_h_ = src_h;}cout << "错切后尺寸:宽=" << out_w_ << ",高=" << out_h_ << endl;// 2. 创建输出图像并初始化背景色(避免随机值)dst = Mat::zeros(out_h_, out_w_, CV_8UC3);dst.setTo(bg_color_); // 显式填充背景色// 3. 遍历输出图像,计算每个像素的颜色(修正映射公式)for (int dst_y = 0; dst_y < out_h_; ++dst_y) {for (int dst_x = 0; dst_x < out_w_; ++dst_x) {double src_y, src_x;if (vertical_) {// 垂直错切:正确映射公式src_x = dst_x; // x坐标不变src_y = dst_y - k * dst_x; // y坐标随x偏移(去掉src_w偏移)} else {// 水平错切:正确映射公式src_y = dst_y; // y坐标不变src_x = dst_x - k * dst_y; // x坐标随y偏移(去掉src_h偏移)}// 插值获取像素值并赋值(仅当源坐标有效时覆盖背景)if (src_x >= 0 && src_x < src_w && src_y >= 0 && src_y < src_h) {dst.at<Vec3b>(dst_y, dst_x) = getPixel(src, src_y, src_x);}}}}
};// 测试代码
int main() {// 1. 读取输入图像(替换为你的图像路径)Mat src = imread("C:/Users/Lenovo/Pictures/pictures/flower.jpg");if (src.empty()) {cerr << "无法读取图像!" << endl;return -1;}// 2. 初始化错切滤波器ShearFilter shear_filter;shear_filter.setAngle(20.0); // 设置错切角度(20度)shear_filter.setBgColor(Scalar(0,0,0));// 背景色:黑色(BGR)shear_filter.setVertical(true); // 水平错切(X方向):false;垂直错切(Y方向):true// 3. 执行错切变换Mat dst;shear_filter.filter(src, dst);// 4. 显示结果imshow("原图像", src);imshow("错切变换后", dst);// 5. 保存结果(可选)imwrite("C:/Users/Lenovo/Pictures/pictures/flower_out.jpg", dst);// 等待按键退出waitKey(0);destroyAllWindows();return 0;
}
三、运行结果
原图

水平错切30°

垂直错切20°

更多资料:https://github.com/0voice
