这些天,敲代码的时候遇到了一个需要自动纠正图片角度的问题,想了很久不知道怎么办。正好老师在上课的时候提到了霍夫变换这个名词,我犹如发现了金矿,回来就猛地一通百度。无奈自己智商实在不太够,网上大神说的我看了很久依然不太理解,于是浪费了很多时间,宛如回到了高中(或者初中)画直线方程图像的年代……
于是,在我终于弄明白之后,就有了这篇文章。写给和我一样的新人看看,也欢迎大神批评指正。
首先,霍夫变换的作用是找出一张图片中的直线(也可以是圆等更复杂的图形,这里只讨论直线),那么怎么找呢?这里需要回顾一下中学数学的极坐标相关知识(至于为什么用极坐标而不是直角坐标,后面再说):
假设图片中有一条直线BC,那么对于BC上任意一点A(x,y),连接AO,将
∠AOB
记为
∠θ
,将OA记为
ρ
则:
因此,我们可以使用公式1表示每一个图片上任何一个点到原点的长度。那么,在这条直线上的其他点呢?霍夫变换的思想就是,在同一条直线上的所有点,当
θ=90∘−∠ABO
,即
OA⊥BC
时,使用公式1都会取得相同的值。下面我尝试简单说明一下,这里混用了直角坐标和极坐标,怪我懒吧……
由上面的式子可知,当 θ 的取值使得 OA和 BC垂直时, BC上每个点经过公式1计算得到的值都相同。于是我们可以开始写代码了。
首先我们需要一张二值化的图片(如果不了解灰度化、二值化等概念,建议先百度或者参考我的其他文章),设有一张二值化的图片,前景为白色,背景为黑色,我们的程序遍历每个像素,如果是前景色,那么就针对该像素的坐标,使用每一个可能的
θ
值来计算相应的
ρ
值。计算完毕后,对每一个
(θ,ρ)
出现的次数进行保存。根据前面所说的,凡是一条直线上的点,经过计算都会取得相同的值,因此当出现的次数超过一定阀值时,这个
(θ,ρ)
可以看做是一条直线。
如果需要将直线转换为直角坐标的话,那么:
好了,讲了这么多,下面放代码吧。代码用OpenCV读取图片文件,其他自己写:
Line HoughLines(Mat &mat)
{
if (mat.channels() > 1)
{
throw (std::exception("ERROR"));
//这里偷个懒,假设传入的图片已经过二值化处理
}
uchar foreColor = 255;
int xCenter = mat.cols / 2;
int yCenter = mat.rows / 2;
int *angelMap[60];
//theta角的取值范围是-90度到90度,每3度为一个区间,如需提高精确度,可以细分区间
int maxP = static_cast<int>(sqrt(xCenter * xCenter + yCenter * yCenter));
//计算rho的取值范围,每2个单位为一个区间,包括正负
for (size_t idx = 0; idx < 60; idx++)
{
angelMap[idx] = new int[maxP + 1]{ 0 };
//分配储存空间,记录每个(rho, theta)值出现的次数
}
for (size_t idx = 0; idx < mat.rows; idx++)
{
uchar* ptr = mat.ptr<uchar>(idx);
for (size_t subIdx = 0; subIdx < mat.cols; subIdx++)
{
if (ptr[subIdx] == foreColor)
{
int x = subIdx - xCenter;
int y = yCenter - idx;
//将坐标原点转换为图片中间,方便计算
for (int theta = -90; theta < 90; theta+=3)
{
int p = (static_cast<int>(x * cosf(PIRate * theta)
+ y * sinf(PIRate * theta)) + maxP) / 2;
angelMap[theta / 3 + 30][p]++;
//分别将rho和theta的值映射到数组的索引下标,然后修改计数
}
}
}
}
int maxRho = 0, maxTheta = 0, maxVal = 0;
for (size_t idx = 0; idx < 60; idx++)
{
//这里偷个懒,只计算最大的那条直线
for (size_t subIdx = 0; subIdx <= maxP; subIdx++)
{
if (angelMap[idx][subIdx] > maxVal)
{
maxVal = angelMap[idx][subIdx];
maxRho = subIdx;
maxTheta = idx;
}
}
}
int angle = (maxTheta - 30) * 3;
Line line(-cosf(angle * PIRate) / sinf(angle * PIRate),
(maxRho * 2 - maxP) / sinf(angle * PIRate));
for (size_t idx = 0; idx < 60; idx++)
{
delete[] angelMap[idx];
}
return line;
}
另有Line的数据结构,简单的写一下:
struct Line
{
float k;
float b;
Line(float k, float b)
{
this->k = k;
this->b = b;
}
int getY(int x)
{
return static_cast<int>(k * x + b + 0.5f);
}
int getX(int y)
{
return static_cast<int>((y - b) / k + 0.5f);
}
};
然后简单测试一下,测试代码就懒得放了,直接看效果图吧(寝室随手拍的,请忽略那一堆衣服)~
效果还不错~完结撒花!