基于Hu距的图像旋转矫正之OpenCV实现

目录

1、常见图像旋转矫正方法

1.1 基于图像边缘轮廓的旋转矫正

1.2 基于傅里叶变换以及霍夫直线检测的旋转矫正

2、基于Hu距图像旋转矫正

2.1 Hu旋转不变性

2.2 实现步骤

2.2.1 分别计算图像二阶距

2.2.2 利用得到的二阶距计算图像偏转角度

2.2.3 利用仿射变换对图像进行旋转矫正

2.4 程序实现

2.5 旋转矫正效果验证


系统环境 Windows 10 64 位 + OpenCV 3.4.1 64 位

1、常见图像旋转矫正方法

常见的图像旋转矫正方式有:基于图像边缘轮廓的旋转矫正和基于傅里叶变换以及霍夫直线检测的旋转矫正两种方法。

1.1 基于图像边缘轮廓的旋转矫正

1)图像灰度化

2)阈值二值化

3)检测轮廓

4)提取轮廓的包围矩阵(图像前景掩膜)

5)通过提取的包围矩阵获取偏转角度

6)利用仿射变换对图像进行偏转

具体实现参考:OpenCV探索之路(十六):图像矫正技术深入探讨

1.2 基于傅里叶变换以及霍夫直线检测的旋转矫正

1)利用DTF变换计算图像的频谱图

2)频谱中心移动

3)对移动后的频谱图进行二值分割

4)利用霍夫直线检测计算图像倾斜角度

5)利用仿射变换对图像进行旋转矫正

具体实现参考:OpenCV实现基于傅里叶变换的旋转文本校正

傅里叶变换的原理解析参考:傅里叶分析之掐死教程(完整版)OpenCV图像的傅里叶变换-(补番)

2、基于Hu距图像旋转矫正

除了以上两种方式可以实现图像旋转矫正外,还可以利用HU距旋转不变性对图像进行旋转矫正。

2.1 Hu旋转不变性

图片经过任意角度旋转、任意比例缩放Hu矩都保持不变,即图像Hu 矩的平移、旋转不变性。Hu旋转不变性详情参考:图像不变性特征—hu矩图像特征_图像矩(Hu矩)。七个不变矩由二阶和三阶中心矩的线性组合构成,具体表达式如下:

image


在实际应用中,在对图片中物体进行处理时,只有 M1 和 M2 体现良好的不变性,其余的几个不变矩产生的误差比较大。因此,认为只有基于二阶矩的不变矩对二维物体的描述具有良好的旋转、缩放和平移不变性(M1和 M2 由二阶矩组成)。

图像的二阶矩表示图像的主轴,二阶矩值的大小可以确定图像的长轴和短轴,以此来计算轴的方向角度。最后,用主轴相对于法线轴的逆时针旋转角度来近似图像的旋转方向。

2.2 实现步骤

2.2.1 分别计算图像二阶距

image

其中,I(x,y)表示图像在点(x,y)处的灰度值,cx,cy表示图像的质心坐标。

2.2.2 利用得到的二阶距计算图像偏转角度

image

2.2.3 利用仿射变换对图像进行旋转矫正

2.4 程序实现

#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/opencv.hpp>
#include<iostream>

using namespace std;

using namespace cv;


//计算图像质心坐标
bool calCentroid(Mat srcImg, int width, int height, int *x, int *y)
{
    if (srcImg.empty())
    {
        return false;
    }

    int i, j;

    long long m00 = 0, m10 = 0, m01 = 0;

    for (i = 0; i < height; i++)
    {
        for (j = 0; j < width; j++)
        {
            uchar tmp = srcImg.at<uchar>(i, j);
            m00 += tmp;
            m10 += tmp * j;
            m01 += tmp * i;
        }
    }

    if (m00 != 0)
    {
        *x = m10 / m00;
        *y = m01 / m00;
        //cout <<"x:"<<*x<< endl;
        //cout << "y:" << *y << endl;
    }
    else
    {
        *x = 0;
        *y = 0;

        return false;

    }

    return true;

}


//计算偏转角度
double getAngle(Mat srcImg)
{

    double angle = 0;

    long long  m00 = 0, m11 = 0, m12 = 0, m22 = 0;

    int Rho11 = 0, Rho12 = 0, Rho22 = 0;

    int gx = 0, gy = 0;

    //计算图像偏转角度
    if (calCentroid(srcImg, srcImg.cols, srcImg.rows, &gx, &gy) == true)//计算图像质心
    {
        //cout << "gx:" << gx << endl;
        //cout << "gy:" << gy << endl;

        //归一化二阶中心矩
        for (int i = 0; i < srcImg.rows; i++)
        {
            for (int j = 0; j < srcImg.cols; j++)
            {
                uchar tmp = srcImg.at<uchar>(i, j);
                m00 = m00 + tmp;
                m12 = m12 + (i - gy)*(j - gx)*tmp;
                m11 = m11 + (i - gy)*(i - gy)*tmp;
                m22 = m22 + (j - gx)*(j - gx)*tmp;
            }
        }

        Rho11 = m11 / m00;
        Rho12 = m12 / m00;
        Rho22 = m22 / m00;

    }

    float tempx = 0, tempy = 0;


    //计算偏转角度
    if (Rho11 > Rho22)
    {
        tempy = Rho11 - Rho22 + sqrt((float)((Rho11 - Rho22)*(Rho11 - Rho22) + 4 * Rho12*Rho12));
        tempx = (float)(-2 * Rho12);

        
        angle = atan(tempy / tempx) * 180 / CV_PI;
        
    }
    else
    {

        tempy = (float)(-2 * Rho12);
        tempx = Rho22 - Rho11 + sqrt((float)((Rho22 - Rho11)*(Rho22 - Rho11) + 4 * Rho12*Rho12));

        angle = atan(tempy / tempx) * 180 / CV_PI;
        
    }


    //偏转角度范围:-45=< angle <=45
    if (angle >= -45 && angle <= 45)
    {
        angle = angle;
    }
    else if (angle < -45)
    {
        angle = -(90 + angle);
    }
    else if (angle > 45)
    {

        angle = 90 - angle;
    }


    return angle;
}




//对图像进行旋转矫正
bool ContoursCorrection(Mat srcImg, Mat &dstImage)
{
    
    if (srcImg.empty())
    {
        return false;
    }

    Mat gray, binImg;
    //灰度化
    cvtColor(srcImg, gray, COLOR_RGB2GRAY);
    imshow("灰度图", gray);
    //二值化
    threshold(gray, binImg, 100, 200, CV_THRESH_BINARY);
    imshow("二值化", binImg);

    vector<vector<Point> > contours;
    vector<Rect> boundRect(contours.size());

    //注意第5个参数为CV_RETR_EXTERNAL,只检索外框  
    findContours(binImg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //找轮廓

    cout << contours.size() << endl;
    for (int i = 0; i < contours.size(); i++)
    {
        //需要获取的坐标  
        CvPoint2D32f rectpoint[4];
        CvBox2D rect = minAreaRect(Mat(contours[i]));

        cvBoxPoints(rect, rectpoint); //获取4个顶点坐标  
        //与水平线的角度  
        
        

        int line1 = sqrt((rectpoint[1].y - rectpoint[0].y)*(rectpoint[1].y - rectpoint[0].y) + (rectpoint[1].x - rectpoint[0].x)*(rectpoint[1].x - rectpoint[0].x));
        int line2 = sqrt((rectpoint[3].y - rectpoint[0].y)*(rectpoint[3].y - rectpoint[0].y) + (rectpoint[3].x - rectpoint[0].x)*(rectpoint[3].x - rectpoint[0].x));
    
        //面积太小的直接pass
        if (line1 * line2 < 600)
        {
            continue;
        }

        

        //新建一个感兴趣的区域图,大小跟原图一样大  
        Mat RoiSrcImg(srcImg.rows, srcImg.cols, CV_8UC3); //注意这里必须选CV_8UC3
        RoiSrcImg.setTo(0); //颜色都设置为黑色  
        //imshow("新建的ROI", RoiSrcImg);
        //对得到的轮廓填充一下  
        drawContours(binImg, contours, -1, Scalar(255), CV_FILLED);

        //抠图到RoiSrcImg
        srcImg.copyTo(RoiSrcImg, binImg);

        ////计算偏转角度
        double angle = getAngle(binImg);

        cout <<"angle:"<<angle<< endl;


        //为了让正方形横着放,所以旋转角度是不一样的。竖放的,给他加90度,翻过来  
        if (line1 > line2)
        {
            angle = 90 + angle;
        }


        //再显示一下看看,除了感兴趣的区域,其他部分都是黑色的了  
        namedWindow("RoiSrcImg", 1);
        imshow("RoiSrcImg", RoiSrcImg);

        
        //对RoiSrcImg进行旋转  
        Point2f center = rect.center;  //中心点 

        Mat M2 = getRotationMatrix2D(center, angle, 1);//计算旋转加缩放的变换矩阵 

        warpAffine(RoiSrcImg, dstImage, M2, RoiSrcImg.size(), 1, 0, Scalar(0));//仿射变换 

        imshow("旋转之后", dstImage);
    
    }


    
    return true;
    

}




int main(int argc, char *argv[]) 
{

    //读取输入图像
    Mat input = imread("src.png", IMREAD_COLOR);

    Mat out(input.size(),CV_8UC3,Scalar(0,0,0));

    ContoursCorrection(input, out);

    
    
    waitKey(0);

    system("pause");

    return 0;

}

2.5 旋转矫正效果验证

image

从上图中可以发现,旋转矫正后图像由倾斜转换成为水平。


公众号《Slater》回复“机器学习”即可免费获取一份机器学习资料。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342