移动端扫描APP解决方案(一)——使用传统图像处理方法提取与矫正拍照文档边缘

一、概述

1.拍照扫描应用

在日常办公中,使用手机拍照扫描文件是一种高效、环保的方式。通过手机扫描,可以将纸质文件转化为电子版,不仅减少了纸张的使用,还节省了时间和精力。扫描后的电子版文件可以方便地储存和管理,不必再担心文件的丢失或损坏。通过使用手机拍照扫描的功能,可以轻松地快速生成高质量的电子版文件,从而提高工作效率并确保文件的安全性。
现在很多手机都内置了拍照扫描并提取出文字的功能,也有很多做得很成功的APP,博主之前也参与开发过类似的APP,这些手机扫描工具都具有简单易用的界面和强大的功能,可以帮助使用者轻松完成文件扫描任务。

2.工程流程

一般的文档拍照扫描App的工作流程通常包括以下步骤:

  1. 访问图像: 访问手机里面的图像。

  2. 访问相机: 应用程序请求用户授权,以访问设备的相机功能。

  3. 图像处理: 拍摄的照片会经过图像处理算法,以优化图像质量,去除背景噪音,调整亮度和对比度等,确保文档清晰可读。

  4. 边缘检测: 应用程序可能会使用边缘检测算法识别文档的边缘,以便更好地裁剪文档。

  5. 文档裁剪: 应用程序根据边缘检测的结果或用户指定的区域,裁剪图像以去除非文档部分,只保留需要的内容。

  6. 图像校正: 一些应用程序可能会对图像进行校正,以确保文档呈正常的方向,例如自动旋转或矫正倾斜。

  7. 图像压缩: 为了减小文件大小,一些应用程序可能会对图像进行压缩,以便更轻松地分享或存储。

  8. 识别文本: 应用程序使用光学字符识别(OCR)技术来识别图像中的文本,并将其转换为可编辑的文本格式。

  9. 保存输出: 用户可以选择将扫描的文档保存到设备上的特定位置,或者分享给其他人,例如通过电子邮件、云存储服务或即时消息应用。

具体流程图如下:

在这里插入图片描述

二、传统图像边缘检测

拍照扫描其中最主要的一环是,文档边缘检测与文档矫正,如果没有边缘检测与矫正,那之后的所有步骤都很难实现。边缘检测是计算机视觉中一个非常古老的问题,它涉及到检测图像中的边缘来确定目标的边界,从而分离感兴趣的目标。

1、边缘检测常用算法

传统图像处理中的边缘检测方法主要依赖于一些经典的滤波器和运算技术,大概有以下几种:

  1. Sobel 操作: Sobel 操作是一种基于卷积的方法,通过在图像中应用Sobel算子(一种特殊的卷积核)来检测图像中的边缘。它在水平和垂直方向上分别进行卷积操作,然后将两个方向的结果合并。

  2. Canny 边缘检测: Canny 边缘检测是一种多阶段的算法,包括噪声抑制、梯度计算、非极大值抑制和边缘跟踪等步骤。Canny 边缘检测在检测边缘的同时,对边缘进行细化,产生较为准确的结果。

  3. Laplacian of Gaussian(LoG): LoG 算法首先对图像进行高斯平滑,然后应用拉普拉斯算子。这有助于检测图像中的变化,从而找到边缘。

  4. Prewitt 操作: 类似于Sobel,Prewitt 算子也是一种卷积操作,用于检测图像中的垂直和水平边缘。

  5. 边缘增强滤波器: 一些特定的滤波器,如Roberts、Frei-Chen等,也可以用于边缘检测。

2、Canny边缘检测算法

Canny边缘检测是一种经典且广泛使用的边缘检测技术,已经成为计算机视觉领域中的主流方法之一,该算法由John Canny于1986年提出,被设计为一种多步骤的过程,以提取图像中的边缘,并在边缘上产生单像素的线条。

2.1 输入灰度图像

Canny算子只能对单通道灰度图像进行处理,
图像灰度是指将彩色图像中的每个像素点的颜色信息转换为相应的灰度值的过程。在灰度图像中,每个像素只有一个通道,表示图像的亮度。灰度值通常在0到255之间,其中0代表黑色,255代表白色。

转换彩色图像为灰度图像的一种常见方法是使用加权平均法,通过将彩色通道的颜色值按照一定的权重进行加权求和。一个常见的加权平均公式是:

灰度值 = 0.299 * 红色通道+ 0.587 *绿色通道 + 0.114 *蓝色通道

这个公式是根据人眼对不同颜色的敏感度来确定的,红色、绿色和蓝色通道的权重分别为0.299、0.587和0.114。
在很多图像处理库中,包括OpenCV都提供了直接将彩色图像转换为灰度图像的函数。例如,在OpenCV中,可以使用cvtColor函数。

2.2 滤波降噪处理

理想的图像具有无噪声、高质量的特点,然而在实际应用中,由于采集设备、环境干扰等多种原因,图像通常包含大量的噪声,其中最常见的是椒盐噪声和高斯噪声。高斯滤波通过卷积图像和高斯核来实现,这有助于去除图像中的高频噪声,提高后续边缘检测的稳定性。
在实际应用中,调整高斯滤波参数和阈值的选择对Canny算法的性能起着关键作用。这使得Canny算子成为处理噪声且要求高精度边缘检测的场景中的常用工具。这里也可以尝试使用双边滤波等算法。
高斯滤波使用的高斯核是具有 x 和 y两个维度的高斯函数,且两个维度上标准差一般取相同,形式为:
在这里插入图片描述
高斯滤波,即使用即使用某一尺寸的二维高斯核与图像进行卷积。由于数字图像的数据形式为离散矩阵,高斯核是对连续高斯函数的离散近似,通过对高斯曲面进行离散采样和归一化得出。例如,尺寸为 3X3,标准差为 1 的高斯核为:
在这里插入图片描述
在确定高斯核后,将其与图像进行离散卷积即可。高斯滤波可以将图像中的噪声部分过滤出来,避免后面进行边缘检测时将错误的噪声信息也误识别为边缘了。

2.3 计算像素梯度

在Canny算子中,梯度计算是通过一阶有限差分来实现的,而常用的梯度算子之一是Sobel算子。Sobel算子对图像进行卷积操作,分别计算图像在x和y方向上的梯度,从而得到两个梯度矩阵。
在这里插入图片描述
其中,I为灰度图像矩阵,且此处的 * 表示互相关运算(卷积运算可视为将卷积核旋转180°后的互相关运算)。需要说明的是,图像矩阵坐标系原点在左上角,且 x正方向为从左到右, y 正方向为从上到下。
在这里插入图片描述
可以计算得到梯度矩阵Gxy。

2.4 非极大值像素梯度抑制

非极大值像素梯度抑制是Canny边缘检测算法中的一个重要步骤,其目的是消除边缘检测带来的杂散响应,使得检测到的边缘更加精细,起到将边缘“瘦身”的作用。
在这里插入图片描述
如果上图表示,C表示为当前非极大值抑制的点,g1-4为它的8连通邻域点,图中蓝色线段表示上一步计算得到的角度图像C点的值,即梯度方向,第一步先判断C灰度值在8值邻域内是否最大,如是则继续检查图中梯度方向交点dTmp1,dTmp2值是否大于C,如C点大于dTmp1,dTmp2点的灰度值,则认定C点为极大值点,置为1,因此最后生成的图像应为一副二值图像,边缘理想状态下都为单像素边缘。

在非极大值抑制的过程中,需要注意的是,梯度方向的交点并不一定精确地位于8领域内的8个点的位置。因此,在实际应用中,为了获取更准确的梯度值,通常会使用相邻两个点的双线性插值所形成的灰度值。

具体而言,对于当前像素位置(x, y)以及垂直向上的梯度方向,找到梯度方向上的两个相邻点,例如(x, y-1)和(x, y+1)。然后,通过双线性插值计算这两个点之间的灰度值,考虑到相邻像素的权重,以更精确地估计灰度值。

这种插值的过程确保了在梯度方向上获得了更为准确的灰度信息。最终,在非极大值抑制后,选择梯度方向上的最大值作为边缘点,有助于形成细致而准确的单像素边缘。这个过程在图像处理中经常使用,特别是在涉及到子像素级别的信息时,以提高边缘检测的精度。在Canny算法中,这一步骤对于生成细致、准确的单像素边缘非常关键。

其中梯度方向均为垂直向上,经过非极大值抑制后取梯度方向上最大值为边缘点,形成细且准确的单像素边缘,如下图:
在这里插入图片描述
这个步骤的效果是保留具有局部最大梯度值的像素,从而实现了对边缘的细化,使得检测到的边缘更加准确。非极大值像素梯度抑制是Canny边缘检测算法中的关键步骤之一,有助于提高算法的精度和抗噪声能力。

2.5 滞后阈值

在Canny边缘检测的最后阶段,需要考虑哪些被检测到的边缘是真实的,哪些是虚假的。为了达到这个目的,引入了两个阈值,通常称为minVal(最小值)和maxVal(最大值)。

具体的处理流程如下:

  1. 对于每个像素,如果其梯度强度大于maxVal,则将其标记为强边缘,这些点被确定为真实的边缘。
  2. 对于梯度强度介于minValmaxVal之间的像素,如果与强边缘相连(通过连通性),则也被标记为边缘,因为它们可能是真实的边缘的一部分。
  3. 对于梯度强度低于minVal的像素,将其标记为非边缘,因为它们被认为是噪声或虚假的边缘。

这个阶段的目标是通过阈值的设定,准确地确定真实的边缘,同时通过考虑连通性,连接较弱的边缘部分,以防止由于噪声或图像细节引起的不连续边缘。

通过适当选择minValmaxVal的值,可以调整Canny算法的敏感性,以适应不同图像和应用场景的需求。这一阶段的调优通常需要根据具体情况进行实验和调整,以获得满意的边缘检测结果。
在这里插入图片描述

3.边缘代码实现

void getCanny(cv::Mat &gray, cv::Mat &canny)
{
	cv::Mat thres;
	double high_thres = threshold(gray, thres, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU), low_thres = high_thres * 0.5;
	cv::Canny(gray, canny, low_thres, high_thres);
}

在这里插入图片描述

在这里插入图片描述

三、传统方法直线检测

在获取文档边缘之后,要对文档的边缘做直线检测,霍夫变换是一种经典的线检测算法,它通过将图像中的点映射到参数空间中的线来实现。该算法具有检测任意方向的线段的能力,并且在处理具有大量噪声的图像时表现良好。

1. 霍夫空间和边缘点到霍夫空间的映射

霍夫空间是一个二维平面,其水平轴表示线的斜率(坡度),而垂直轴表示直线在边缘图像上的截距。在这个空间中,边缘图像上的一条线可以用 (y = ax + b) 的形式表示(Hough,1962年)。每一条直线在霍夫空间上对应于该直线的斜率 (a) 和截距 (b)。

具体而言,边缘图像上的一条线会在霍夫空间上产生一个点,因为该线的特征可以用斜率 (a) 和截距 (b) 来唯一确定。相反,边缘图像上的单个边缘点 ((xᵢ, yᵢ)) 可以通过无数条直线。因此,在霍夫空间中,这些边缘点以 (b = axᵢ + yᵢ) 的形式生成一条线(Leavers,1992)。

在霍夫变换算法中,霍夫空间的主要目的是确定边缘图像中是否存在直线。通过在霍夫空间中累积边缘图像上的点,可以找到在图像中表示共线的直线。这使得霍夫变换成为一种强大的线检测工具,可以应用于各种方向的直线检测,而不需要事先知道直线的方向。

在这里插入图片描述
使用 (y = ax + b) 形式的直线和带有斜率和截距的霍夫空间表示方式存在一个缺陷。在这种形式下,该算法无法有效检测垂直线,因为对于垂直线,斜率 (a) 是不确定的,可能是无穷大(Leavers,1992)。在编程中,这意味着计算机将需要无限量的存储空间来表示所有可能的斜率值。

为了避免这个问题,可以使用一种更适合垂直线的表示方式。一条直线可以由一条称为法线的线表示,该线穿过原点并垂直于该直线。法线的标准形式为 ρ = x cos( θ )+ y sin( θ ),其中ρ 是法线的长度,θ 是法线与 x 轴之间的角度。

这种表示方式的优势在于,对于所有可能的直线,都可以使用相对较小的参数空间来表示,从而减小了计算和存储的复杂性。通过使用法线表示,霍夫变换可以更全面地检测图像中的直线,包括垂直线。
在这里插入图片描述
通过使用霍夫空间的极坐标表示ρ和θ,而不再使用斜率 (a) 和截距 (b),我们可以更有效地表示直线,并解决在处理垂直线时斜率 (a) 的无限值问题。在这种表示下,水平轴表示 (\theta) 值,垂直轴表示ρ值。

在这种情况下,边缘点到霍夫空间的映射仍然遵循相似的原理,但现在边缘点 ((x, y)) 在霍夫空间中生成的是余弦曲线,而不再是直线(Leavers,1992)。具体而言,边缘点 ((x, y)) 对应于在霍夫空间中以ρ = x cos( θ )+ y sin( θ )形式表示的余弦曲线。

这种正弦曲线的表示方式通过消除了在处理垂直线时斜率 (a) 的无限值问题,使得霍夫变换更加稳健。这样,霍夫变换可以更全面地检测图像中的直线,并且通过在极坐标中表示,也更容易在计算中处理。
在这里插入图片描述
边缘点在霍夫空间中产生余弦曲线。如果将边缘图像中的所有边缘点映射到霍夫空间上,将会生成许多余弦曲线。当两个边缘点位于同一条直线上时,它们对应的余弦曲线将在特定的 (ρ,θ) 对上相互交叉。因此,霍夫变换算法通过找到在霍夫空间中交叉点数量大于某个阈值的 (ρ,θ) 对来检测直线。

需要注意的是,如果不对霍夫空间进行预处理,比如邻域抑制,以去除边缘图像中的相似线条,那么这种阈值化方法可能不会总是产生最佳结果。在一些情况下,可能会出现多个交叉点,使得检测到的直线数量较多。因此,根据具体应用,可能需要对霍夫空间进行进一步的处理,以提高算法的鲁棒性和准确性。这可以包括使用非极大值抑制或其他技术来优化霍夫变换的输出。

2. 算法实现

确定ρ 和θ 的范围是霍夫变换中的重要步骤。通常情况下,θ的范围是 [0, 180] 度,而ρ 的范围是 ([-d, d]),其中 (d) 是边缘图像对角线的长度。这种范围的量化意味着我们在霍夫空间中有了有限数量的可能值。

  1. 创建一个称为累加器的二维数组,表示霍夫空间的维度为(num_rhos,num_thetas)。初始化累加器中的所有值为零。

  2. 对原始图像执行边缘检测。可以使用选择的任何边缘检测算法来完成这一步。

  3. 对于边缘图像上的每个像素,检查该像素是否为边缘像素。如果是边缘像素,则循环遍历所有可能的ρ 值,计算对应的 θ,在累加器中找到 θ和 ρ的索引,并递增累加器中对应的值。

  4. 循环遍历累加器中的所有值。如果某个值大于某个阈值,则获取 ρ和 θ 的索引,从索引对中获取对应的 ρ和 θ的值。然后,可以将这些值转换回 (y = ax + b) 的形式。

3.代码实现

	cv::Mat gray, canny, edge;
	cvtColor(img_proc, gray, cv::COLOR_BGR2GRAY);

	get_canny(gray, edge);

	//二值化
	binaryImage(edge, canny);
	if (debug)
	{
		imshow("二值图像", canny);
	}

	//从边缘图像中提取线条
	std::vector<cv::Vec4i> lines;
	std::vector<Line> horizontals, verticals;

	///第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
	///第二个参数,InputArray类型的lines,经过调用HoughLinesP函数后后存储了检测到的线条的输出矢量,每一条线由具有四个元素的矢量(x_1, y_1, x_2, y_2)  表示,其中,(x_1, y_1)和(x_2, y_2) 是是每个检测到的线段的结束点。
	///第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。
	///第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
	///第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
	///第六个参数,double类型的minLineLength,有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。
	///第七个参数,double类型的maxLineGap,有默认值0,允许将同一行点与点之间连接起来的最大的距离。
	HoughLinesP(canny, lines, 1, CV_PI / 180, 100, 100, 20);

在这里插入图片描述

四、整体代码实现

#include "DocumentCorrection.h"

struct Line
{
	cv::Point _p1;
	cv::Point _p2;
	cv::Point _center;

	Line(cv::Point p1, cv::Point p2)
	{
		_p1 = p1;
		_p2 = p2;
		_center = cv::Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
	}
};

void get_canny(cv::Mat& gray, cv::Mat& canny)
{
	cv::Mat thres;
	double high_thres = threshold(gray, thres, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU), low_thres = high_thres * 0.5;
	cv::Canny(gray, canny, low_thres, high_thres);
}

bool cmp_y(const Line &p1, const Line &p2)
{
	return p1._center.y < p2._center.y;
}

bool cmp_x(const Line &p1, const Line &p2)
{
	return p1._center.x < p2._center.x;
}

cv::Point2f computeIntersect(Line &l1, Line &l2)
{
	int x1 = l1._p1.x, x2 = l1._p2.x, y1 = l1._p1.y, y2 = l1._p2.y;
	int x3 = l2._p1.x, x4 = l2._p2.x, y3 = l2._p1.y, y4 = l2._p2.y;
	if (float d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
	{
		cv::Point2f pt;
		pt.x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d;
		pt.y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d;
		return pt;
	}
	return cv::Point2f(-1, -1);
}

void binaryImage(cv::Mat &src, cv::Mat &dst)
{
	int maxVal = 205;
	int blockSize = 33;
	double C = 0;
	//adaptiveThreshold(src, dst, maxVal, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, blockSize, C);
	threshold(src, dst, 55, 255, cv::THRESH_BINARY);
}

void correction(cv::Mat &src, cv::Mat &dst, bool debug)
{
	cv::Mat img_proc = src.clone();
	int w_proc = src.size().width;
	int h_proc = src.size().height;
	cv::Mat img_dis = img_proc.clone();

	cv::Mat gray, canny, edge;
	cvtColor(img_proc, gray, cv::COLOR_BGR2GRAY);

	get_canny(gray, edge);

	//二值化
	binaryImage(edge, canny);
	if (debug)
	{
		imshow("二值图像", canny);
	}

	//从边缘图像中提取线条
	std::vector<cv::Vec4i> lines;
	std::vector<Line> horizontals, verticals;

	///第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
	///第二个参数,InputArray类型的lines,经过调用HoughLinesP函数后后存储了检测到的线条的输出矢量,每一条线由具有四个元素的矢量(x_1, y_1, x_2, y_2)  表示,其中,(x_1, y_1)和(x_2, y_2) 是是每个检测到的线段的结束点。
	///第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。
	///第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
	///第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
	///第六个参数,double类型的minLineLength,有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。
	///第七个参数,double类型的maxLineGap,有默认值0,允许将同一行点与点之间连接起来的最大的距离。
	HoughLinesP(canny, lines, 1, CV_PI / 180, 100, 100, 20);
	for (size_t i = 0; i < lines.size(); i++)
	{
		cv::Vec4i v = lines[i];
		double delta_x = v[0] - v[2];
		double delta_y = v[1] - v[3];
		Line l(cv::Point(v[0], v[1]), cv::Point(v[2], v[3]));
		//得到水平线
		if (fabs(delta_x) > fabs(delta_y))
		{
			horizontals.push_back(l);
		}
		//得到垂直线
		else
		{
			verticals.push_back(l);
		}
		// for visualization only
		if (debug)
		{
			line(img_proc, cv::Point(v[0], v[1]), cv::Point(v[2], v[3]), cv::Scalar(0, 0, 255), 1, cv::LINE_AA);
		}
	}
	
	if (debug)
	{
		cv::namedWindow("line", 0);
		imshow("line", img_proc);
	}
	
	// 检测不到全部边缘时的处理方式
	if (horizontals.size() < 2)
	{
		if (horizontals.size() == 0 || horizontals[0]._center.y > h_proc / 2)
		{
			horizontals.push_back(Line(cv::Point(0, 0), cv::Point(w_proc - 1, 0)));
		}
		if (horizontals.size() == 0 || horizontals[0]._center.y <= h_proc / 2)
		{
			horizontals.push_back(Line(cv::Point(0, h_proc - 1), cv::Point(w_proc - 1, h_proc - 1)));
		}
	}
	if (verticals.size() < 2)
	{
		if (verticals.size() == 0 || verticals[0]._center.x > w_proc / 2)
		{
			verticals.push_back(Line(cv::Point(0, 0), cv::Point(0, h_proc - 1)));
		}
		if (verticals.size() == 0 || verticals[0]._center.x <= w_proc / 2)
		{
			verticals.push_back(Line(cv::Point(w_proc - 1, 0), cv::Point(w_proc - 1, h_proc - 1)));
		}
	}

	// 按线的中心坐标排序
	std::sort(horizontals.begin(), horizontals.end(), cmp_y);//垂直
	sort(verticals.begin(), verticals.end(), cmp_x);//水平
	//画边缘的四条线
	if (debug)
	{
		line(img_proc, horizontals[0]._p1, horizontals[0]._p2, cv::Scalar(0, 255, 0), 2, cv::LINE_AA);
		line(img_proc, horizontals[horizontals.size() - 1]._p1, horizontals[horizontals.size() - 1]._p2, cv::Scalar(0, 255, 0), 2, cv::LINE_AA);
		line(img_proc, verticals[0]._p1, verticals[0]._p2, cv::Scalar(255, 0, 0), 2, cv::LINE_AA);
		line(img_proc, verticals[verticals.size() - 1]._p1, verticals[verticals.size() - 1]._p2, cv::Scalar(255, 0, 0), 2, cv::LINE_AA);
		
		cv::namedWindow("检测线", 0);
		cv::imshow("检测线", img_proc);
	}

	/*透视变换*/

	//设置校正后的图像大小
	int w_o = src.size().width;
	int h_o = src.size().height;
	
	dst = cv::Mat::zeros(h_o, w_o, CV_8UC3);

	std::vector<cv::Point2f> dst_pts, img_pts;
	dst_pts.push_back(cv::Point(0, 0));
	dst_pts.push_back(cv::Point(w_o - 1, 0));
	dst_pts.push_back(cv::Point(0, h_o - 1));
	dst_pts.push_back(cv::Point(w_o - 1, h_o - 1));

	//原图对应的检测到的交叉点
	img_pts.push_back(computeIntersect(horizontals[0], verticals[0]));
	img_pts.push_back(computeIntersect(horizontals[0], verticals[verticals.size() - 1]));
	img_pts.push_back(computeIntersect(horizontals[horizontals.size() - 1], verticals[0]));
	img_pts.push_back(computeIntersect(horizontals[horizontals.size() - 1], verticals[verticals.size() - 1]));

	// 转换为原图的比例
	for (size_t i = 0; i < img_pts.size(); i++)
	{
		if (debug)
		{
			circle(img_proc, img_pts[i], 10, cv::Scalar(255, 255, 0), 3);
			cv::namedWindow("校正点", 0);
			cv::imshow("校正点", img_proc);
		}
	
	}

	//得到变换矩阵
	cv::Mat transmtx = getPerspectiveTransform(img_pts, dst_pts);

	//视角转换
	warpPerspective(src, dst, transmtx, dst.size());

	if (debug)
	{
		cv::namedWindow("dst", 0);
		cv::imshow("dst", dst);
		cv::waitKey();
	}
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/137903.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在Vue.js中,什么是mixins?它们的作用是什么?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

QML14、Qt之Q_PROPERTY宏理解

在初学Qt的过程中,时不时地要通过F2快捷键来查看QT类的定义,发现类定义中有许多Q_PROPERTY的东西,比如最常用的QWidget的类定义: Qt中的Q_PROPERTY宏在Qt中是很常用的,那么它有什么作用呢? Qt提供了一个绝妙的属性系统,Q_PROPERTY()是一个宏,用来在一个类中声明一个属…

谈谈前端如何防止数据泄露

shigen日更文章的博客写手&#xff0c;擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长&#xff0c;分享认知&#xff0c;留住感动。 最近突然发现了一个好玩的事情&#xff0c;部分网站进去的时候几乎都是死的&#xff0c;那种死是区别于我们常见…

【Hadoop】MapReduce详解

&#x1f984; 个人主页——&#x1f390;开着拖拉机回家_大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f…

数据结构 | 栈的实现

数据结构 | 栈的实现 文章目录 数据结构 | 栈的实现栈的概念及结构栈的实现 需要实现的函数初始化栈入栈出栈获取栈顶元素获取栈中有效元素个数检测栈是否为空销毁栈Stack.c 栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素…

AI的尽头是解决屎山代码

众所周知&#xff0c;Copilot 被认为是比 ChatGPT 更深谙程序员心思的工具。在今天凌晨的 GitHub Universe 2023 大会上&#xff0c;GitHub 公布了 Copilot 的最新消息&#xff0c;这一神器旨在解放程序员的双手&#xff0c;AI 将彻底改变开发者的编程方式。 在本次盛会上&…

数据结构:并查集(概念,代码实现,并查操作优化)

目录 1.表示集合关系2.并查集的代码实现1.基本操作&#xff1a;查2.基本操作&#xff1a;并 3.并查集的优化1.并&#xff08;Union&#xff09;操作的优化2.Find操作的优化&#xff08;压缩路径) 1.表示集合关系 用互不相交的树&#xff0c;表示多个集合。 ①查&#xff1a;查找…

AI应用新时代的起点,亚马逊云科技加速大模型应用

大语言模型 何为大语言模型&#xff0c;可以一句话概括&#xff1a;深度学习是机器学习的分支&#xff0c;大语言模型是深度学习的分支。 机器学习是人工智能&#xff08;AI&#xff09;的一个分支领域&#xff0c;核心是让计算机系统从数据中学习以提高性能。与直接编程不同…

【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; 进程程序替换 1. 前言2. exec…

【仿真动画】双机器人协作完成一个任务(切割)

场景 动画 两个机器人协同工作完成一个任务需要解决以下几个关键问题&#xff1a; 通信&#xff1a;两个机器人需要能够相互通信&#xff0c;以共享信息&#xff0c;例如位置、姿态、状态等。规划&#xff1a;需要对两个机器人的运动轨迹进行规划&#xff0c;确保两个机器人不会…

RESTful API概述以及如何使用它构建 web 应用程序

REST&#xff08;Representational State Transfer&#xff09;是一种设计风格和架构原则&#xff0c;它是一种为 Web 应用程序提供简化和标准化的 API 的方式。RESTful API&#xff08;RESTful Web Services&#xff09;是符合 REST 架构风格的网络应用程序 API&#xff0c;它…

未来之路:大模型技术在自动驾驶的应用与影响

本文深入分析了大模型技术在自动驾驶领域的应用和影响&#xff0c;万字长文&#xff0c;慢慢观看~ 文中首先概述了大模型技术的发展历程&#xff0c;自动驾驶模型的迭代路径&#xff0c;以及大模型在自动驾驶行业中的作用。接着&#xff0c;详细介绍了大模型的基本定义、基础功…

关系查询处理和查询优化

关系数据库系统的查询处理 4 个阶段 查询分析查询检查【此时的完整性检查是初步的、静态的检查】查询优化【分为代数优化、物理优化】查询执行 关系数据库系统的查询优化 查询优化的优点不仅在于用户不必考虑如何最好地表达查询以获得较高地效率&#xff0c;而且在于系统可…

Springboot项目部署及多环境开发

一、项目部署 我们之前写的代码都是部署在本地的tomcat上&#xff0c;别人是无法访问我们写的程序的。在实际开发中&#xff0c;我们都要将开发完毕的项目部署到公司的服务器上。 我们的代码需要经过编译打包生成一个jar包&#xff0c;这个过程需要借助一个插件来实现。 创建sp…

2024最新基于物联网单片机毕业设计选题汇总(合集)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

初始MySQL(四)(查询加强练习,多表查询)

目录 查询加强 where加强 order by加强 group by 分页查询 总结 多表查询(重点) 笛卡尔集及其过滤 自连接 子查询 子查询当作临时表 all/any 多列子查询 #先创建三张表 #第一张表 CREATE TABLE dept(deptno MEDIUMINT NOT NULL DEFAULT 0,dname VARCHAR(20) NOT …

2023-11-13 LeetCode每日一题(区域和检索 - 数组可修改)

2023-11-13每日一题 一、题目编号 307. 区域和检索 - 数组可修改二、题目链接 点击跳转到题目位置 三、题目描述 给你一个数组 nums &#xff0c;请你完成两类查询。 其中一类查询要求 更新 数组 nums 下标对应的值另一类查询要求返回数组 nums 中索引 left 和索引 right…

Oracle主备切换,ogg恢复方法(经典模式)

前言: 文章主要介绍Oracle数据库物理ADG主备在发生切换时(switchover,failover)&#xff0c;在主库、备库运行的ogg进程(经典模式)如何进行恢复。 测试恢复场景: 1 主备发生switchover切换&#xff0c;主库为ogg源端 2 主备发生switchover切换&#xff0c;备库为ogg源端 3 主备…

【Linux】Linux动态库和静态库

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;Linux &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【Linux】…

AIOT数字孪生智慧工地一体化管理平台源码

智慧工地app基于物联网和移动互联网技术&#xff0c;利用各类传感器及终端设备通过与云端服务器的实时数据交互&#xff0c;为施工现场的管理人员提供环境监测、劳务实名制管理、物料管理、巡检记录、设备管理等一系列优质高效的行业解决方案。 一、智能工地应用价值 智慧工地…
最新文章