代码实现:
int main()
{
cv::Mat image = cv::imread("1.jpg", 0);
if (image.empty()) return -1;
//图像尺寸转换
const int nrows = image.rows, ncols = image.cols;
//获取DFT尺寸
int crows = cv::getOptimalDFTSize(nrows), ccols = cv::getOptimalDFTSize(ncols);
//复制图层,超过边界区域填充为0
cv::Mat result;
cv::copyMakeBorder(image, result, 0, crows - nrows, 0, ccols - ncols, cv::BORDER_CONSTANT, cv::Scalar(0));
//为傅里叶变换的结果(实部和虚部)分配存储空间
cv::Mat groupMat[] = { cv::Mat_<float>(result), cv::Mat::zeros(result.size(), CV_32F) };
//通道合并
cv::Mat mergeMat;
cv::merge(groupMat, 2, mergeMat);
//DFT变换
cv::dft(mergeMat, mergeMat);
//分离通道
cv::split(mergeMat, groupMat);
//计算幅值
cv::magnitude(groupMat[0], groupMat[1], groupMat[0]);
cv::Mat dftresult;
dftresult = groupMat[0].clone();
//对数变换
dftresult += 1;
cv::log(dftresult, dftresult);
//裁剪幅度图
dftresult = dftresult(cv::Rect(0, 0, ncols, nrows));
//归一化
cv::normalize(dftresult, dftresult, 0, 1, CV_MINMAX);
//图像类型转换
//输出图像类型,尺寸缩放因子,尺寸偏移量
dftresult.convertTo(dftresult, CV_8UC1, 255, 0);
//中心变换
int cx = dftresult.cols / 2, cy = dftresult.rows / 2;
cv::Mat tmp;
//Top-Left-为每个象限创建ROI
cv::Mat q0(dftresult, cv::Rect(0, 0, cx, cy));
//Bottom-Left
cv::Mat q1(dftresult, cv::Rect(0, cy, cx, cy));
//Top-Right
cv::Mat q2(dftresult, cv::Rect(cx, 0, cx, cy));
//Bottom-right
cv::Mat q3(dftresult, cv::Rect(cx, cy, cx, cy));
q0.copyTo(tmp), q3.copyTo(q0), tmp.copyTo(q3);
q1.copyTo(tmp), q2.copyTo(q1), tmp.copyTo(q2);
cv::imshow("dftresult", dftresult);
//固定阈值二值化
cv::Mat binaryMat;
cv::threshold(dftresult, binaryMat, 122, 255, CV_THRESH_BINARY);
cv::imshow("binaryMat", binaryMat);
//霍夫变换
//输入图像,输出线向量(线向量由两个元素组成,第一个是距离原点的距离,第二个是线旋转角)
//累积像素的距离分辨率,累积弧度的角度分辨率
//要检测一条直线所需最少的曲线交点
//多尺度霍夫变换参数,是一个距离分辨率因子
std::vector<cv::Vec2f> lines;
cv::HoughLines(binaryMat, lines, 1, CV_PI / 180, 100, 0, 0);
//检测线个数
std::cout << lines.size() << std::endl;
//绘制检测线
cv::Mat houghMat(binaryMat.size(), CV_8UC3);
for (int i = 0; i < lines.size(); i++)
{
float rho = lines[i][0], theta = lines[i][1];
cv::Point pt1, pt2;
//坐标变换生成线表达式
double a = cos(theta), b = sin(theta);
double x0 = a * rho, y0 = b * rho;
pt1.x = cvRound(x0 + 1000 * (-b));
pt1.y = cvRound(y0 + 1000 * a);
pt2.x = cvRound(x0 - 1000 * (-b));
pt2.y = cvRound(y0 - 1000 * a);
cv::line(houghMat, pt1, pt2, cv::Scalar(0, 0, 255), 3, CV_AA);
}
cv::imshow("houghMat", houghMat);
float theta = 0;
for (int i = 0; i < lines.size(); i++)
{
float thetaTemp = lines[i][1] * 180 / CV_PI;
if (thetaTemp > 0 && thetaTemp < 90) { theta = thetaTemp; break; }
}
//角度变换
//因为DFT之前的原图像在x,y方向上表示空间坐标,DFT是经过x,y方向上的傅里叶变换来统计像素在这两个方向上不同频率的分布情况
//所以DFT得到的图像在x,y方向上不再表示空间上的长度,而是频率。opencv的DFT函数设计为输出(频谱)图像与输入(像素)图像尺寸一致。
//如果输入图像是正方形,那么输出图像在x y方向上的(频率)坐标间隔是相同的;如果输入图像不是正方形,就会造成输出图像在x或y方向上的缩放,即两轴的坐标间隔不一致。
//正方形被拉伸为长方形,其对角线的斜率也势必产生变化。所以在检测直线之后,还要根据图像的原始尺寸调整这个对角线的倾角。
float angelT = nrows * tan(theta / 180 * CV_PI) / ncols;
theta = atan(angelT) * 180 / CV_PI;
std::cout << theta << std::endl;
//取图像中点
cv::Point2f centerPoint = cv::Point2f(ncols / 2, nrows / 2);
double scale = 1;
//计算旋转矩阵
cv::Mat warpMat = cv::getRotationMatrix2D(centerPoint, theta, scale);
//仿射变换
cv::Mat resultimage;
cv::warpAffine(image, resultimage, warpMat, resultimage.size());
cv::imshow("image", image);
cv::imshow("result", resultimage);
cv::waitKey(0);
return 0;
}
过程解释:
//图像尺寸转换
const int nrows = image.rows, ncols = image.cols;
//获取DFT尺寸
int crows = cv::getOptimalDFTSize(nrows), ccols = cv::getOptimalDFTSize(ncols);
//复制图层,超过边界区域填充为0
cv::Mat result;
cv::copyMakeBorder(image, result, 0, crows - nrows, 0, ccols - ncols, cv::BORDER_CONSTANT, cv::Scalar(0));
OpenCV中的DFT采用的是快速算法,这种算法在图像的尺寸是2、3和5的倍数时处理速度最快,所以需要用getOptimalDFTSize()找到最适合的尺寸,然后用copyMakeBorder()填充多余的部分。填充的颜色如果是纯色对变换结果的影响不会很大,后面寻找倾斜线的过程又会完全忽略这一点影响。
cv::Mat groupMat[] = { cv::Mat_<float>(result), cv::Mat::zeros(result.size(), CV_32F) };
cv::Mat mergeMat;
cv::merge(groupMat, 2, mergeMat);
cv::dft(mergeMat, mergeMat);
DFT要分别计算实部和虚部,把要处理的图像作为输入的实部、一个全零的图像作为输入的虚部。dft()输入和输出分别为单张图像,先要用merge()把实虚部图像合并,分别处于图像mergeMat的两个通道内。计算得到的实虚部仍然保存在mergeMat的两个通道内。
cv::split(mergeMat, groupMat);
cv::magnitude(groupMat[0], groupMat[1], groupMat[0]);
cv::Mat dftresult;
dftresult = groupMat[0].clone();
dftresult += 1;
cv::log(dftresult, dftresult);
一般都会用幅度图像来表示图像傅里叶的变换结果(傅里叶谱)。
幅度的计算公式:magnitude = sqrt(Re(DFT)^2 + Im(DFT)^2)。
由于幅度的变化范围很大,而一般图像亮度范围只有[0,255],容易造成一大片漆黑,只有几个点很亮。所以要对图像进行对数变换
dftresult = dftresult(cv::Rect(0, 0, ncols, nrows));
cv::normalize(dftresult, dftresult, 0, 1, CV_MINMAX);
dftresult.convertTo(dftresult, CV_8UC1, 255, 0);
int cx = dftresult.cols / 2, cy = dftresult.rows / 2;
cv::Mat tmp;
cv::Mat q0(dftresult, cv::Rect(0, 0, cx, cy));
cv::Mat q1(dftresult, cv::Rect(0, cy, cx, cy));
cv::Mat q2(dftresult, cv::Rect(cx, 0, cx, cy));
cv::Mat q3(dftresult, cv::Rect(cx, cy, cx, cy));
q0.copyTo(tmp), q3.copyTo(q0), tmp.copyTo(q3);
q1.copyTo(tmp), q2.copyTo(q1), tmp.copyTo(q2);
虽然用log()缩小了数据范围,但仍然不能保证数值都落在[0,255]之内,所以要先用normalize()规范化到[0,1]内,再用convertTo()把小数映射到[0,255]内的整数。dft()直接获得的结果中,低频部分位于四角,高频部分位于中间。习惯上会把图像做四等份,互相对调,使低频部分位于图像中心,也就是让频域原点位于中心。
运行结果:
从傅里叶谱可以明显地看到一条过中心点的倾斜直线。要想求出这个倾斜角,首先要在图像上找出这条直线。
一个很方便的方法是采用霍夫变换检测直线。霍夫变换要求输入图像是二值的,所以要用threshold()把图像二值化。
cv::Mat binaryMat;
cv::threshold(dftresult, binaryMat, 122, 255, CV_THRESH_BINARY);
cv::imshow("binaryMat", binaryMat);
std::vector<cv::Vec2f> lines;
cv::HoughLines(binaryMat, lines, 1, CV_PI / 180, 100, 0, 0);
cv::Mat houghMat(binaryMat.size(), CV_8UC3);
for (int i = 0; i < lines.size(); i++)
{
float rho = lines[i][0], theta = lines[i][1];
cv::Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a * rho, y0 = b * rho;
pt1.x = cvRound(x0 + 1000 * (-b));
pt1.y = cvRound(y0 + 1000 * a);
pt2.x = cvRound(x0 - 1000 * (-b));
pt2.y = cvRound(y0 - 1000 * a);
cv::line(houghMat, pt1, pt2, cv::Scalar(0, 0, 255), 3, CV_AA);
}
cv::imshow("houghMat", houghMat);
二值化的一种结果:
检测到的直线:
上面得到了三个角度,一个是0度,一个是90度,另一个就是我们所需要的倾斜角。要把这个角找出来,而且要考虑误差。
float theta = 0;
for (int i = 0; i < lines.size(); i++)
{
float thetaTemp = lines[i][1] * 180 / CV_PI;
if (thetaTemp > 0 && thetaTemp < 90) { theta = thetaTemp; break; }
}
float angelT = nrows * tan(theta / 180 * CV_PI) / ncols;
theta = atan(angelT) * 180 / CV_PI;
由于DFT的特点,只有输入图像是正方形时,检测到的角才是文本真正旋转的角度。但我们的输入图像不一定是正方形的,所以要根据图像的长宽比改变这个角度。
cv::Point2f centerPoint = cv::Point2f(ncols / 2, nrows / 2);
double scale = 1;
cv::Mat warpMat = cv::getRotationMatrix2D(centerPoint, theta, scale);
cv::Mat resultimage(image.size(), image.type());
cv::warpAffine(image, resultimage, warpMat, result.size());
cv::imshow("image", image);
cv::imshow("result", resultimage);
先用getRotationMatrix2D()获得一个2*3的仿射变换矩阵,再把这个矩阵输入warpAffine(),做一个单纯旋转的仿射变换。
运行结果: