图像矫正 + 文本矫正 技术深入探讨

刚进入实验室导师就交给我一个任务,就是让我设计算法给图像进行矫正。哎呀,我不太会图像这块啊,不过还是接下来了,硬着头皮开干吧!

那什么是图像的矫正呢?举个例子就好明白了。

我的好朋友小明给我拍了这几张照片,因为他的拍照技术不咋地,照片都拍得歪歪扭扭的,比如下面这些照片:

人民币

发票

文本

这些图片让人看得真不舒服!看个图片还要歪脖子看,实在是太烦人了!我叫小明帮我扫描一下一本教科书,小明把每一页书都拍成上面的文本那样了。好气啊那该怎么办呢?一页一页用PS来处理?1000页的矫正啊,当然交给计算机去做!

真的,对于图像矫正的问题,在图像处理领域还真得多,比如人民币的矫正、文本的矫正、车牌的矫正、身份证矫正等等。这些都是因为拍摄者总不可能100%正确地拍摄好图片,这就要求我们通过后期的图像处理技术将图片还原好,才能进一步做后面的处理,比如数字分割啊数字识别啊,不然歪歪扭扭的文字数字,想识别出来估计就很难了。

上面几个图,我们在日常生活中遇到的可不少,因为拍摄时拍的不好,导致拍出来的图片歪歪扭扭的,很不自然,那么我们能不能把这些图片尽可能地矫正过来呢?

OpenCV告诉我们,没问题!工具我给你,算法你自己设计!

比如图一,我要想将人民币矫正,并且把人民币整个抠出来保存,该怎么做?那就涉及到了图像的矫正和感兴趣区域提取两大技术了。

总的来说,要进行进行图像矫正,至少有以下几项知识储备:

  • 轮廓提取技术
  • 霍夫变换知识
  • ROI感兴趣区域知识

下面以人民币矫正、发票矫正、文本矫正为例,一步步剖析如何实现图像矫正。

首先分析如何矫正人民币。

比如我们要矫正这张人民币,思路应该是怎么样?

首先分析这张图的特点。

在这张图里,人民币有一定的倾斜角度,但是角度不大;人民币的背景是黑色的,而且人民币的边缘应该比较明显。

没错,我们就抓住人民币的的边缘比较明显来做文章!我们是不是可以先把人民币的轮廓找出来(找出来的轮廓当然就是一个大大的矩形),然后用矩形去包围它,得到他的旋转角度,然后根据得到的角度进行旋转,那样不就可以实现矫正了吗!

再详细地总结处理步骤:

  1. 图片灰度化
  2. 阈值二值化
  3. 检测轮廓
  4. 寻找轮廓的包围矩阵,并且获取角度
  5. 根据角度进行旋转矫正
  6. 对旋转后的图像进行轮廓提取
  7. 对轮廓内的图像区域抠出来,成为一张独立图像

我把该矫正算法命名为基于轮廓提取的矫正算法,因为其关键技术就是通过轮廓来获取旋转角度。

C++版本实现,但是由于使用python opencv实现,达不到这种效果,故后面贴出了python opencv实现的代码

#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;

//第一个参数:输入图片名称;第二个参数:输出图片名称
void GetContoursPic(const char* pSrcFileName, const char* pDstFileName)
{
	Mat srcImg = imread(pSrcFileName);
	imshow("原始图", srcImg);
	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个顶点坐标  
		//与水平线的角度  
		float angle = rect.angle;
		cout << angle << endl;

		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));
		//rectangle(binImg, rectpoint[0], rectpoint[3], Scalar(255), 2);
		//面积太小的直接pass
		if (line1 * line2 < 600)
		{
			continue;
		}

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

		//新建一个感兴趣的区域图,大小跟原图一样大  
		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);


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

		//创建一个旋转后的图像  
		Mat RatationedImg(RoiSrcImg.rows, RoiSrcImg.cols, CV_8UC1);
		RatationedImg.setTo(0);
		//对RoiSrcImg进行旋转  
		Point2f center = rect.center;  //中心点  
		Mat M2 = getRotationMatrix2D(center, angle, 1);//计算旋转加缩放的变换矩阵 
		warpAffine(RoiSrcImg, RatationedImg, M2, RoiSrcImg.size(),1, 0, Scalar(0));//仿射变换 
		imshow("旋转之后", RatationedImg);
		imwrite("r.jpg", RatationedImg); //将矫正后的图片保存下来
	}

#if 1
	//对ROI区域进行抠图

	//对旋转后的图片进行轮廓提取  
	vector<vector<Point> > contours2;
	Mat raw = imread("r.jpg");
	Mat SecondFindImg;
	//SecondFindImg.setTo(0);
	cvtColor(raw, SecondFindImg, COLOR_BGR2GRAY);  //灰度化  
	threshold(SecondFindImg, SecondFindImg, 80, 200, CV_THRESH_BINARY);
	findContours(SecondFindImg, contours2, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
	//cout << "sec contour:" << contours2.size() << endl;

	for (int j = 0; j < contours2.size(); j++)
	{
		//这时候其实就是一个长方形了,所以获取rect  
		Rect rect = boundingRect(Mat(contours2[j]));
		//面积太小的轮廓直接pass,通过设置过滤面积大小,可以保证只拿到外框
		if (rect.area() < 600)
		{
			continue;
		}
		Mat dstImg = raw(rect);
		imshow("dst", dstImg);
		imwrite(pDstFileName, dstImg);
	}
#endif


}


void main()
{
	GetContoursPic("6.jpg", "FinalImage.jpg");
	waitKey();
}

效果依次如下:
原始图

二值化图

掩膜mask是这样的

旋转矫正之后

将人民币区域抠出来

python opencv版本:

def point_judge(center, bbox):
    """
    用于将矩形框的边界按顺序排列
    :param center: 矩形中心的坐标[x, y]
    :param bbox: 矩形顶点坐标[[x1, y1], [x2, y2], [x3, y3], [x4, y4]]
    :return: 矩形顶点坐标,依次是 左下, 右下, 左上, 右上
    """
    left = []
    right = []
    for i in range(4):
        if bbox[i][0] > center[0]:  # 只要是x坐标比中心点坐标大,一定是右边
            right.append(bbox[i])
        else:
            left.append(bbox[i])
    if right[0][1] > right[1][1]:  # 如果y点坐标大,则是右上
        right_down = right[1]
        right_up = right[0]
    else:
        right_down = right[0]
        right_up = right[1]

    if left[0][1] > left[1][1]:  # 如果y点坐标大,则是左上
        left_down = left[1]
        left_up = left[0]
    else:
        left_down = left[0]
        left_up = left[1]
    return left_down, right_down, left_up, right_up

def adjust_money(image):

    #灰度化,二值化
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    ret, bianry = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

    #腐蚀膨胀
    dilate = cv2.dilate(bianry, (3, 3), iterations=3)
    erode = cv2.erode(dilate, (3, 3), iterations=3)

    #找轮廓,排序轮廓
    _, contours, hierarchy = cv2.findContours(erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    contours.sort(key=len, reverse=True)

    #minAreaRect获取轮廓的最小外接矩形,返回矩形的中心坐标,长宽,旋转角度
    #boxPoints解析最小外接矩形获取到最小外接矩形的四个顶点,注意四个顶点的排序规则
    #使用排序后第一个最大轮廓contours[0]
    retangle = cv2.minAreaRect(contours[0])
    boxes = cv2.boxPoints(retangle)

    #获取最小外接矩形的高和宽,对于人民币来说 宽大于高
    height = math.sqrt((boxes[1][0] - boxes[0][0]) ** 2 + (boxes[1][1] - boxes[0][1]) ** 2)
    weight = math.sqrt((boxes[3][0] - boxes[0][0]) ** 2 + (boxes[3][1] - boxes[0][1]) ** 2)

    left_down, right_down, left_up, right_up = point_judge([int(retangle[0][0]), int(retangle[0][1])], boxes)
    src = np.float32([left_down, right_down, left_up, right_up])  # 这里注意必须对应

    #如果高大于宽,那么需要重新调整最小外接矩形的四个顶点与转换矩阵的对应关系
    # if height > weight:
    #     src = np.float32([right_down, right_up, left_down, left_up])

    dst = np.float32(
        [[0, 0], [int(max(retangle[1][0], retangle[1][1])), 0], [0, int(min(retangle[1][0], retangle[1][1]))],
         [int(max(retangle[1][0], retangle[1][1])),
          int(min(retangle[1][0], retangle[1][1]))]])  # rect中的宽高不清楚是个怎么机制,但是对于身份证,肯定是宽大于高,因此加个判定
    m = cv2.getPerspectiveTransform(src, dst)  # 得到投影变换矩阵
    result = cv2.warpPerspective(gray, m,
                                 (int(max(retangle[1][0], retangle[1][1])), int(min(retangle[1][0], retangle[1][1]))),
                                 flags=cv2.INTER_CUBIC)  # 投影变换

    cv2.drawContours(image,contours[0],-1,(0,0,255),2)
    cv2.imshow("original image", image)
    cv2.imshow("binary image", erode)
    cv2.imshow("warp image", result)

if __name__=='__main__':
    image = cv2.imread("7.png",cv2.IMREAD_COLOR)
    adjust_money(image)
    cv2.waitKey(0)

 

该算法的效果还是很不错的!那赶紧试试其他图片,我把倾斜的发票图像拿去试试。

对于pyhon opencv版本代码,需要把加上后面这段代码才能正确旋转图像:

#如果高大于宽,那么需要重新调整最小外接矩形的四个顶点与转换矩阵的对应关系
    if height > weight:
         src = np.float32([right_down, right_up, left_down, left_up])

另外使用最小外接矩形中心点和旋转角度进行发票矫正的代码:

def adjust_invoice(image):
    # 灰度化,二值化
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    ret, bianry = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

    # 腐蚀膨胀
    dilate = cv2.dilate(bianry, (3, 3), iterations=3)
    erode = cv2.erode(dilate, (3, 3), iterations=3)

    # 找轮廓,排序轮廓
    _, contours, hierarchy = cv2.findContours(erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    contours.sort(key=len, reverse=True)

    # minAreaRect获取轮廓的最小外接矩形,返回矩形的中心坐标,长宽,旋转角度
    # boxPoints解析最小外接矩形获取到最小外接矩形的四个顶点,注意四个顶点的排序规则
    # 使用排序后第一个最大轮廓contours[0]
    retangle = cv2.minAreaRect(contours[0])
    boxes = cv2.boxPoints(retangle)

    # 获取最小外接矩形的高和宽,不同发票的高和宽都不是一样的
    height = math.sqrt((boxes[1][0] - boxes[0][0]) ** 2 + (boxes[1][1] - boxes[0][1]) ** 2)
    weight = math.sqrt((boxes[3][0] - boxes[0][0]) ** 2 + (boxes[3][1] - boxes[0][1]) ** 2)

    angle = retangle[2]
    if(height > weight):
         angle = angle + 90

    #使用最小外接矩形的中心点,偏移角度获取变换矩阵
    M = cv2.getRotationMatrix2D(retangle[0], angle, 1)
    result = cv2.warpAffine(gray,M,(gray.shape[0],gray.shape[1]))

    cv2.drawContours(image, contours[0], -1, (0, 0, 255), 2)
    cv2.imshow("original image", image)
    cv2.imshow("binary image", erode)
    cv2.imshow("warp image", result)

if __name__=='__main__':
    image = cv2.imread("2.png",cv2.IMREAD_COLOR)
    adjust_invoice(image)
    cv2.waitKey(0)

 

原始图

倾斜矫正之后

最后把目标区域抠出来,成为单独的照片。

上面的算法可以很好的处理人民币和发票两种情况的倾斜矫正,那文本矫正可以吗?我赶紧试了一下,结果是失败的。

原图

算法矫正后,还是原样,矫正失败。

认真分析一下,还是很容易看出文本矫正失败的原因的。

原因就在于,人民币图像和发票图像他们有明显的的边界轮廓,而文本图像没有。文本图像的背景是白色的,所以我们没有办法像人民币发票那类有明显边界的矩形物体那样,提取出轮廓并旋转矫正。

经过深入分析可以看出,虽然文本类图像没有明显的边缘轮廓,但是他们有一个很重要的特征,那就是每一行文字都是呈现一条直线形状,而且这些直线都是平行的!

对于这种情况,我想到了另一种方法:基于直线探测的矫正算法

首先介绍一下我的算法思路:

  1. 用霍夫线变换探测出图像中的所有直线
  2. 计算出每条直线的倾斜角,求他们的平均值
  3. 根据倾斜角旋转矫正
  4. 最后根据文本尺寸裁剪图片

然后给出OpenCV的实现算法:


#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;

#define ERROR 1234

//度数转换
double DegreeTrans(double theta)
{
	double res = theta / CV_PI * 180;
	return res;
}


//逆时针旋转图像degree角度(原尺寸)    
void rotateImage(Mat src, Mat& img_rotate, double degree)
{
	//旋转中心为图像中心    
	Point2f center;
	center.x = float(src.cols / 2.0);
	center.y = float(src.rows / 2.0);
	int length = 0;
	length = sqrt(src.cols*src.cols + src.rows*src.rows);
	//计算二维旋转的仿射变换矩阵  
	Mat M = getRotationMatrix2D(center, degree, 1);
	warpAffine(src, img_rotate, M, Size(length, length), 1, 0, Scalar(255,255,255));//仿射变换,背景色填充为白色  
}

//通过霍夫变换计算角度
double CalcDegree(const Mat &srcImage, Mat &dst)
{
	Mat midImage, dstImage;

	Canny(srcImage, midImage, 50, 200, 3);
	cvtColor(midImage, dstImage, CV_GRAY2BGR);

	//通过霍夫变换检测直线
	vector<Vec2f> lines;
	HoughLines(midImage, lines, 1, CV_PI / 180, 300, 0, 0);//第5个参数就是阈值,阈值越大,检测精度越高
	//cout << lines.size() << endl;

	//由于图像不同,阈值不好设定,因为阈值设定过高导致无法检测直线,阈值过低直线太多,速度很慢
	//所以根据阈值由大到小设置了三个阈值,如果经过大量试验后,可以固定一个适合的阈值。

	if (!lines.size())
	{
		HoughLines(midImage, lines, 1, CV_PI / 180, 200, 0, 0);
	}
	//cout << lines.size() << endl;

	if (!lines.size())
	{
		HoughLines(midImage, lines, 1, CV_PI / 180, 150, 0, 0);
	}
	//cout << lines.size() << endl;
	if (!lines.size())
	{
		cout << "没有检测到直线!" << endl;
		return ERROR;
	}

	float sum = 0;
	//依次画出每条线段
	for (size_t i = 0; i < lines.size(); i++)
	{
		float rho = lines[i][0];
		float theta = lines[i][1];
		Point pt1, pt2;
		//cout << theta << endl;
		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));
		//只选角度最小的作为旋转角度
		sum += theta;

		line(dstImage, pt1, pt2, Scalar(55, 100, 195), 1, LINE_AA); //Scalar函数用于调节线段颜色

		imshow("直线探测效果图", dstImage);
	}
	float average = sum / lines.size(); //对所有角度求平均,这样做旋转效果会更好

	cout << "average theta:" << average << endl;

	double angle = DegreeTrans(average) - 90;

	rotateImage(dstImage, dst, angle);
	//imshow("直线探测效果图2", dstImage);
	return angle;
}


void ImageRecify(const char* pInFileName, const char* pOutFileName)
{
	double degree;
	Mat src = imread(pInFileName);
	imshow("原始图", src);
	Mat dst;
	//倾斜角度矫正
	degree = CalcDegree(src,dst);
	if (degree == ERROR)
	{
		cout << "矫正失败!" << endl;
		return;
	}
	rotateImage(src, dst, degree);
	cout << "angle:" << degree << endl;
	imshow("旋转调整后", dst);

	Mat resulyImage = dst(Rect(0, 0, dst.cols, 500)); //根据先验知识,估计好文本的长宽,再裁剪下来
	imshow("裁剪之后", resulyImage);
	imwrite("recified.jpg", resulyImage); 
}


int main()
{
	ImageRecify("correct2.jpg", "FinalImage.jpg");
	waitKey();
	return 0;
}

python opencv 版本

#度数转换
def degreeTransform(theta):
    res = theta / np.pi * 180
    return res


def adjust_text(image):

    copy = image.copy()

    canny = cv2.Canny(image,50,200,3)

    lines = cv2.HoughLines(canny,1,np.pi/180,220)
    print(lines[0])
    print(lines.shape)
    # for x1, y1, x2, y2 in lines[0]:
    #     cv2.line(image, (x1, y1), (x2, y2), (0, 255, 0), 100)

    sum = 0.0
    for i in range(0, len(lines)):
        rho, theta = lines[i][0][0], lines[i][0][1]

        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a * rho
        y0 = b * rho
        x1 = int(x0 + 1000 * (-b))
        y1 = int(y0 + 1000 * (a))
        x2 = int(x0 - 1000 * (-b))
        y2 = int(y0 - 1000 * (a))

        sum += theta

        cv2.line(image, (x1, y1), (x2, y2), (0, 0, 255), 1)

    average = sum/len(lines)

    print("average: {}".format(average))

    angle = degreeTransform(average)
    print("angle: {}".format(angle))
    angle = angle-90


    M = cv2.getRotationMatrix2D((image.shape[0]/2,image.shape[1]/2),angle,1)
    result = cv2.warpAffine(copy,M,(image.shape[1],image.shape[0]))
    cv2.imshow("warp image", result)


    cv2.imshow("canny",canny)
    cv2.imshow("lines",image)




if __name__=='__main__':
    image = cv2.imread("3.png",cv2.IMREAD_COLOR)
    adjust_text(image)
    cv2.waitKey(0)

 

看看效果。这是原始图

直线探测的效果。

矫正之后的效果。

我们发现矫正之后的图像有较多留白,影响观看,所以需要进一步裁剪,保留文字区域。

赶紧再试多一张。

原始图

直线探测

矫正效果

进一步裁剪

可以看出,基于直线探测的矫正算法在文本处理上效果真的很不错!

最后总结一下两个算法的应用场景:

  • 基于轮廓提取的矫正算法更适用于车牌、身份证、人民币、书本、发票一类矩形形状而且边界明显的物体矫正。

  • 基于直线探测的矫正算法更适用于文本类的矫正。

  • 5
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我可以回答您的问题。图像矫正是一种重要的图像处理技术,它可以将图像旋转、平移、缩放等操作,使其更加清晰、易于分析和处理。下面是利用 OpenCV 和 Python 快速实现图像矫正的步骤: 1. 导入必要的库 ``` import cv2 import numpy as np ``` 2. 读取图像并转换为灰度图像 ``` img = cv2.imread('your_image_path.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ``` 3. 检测图像中的边缘 ``` edges = cv2.Canny(gray, 50, 150, apertureSize=3) ``` 4. 检测图像中的直线 ``` lines = cv2.HoughLines(edges, 1, np.pi/180, 200) ``` 5. 对检测到的直线进行排序,以便找到最长的两条直线 ``` lines = sorted(lines, key=lambda x: x[0][0], reverse=False) line1 = lines[0] line2 = lines[-1] ``` 6. 计算直线的交点,以确定旋转角度 ``` rho1, theta1 = line1[0] rho2, theta2 = line2[0] a = np.cos(theta1) b = np.sin(theta1) x0 = a * rho1 y0 = b * rho1 c = np.cos(theta2) d = np.sin(theta2) x1 = c * rho2 y1 = d * rho2 intersection = np.linalg.solve([[a, b], [c, d]], [x0, y1]) angle = np.arctan2(y1 - intersection[1], x1 - intersection[0]) angle = angle * 180 / np.pi ``` 7. 旋转图像并进行矫正 ``` (h, w) = img.shape[:2] center = (w // 2, h // 2) M = cv2.getRotationMatrix2D(center, angle, 1.0) rotated = cv2.warpAffine(img, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) ``` 这样就可以快速实现图像矫正了。希望我的回答能够帮到您!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值