一、图像错切原理
图像错切变换在图像几何形变方面非常有用,常见的错切变换分为X方向(水平) 与Y方向(垂直) 的错切变换。对应的数学矩阵分别如下:
X方向错切矩阵(y坐标不变,x坐标随y变化):
1 k 0 0 1 0 0 0 1 \] \\begin{bmatrix} 1 \& k \& 0 \\\\ 0 \& 1 \& 0 \\\\ 0 \& 0 \& 1 \\end{bmatrix} 100k10001
**Y方向错切矩阵**(x坐标不变,y坐标随x变化):
\[ 1 0 0 k 1 0 0 0 1 \] \\begin{bmatrix} 1 \& 0 \& 0 \\\\ k \& 1 \& 0 \\\\ 0 \& 0 \& 1 \\end{bmatrix} 1k0010001
假设 P ( x 1 , y 1 ) P(x_1, y_1) P(x1,y1) 为错切变换前的像素点, P ′ ( x 2 , y 2 ) P'(x_2, y_2) P′(x2,y2) 为变换后像素点:
* X方向错切变换: x 2 = x 1 + k ⋅ y 1 x_2 = x_1 + k \\cdot y_1 x2=x1+k⋅y1, y 2 = y 1 y_2 = y_1 y2=y1
* Y方向错切变换: y 2 = y 1 + k ⋅ x 1 y_2 = y_1 + k \\cdot x_1 y2=y1+k⋅x1, x 2 = x 1 x_2 = x_1 x2=x1
其中 k = tan ( θ ) k = \\tan(\\theta) k=tan(θ), θ \\theta θ 为错切角度(通常取0\~45度)。
## 二、opencv实现错切
### 2.1 实现步骤
基于C++ OpenCV实现错切变换,实现步骤如下:
1. **计算输出图像尺寸**:错切后图像会在水平或垂直方向扩展,需根据错切角度计算新的宽高(超出原图像部分填充背景色)。
2. **像素坐标映射**:遍历输出图像的每个像素,反向计算其在原图像中对应的源像素坐标(避免空洞问题)。
3. **线性插值**:由于源像素坐标可能为小数,通过线性插值计算最终像素值(原Java代码使用线性插值,此处保持一致)。
4. **背景色填充**:若源像素坐标超出原图像范围,填充预设背景色。
### 2.2 计算错切后图像的宽与高
根据错切方向(垂直/水平)和角度,通过正切函数计算扩展后的尺寸。
* 水平错切(X方向):输出宽度 = 原宽度 + 原高度 × tan ( θ ) \\tan(\\theta) tan(θ),高度不变。
* 垂直错切(Y方向):输出高度 = 原高度 + 原宽度 × tan ( θ ) \\tan(\\theta) tan(θ),宽度不变。
```cpp
// 角度转弧度(OpenCV中CV_PI表示π)
double angle_rad = angle * CV_PI / 180.0;
// 计算输出图像宽高
if (vertical) {
out_h = static_cast(src_h + src_w * tan(angle_rad));
out_w = src_w;
} else {
out_w = static_cast(src_w + src_h * tan(angle_rad));
out_h = src_h;
}
```
### 2.3 目标像素坐标映射
反向映射(从输出像素找原像素)可避免变换后图像出现空洞。根据错切方向调整映射公式:
* 水平错切(vertical=false): s r c x = d s t x − tan ( θ ) × ( d s t y − s r c h ) src_x = dst_x - \\tan(\\theta) \\times (dst_y - src_h) srcx=dstx−tan(θ)×(dsty−srch), s r c y = d s t y src_y = dst_y srcy=dsty
* 垂直错切(vertical=true): s r c y = d s t y − tan ( θ ) × ( d s t x − s r c w ) src_y = dst_y - \\tan(\\theta) \\times (dst_x - src_w) srcy=dsty−tan(θ)×(dstx−srcw), s r c x = d s t x src_x = dst_x srcx=dstx
```cpp
// 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 线性插值计算像素值
处理源像素坐标为小数的情况,通过相邻像素的线性加权(权重 u u u)计算最终像素值,同时处理边界(超出原图像范围填充背景色)。
```cpp
// 计算整数坐标(向下取整)
int y0 = static_cast(floor(src_y));
int x0 = static_cast(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(y0, x0); // 左上角像素
Vec3b p1 = vertical ? src.at(y1, x0) : src.at(y0, x1); // 相邻像素
// 线性插值:p = p0*(1-u) + p1*u
Vec3b result;
for (int c = 0; c < 3; ++c) { // 遍历B、G、R三个通道
result[c] = static_cast(p0[c] * (1 - u) + p1[c] * u);
}
return result;
```
### 2.5 完整代码
完整实现代码如下:
```cpp
#include
#include
#include
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(bg_color_[0]),
static_cast(bg_color_[1]),
static_cast(bg_color_[2]));
}
// 计算整数坐标(向下取整)
int y0 = static_cast(floor(src_y));
int x0 = static_cast(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(y0, x0);
Vec3b p1 = vertical_ ? src.at(y1, x0) : src.at(y0, x1);
// 线性插值计算每个通道
Vec3b result;
for (int c = 0; c < 3; ++c) {
result[c] = static_cast(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); // 垂直错切:高度 = 原高 + 原宽×k
out_w_ = src_w;
} else {
out_w_ = cvRound(src_w + src_h * k); // 水平错切:宽度 = 原宽 + 原高×k
out_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(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°

更多资料: