目录
一、简介
如果你看过本人的上一篇教程,那么你肯定知道如何在Android项目中集成OpenCV库。没看过的兄弟姐妹们可以回去看一下,这里将不再赘述。这篇文章主要讲述的是如何利用OpenCV库去实现一个简单的图片矫正功能,其中的矫正主要指的是对照片中的内容进行透视变换。说的简单一点就是你对一张纸拍了一张照片,然后你拍摄的角度可能是不确定的,也就是照片中纸的角度不一定是正向的也有可能是斜着的。如下图所示,左边图片中的纸是正向的,而右边图片中的纸是斜着的,同一张纸拍摄的角度不同。矫正就是需要将右图中的纸矫正到看起来和左图中纸的角度一样,话不多说。
二、核心方法
这篇文章只会介绍项目中关于透视变换的核心方法,如果大家对其它代码感兴趣的话可以在附件中下载完整的项目。以下是项目中最最核心的一个方法,第一个参数sourceBitmap表示的是你需要矫正的原图,第二个参数cornerPoints是图片中需要矫正的内容四周的角点(图片中物体的边缘坐标),角点的取值顺序为[左上->右上->右下->左下]。
private Bitmap manualDocumentCorrection(Bitmap sourceBitmap, Point[] cornerPoints)
这里还是拿上面纸的图片举例子,纸四周的四个角就是四个角点。这里值得注意的是需要矫正的内容一定要在角点构成的四边形内,在后面会使用到这里获取的角点去计算透视变换后图像的长和宽。在代码中角点是需要手动去指定,而且顺序一定要是[左上->右上->右下->左下]。实际上在OpenCV中提供了自动检测图像边缘的方法,只不过识别效果不是很好,所以手动指定会比较精准一些,手动指定角点的逻辑不会放在此篇文章中大家如果想了解具体的实现可以下载附件中完整的源代码。
三、具体实现
以下是manualDocumentCorrection方法的具体实现,每一行代码作者都加上了注释方便大家理解,流程大概是这样:
1.将Android中的Bitmap转换成OpenCV中的Mat对象
2.校验用户选择的角点,不能为空而且一定要是四个,下图是日志中打印的角点坐标
3.根据角点计算文档的宽高,宽和高都是取最大值
4.进行透视变换,将变换后的Mat对象转回Bitmap
//将Bitmap转换为Mat,确保正确的颜色通道顺序
Mat sourceMat = new Mat();
//把输入的 Bitmap 拷贝成 ARGB_8888 格式,确保每像素32位,便于 OpenCV 操作
Bitmap bmp32 = sourceBitmap.copy(Bitmap.Config.ARGB_8888, true);
//类型转换 将Bitmap类型转换成OpenCV的Mat类型
Utils.bitmapToMat(bmp32, sourceMat);
// 处理RGBA到BGR的转换 OpenCV中默认使用BGR,而Android是RGBA,这一步把颜色格式转换成OpenCV标准的BGR
Imgproc.cvtColor(sourceMat, sourceMat, Imgproc.COLOR_RGBA2BGR);
// 确保角点不为空且有效 校验角点一定要是四个而且不能为空
if (cornerPoints == null || cornerPoints.length != 4) {
Log.e(TAG, "角点无效,无法进行校正");
return null;
}
// 使用用户选择的角点
Point[] userSelectedCorners = cornerPoints;
// 调试日志,输出选择的角点坐标
// for (int i = 0; i < userSelectedCorners.length; i++) {
// Log.d(TAG, "角点" + i + ": x=" + userSelectedCorners[i].x + ", y=" + userSelectedCorners[i].y);
// }
// 计算文档的宽度和高度 用于拉直
double topWidth = Math.sqrt(Math.pow(userSelectedCorners[1].x - userSelectedCorners[0].x, 2) +
Math.pow(userSelectedCorners[1].y - userSelectedCorners[0].y, 2));
double bottomWidth = Math.sqrt(Math.pow(userSelectedCorners[2].x - userSelectedCorners[3].x, 2) +
Math.pow(userSelectedCorners[2].y - userSelectedCorners[3].y, 2));
//计算上边和下边的长度,取最大值作为“文档宽度” 这里的文档指的是最后变换后文档的图片
double finalWidth = Math.max(topWidth, bottomWidth);
//这里也是一样
double leftHeight = Math.sqrt(Math.pow(userSelectedCorners[3].x - userSelectedCorners[0].x, 2) +
Math.pow(userSelectedCorners[3].y - userSelectedCorners[0].y, 2));
double rightHeight = Math.sqrt(Math.pow(userSelectedCorners[2].x - userSelectedCorners[1].x, 2) +
Math.pow(userSelectedCorners[2].y - userSelectedCorners[1].y, 2));
//计算左右边的长度,取最大值作为“文档高度”
double finalHeight = Math.max(leftHeight, rightHeight);
// 检查计算出的宽度和高度是否有效 判断文档的尺寸是否太小了 如果太小可能是选错点了
if (finalWidth <= 10 || finalHeight <= 10) {
Log.e(TAG, "计算出的文档尺寸过小,无法进行校正: " + finalWidth + "x" + finalHeight);
return null;
}
// 使用固定的纵横比例,避免奇怪的形变 如果宽高比例不合理(例如太扁或太长),自动调整为 A4 纸比例。
double aspectRatio = finalWidth / finalHeight;
if (aspectRatio < 0.5 || aspectRatio > 2.0) {
Log.w(TAG, "纵横比例异常 (" + aspectRatio + "),使用标准A4比例");
aspectRatio = 210.0 / 297.0; // A4纸比例
finalHeight = finalWidth / aspectRatio;
}
// 设置合理的分辨率 为了防止生成太大的图像,占用内存,这里设置了最大宽高(2000px)
int maxSize = 2000; // 限制最大尺寸
if (finalWidth > maxSize) {
finalWidth = maxSize;
finalHeight = finalWidth / aspectRatio;
}
if (finalHeight > maxSize) {
finalHeight = maxSize;
finalWidth = finalHeight * aspectRatio;
}
// 显示识别后的宽高
Log.i(TAG, "手动选择的文档尺寸: " + (int)finalWidth + "x" + (int)finalHeight);
// 创建源点和目标点 srcPoints:用户选择的原图中的四个角点 dstPoints:变换后的矩形目标区域
MatOfPoint2f srcPoints = new MatOfPoint2f(
userSelectedCorners[0], // 左上
userSelectedCorners[1], // 右上
userSelectedCorners[2], // 右下
userSelectedCorners[3] // 左下
);
//获取透视变换矩阵 计算从 srcPoints 到 dstPoints 的透视变换矩阵
MatOfPoint2f dstPoints = new MatOfPoint2f(
new Point(0, 0), // 左上
new Point(finalWidth - 1, 0), // 右上
new Point(finalWidth - 1, finalHeight - 1), // 右下
new Point(0, finalHeight - 1) // 左下
);
// 获取透视变换矩阵 计算从 srcPoints 到 dstPoints 的透视变换矩阵
Mat perspectiveTransform = Imgproc.getPerspectiveTransform(srcPoints, dstPoints);
// 检查变换矩阵是否有效
if (perspectiveTransform.empty()) {
Log.e(TAG, "透视变换矩阵计算失败");
return null;
}
// 应用透视变换
Mat warpedMat = new Mat((int)finalHeight, (int)finalWidth, CvType.CV_8UC3);
//用变换矩阵将文档拉直
Imgproc.warpPerspective(sourceMat, warpedMat, perspectiveTransform, warpedMat.size());
// 检查变换结果是否有效
if (warpedMat.empty() || warpedMat.cols() == 0 || warpedMat.rows() == 0) {
Log.e(TAG, "透视变换失败,结果为空");
return null;
}
// 应用增强处理 - 开始增强过程 这里代码我去掉了 自己需要的话可以实现一手
// 转换回Bitmap
Bitmap resultBitmap = Bitmap.createBitmap((int)finalWidth, (int)finalHeight, Bitmap.Config.ARGB_8888);
// 转回RGBA格式
Imgproc.cvtColor(warpedMat, warpedMat, Imgproc.COLOR_BGR2RGBA);
Utils.matToBitmap(warpedMat, resultBitmap);
// 释放Mat资源
sourceMat.release();
warpedMat.release();
perspectiveTransform.release();
//返回矫正后的bitmap
return resultBitmap;