Android集成OpenCV实现图片梯形校正

前言

SDK下载地址

Releases - OpenCV

这里使用4.8.1版本

OpenCV4.8.1

在 Android 中使用 OpenCV 主要涉及 集成 OpenCV 库加载原生库调用图像处理接口 三大步骤。

下载 OpenCV

  • 官网地址:https://opencv.org/releases/
  • 下载 OpenCV for Android(如 opencv-4.8.1-android-sdk.zip
  • 解压后得到 OpenCV-android-sdk 文件夹

注意:

不要直接使用 GitHub 上的源码,要用官方发布的 SDK 包。

集成方式

这是最稳定、兼容性最好的方式。

集成代码

复制 OpenCV SDK 到项目

  • OpenCV-android-sdk/sdk/java 文件夹重命名为 opencv
  • 放入你的 Android 项目根目录下(与 app 同级)
  • 删除javadoc文件夹
  • src下创建main/java文件夹,把org文件夹复制进来
  • resAndroidManifest.xml移动到src/main

创建一个类

1
2
3
4
5
package org.opencv;

public class BuildConfig {
public final static boolean DEBUG = true;
}

修改 opencv/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
plugins {
id 'com.android.library'
}

android {
namespace = 'org.opencv'
compileSdk = 36 // 与主项目一致

defaultConfig {
minSdk = 31
targetSdk = 36

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

在主项目 settings.gradle.kts 中包含 opencv 模块

1
include(":opencv")

app/build.gradle.kts 中添加依赖

1
2
3
dependencies {
implementation(project(":opencv"))
}

集成原生库

查看架构

注意

原生库不同的 CPU 架构有不同的 ABI,我们不要全都复制,按照实际的情况复制对应架构的就行。

查看架构

1
adb shell getprop ro.product.cpu.abi

输出可能是:

  • arm64-v8a → 你需要提供 arm64-v8a/libopencv_java4.so
  • x86_64 → 模拟器,需要 x86_64/libopencv_java4.so

确保你的设备 CPU 架构与 .so 文件匹配。

常见的 ABI 有:

  • arm64-v8a(主流 64 位 ARM)
  • armeabi-v7a(32 位 ARM,兼容性好)
  • x86 / x86_64(模拟器)

原生库ABI选择原则

要考虑实际的运行环境:

如果只在现代设置上运行只选arm64-v8a 就行了。

如果要兼容早期的手机再添加上armeabi-v7a

如果要在模拟器上运行再添加上x86_64,现在基本上都是64位的模拟器了。

复制原生库

复制OpenCV库

  • OpenCV-android-sdk/sdk/native/libs 文件夹内需要的ABI复制出来
  • 复制到 app/src/main/jniLibs/(若无此目录则新建)
  • 最终路径:app/src/main/jniLibs/arm64-v8a/libopencv_java4.so

复制OpenCV依赖库

OpenCV 4.8.1 是使用 Android NDK 编译的,依赖libc++_shared.so,而 libc++_shared.so 并不是 OpenCV 自带的,而是 NDK 提供的标准 C++ 共享库

因此,你需要从你本地安装的 Android NDK 目录中获取它。

复制libc++_shared.so文件

查找NDK位置

File => Settings => Language & Frameworks => Android SDK

里面可以看到SDK的路径找到NDK下的

D:\Tools\AndroidSDK\ndk\25.1.8937393\toolchains\llvm\prebuilt\windows-x86_64\sysroot\usr\lib

如图

image-20260108184408590

设置ABI过滤(非必要)

ABI(Application Binary Interface,应用二进制接口)定义了应用程序与系统之间在底层(如寄存器使用、调用约定、数据对齐等)如何交互。

不同的 CPU 架构有不同的 ABI,例如:

  • armeabi-v7a:32 位 ARM 架构(较旧的 Android 设备)
  • arm64-v8a:64 位 ARM 架构(现代主流 Android 设备)
  • x86:32 位 Intel/AMD 架构(部分模拟器或老旧设备)
  • x86_64:64 位 Intel/AMD 架构(部分模拟器或 Chromebook)

abiFilters 的作用

当你使用 NDK(Native Development Kit) 编写 C/C++ 代码并编译成 .so 动态库时,这些库必须为特定的 ABI 编译。

abiFilters 告诉 Gradle 只打包指定 ABI 的原生库到最终 APK 或 AAB 中

1
2
3
4
5
6
7
android {
defaultConfig {
ndk {
abiFilters += listOf("arm64-v8a", "x86_64") // 或 "armeabi-v7a"
}
}
}

设置原生库路径(非必要)

注意这个配置不是必要的

如果把 .so 文件放在 默认路径 src/main/jniLibs/ 下,不需要额外配置 sourceSets,Android Gradle Plugin(AGP)会自动识别并打包这些 native 库。

1
2
3
4
5
6
7
android {
sourceSets {
named("main") {
jniLibs.setSrcDirs(listOf("src/main/jniLibs"))
}
}
}

验证是否生效

你可以通过以下方式确认 .so 是否被打包进 APK:

  • 构建 APK 后,用 Android StudioBuild > Analyze APK… 打开它;
  • 查看 lib/arm64-v8a/ 等目录下是否有 libopencv_java4.so

项目结构示意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MyApp/
├── app/
│ └── src/main/
│ ├── java/... (你的代码)
│ └── jniLibs/
│ ├── arm64-v8a/
│ │ └── libopencv_java4.so
│ └── armeabi-v7a/
│ └── libopencv_java4.so
└── opencv/
├── build.gradle
└── src/main/
├── res/
├── AndroidManifest.xml
└── java/org/opencv/...

初始化 OpenCV

初始化 OpenCV(必须)

OpenCV 需要加载原生库才能使用。不能直接调用 Mat 等类

推荐方式:使用 OpenCVLoader.initDebug()

1
2
3
4
5
6
7
8
9
10
11
class MainActivity : ComponentActivity() {
val TAG = "MainActivity"

init {
if (!OpenCVLoader.initDebug()) {
Log.e("OpenCV", "OpenCV init failed");
} else {
Log.d("OpenCV", "OpenCV loaded successfully");
}
}
}

注意:

  • initDebug() 适用于开发;发布时建议用 initAsync() 避免 ANR。
  • 如果你只在特定功能中使用 OpenCV,可在该 Activity 或 Fragment 中初始化。

示例

示例:从相册选图 → 转灰度 → 显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 从资源或文件加载 Bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test_image);

// 2. 转为 Mat
Mat mat = new Mat();
Utils.bitmapToMat(bitmap, mat);

// 3. 转灰度
Mat grayMat = new Mat();
Imgproc.cvtColor(mat, grayMat, Imgproc.COLOR_RGBA2GRAY);

// 4. 转回 Bitmap 显示
Bitmap resultBitmap = Bitmap.createBitmap(grayMat.cols(), grayMat.rows(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(grayMat, resultBitmap);

// 5. 显示到 ImageView
ImageView imageView = findViewById(R.id.imageView);
imageView.setImageBitmap(resultBitmap);

🔧 常用模块:

  • Imgproc:图像处理(滤波、边缘、变换等)
  • Core:矩阵运算
  • Imgcodecs:读写图像文件(注意:Android 中通常用 Bitmap 代替)
  • Features2d:特征点检测(SIFT、ORB 等)

常见问题解决

问题 解决方案
UnsatisfiedLinkError 确保 jniLibs 中有对应 ABI 的 .so 文件
图像方向错误 Android Bitmap 是 RGBA,OpenCV 默认 BGR,注意颜色空间转换
内存泄漏 使用完 Mat 后调用 mat.release()
APK 体积过大 参考上一问:裁剪 ABI 或使用精简版 OpenCV

进阶建议

  1. 异步处理:图像操作放在 AsyncTaskCoroutine 中,避免卡 UI。
  2. 内存优化:复用 Mat 对象,及时 release()
  3. 自动检测文档四角:结合 Canny + findContours + approxPolyDP 实现扫描仪效果。
  4. 替代方案:如果只需简单处理(缩放、旋转),可考虑纯 Java/Kotlin 实现,避免引入 OpenCV。

图形校正

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import android.graphics.Bitmap
import android.util.Log
import org.opencv.android.Utils
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import androidx.core.graphics.createBitmap
import com.google.gson.Gson

object ZOpenCVUtil {

/**
* 对梯形歪斜的 Bitmap 进行透视校正
*
* @param srcBitmap 原始图像
* @param srcPoints 图像中四边形的四个角点(按顺序:左上、右上、右下、左下)
* 每个 Point(x, y) 对应像素坐标
* @return 校正后的 Bitmap,若失败返回 null
*/
fun correctPerspective(
srcBitmap: Bitmap,
srcPoints: List<Point>, // 必须是 4 个点
): Bitmap {
if (srcPoints.size != 4) {
throw IllegalArgumentException("必须提供恰好 4 个角点")
}

// 将 Bitmap 转为 Mat
val srcMat = Mat()
Utils.bitmapToMat(srcBitmap, srcMat)

val outputWidth = srcBitmap.width
val outputHeight = srcBitmap.height

// 目标矩形的四个角点(正面视角)
val dstPoints = listOf(
Point(0.0, 0.0), // 左上
Point(outputWidth.toDouble(), 0.0), // 右上
Point(outputWidth.toDouble(), outputHeight.toDouble()), // 右下
Point(0.0, outputHeight.toDouble()) // 左下
)

// 转换为 MatOfPoint2f
val srcMatOfPoints = MatOfPoint2f(*srcPoints.toTypedArray())
val dstMatOfPoints = MatOfPoint2f(*dstPoints.toTypedArray())

// 计算透视变换矩阵
val perspectiveTransform = Imgproc.getPerspectiveTransform(srcMatOfPoints, dstMatOfPoints)

// 应用透视变换
val dstMat = Mat()
Imgproc.warpPerspective(
srcMat,
dstMat,
perspectiveTransform,
Size(outputWidth.toDouble(), outputHeight.toDouble()),
Imgproc.INTER_CUBIC // 高质量插值
)

// 转回 Bitmap
val resultBitmap = createBitmap(outputWidth, outputHeight)
Utils.matToBitmap(dstMat, resultBitmap)

// 释放资源
srcMat.release()
dstMat.release()
srcMatOfPoints.release()
dstMatOfPoints.release()
perspectiveTransform.release()

return resultBitmap
}

/**
* 自动检测图像中 Document(如 A4 纸)并自动进行透视校正
* 顶部大底部小校正
*/
fun scanDocumentByAngle(
srcBitmap: Bitmap,
angle: Int = 24,
): Bitmap {
val srcPoints = mutableListOf<Point>()
val pc = 1.0 * angle / 180 * srcBitmap.width
srcPoints.add(Point(0.0, 0.0))
srcPoints.add(Point(srcBitmap.width.toDouble(), 0.0))
srcPoints.add(Point(srcBitmap.width.toDouble() - pc, srcBitmap.height.toDouble()))
srcPoints.add(Point(pc, srcBitmap.height.toDouble()))
return correctPerspective(srcBitmap, srcPoints)
}

/**
* 自动检测图像中最大的四边形区域(如文档),并进行透视校正
*
* @param srcBitmap 输入的原始 Bitmap
* @return 校正后的 Bitmap,若未找到四边形则返回 源图像
*/
fun scanDocumentAuto(
srcBitmap: Bitmap,
): Bitmap {
val srcMat = Mat()
Utils.bitmapToMat(srcBitmap, srcMat)
// 1. 转为灰度图
val grayMat = Mat()
Imgproc.cvtColor(srcMat, grayMat, Imgproc.COLOR_BGR2GRAY)

// 2. 高斯模糊(降噪)
val blurred = Mat()
Imgproc.GaussianBlur(grayMat, blurred, Size(5.0, 5.0), 0.0)

// 3. Canny 边缘检测
val edges = Mat()
Imgproc.Canny(blurred, edges, 50.0, 150.0)

// 4. 膨胀(连接边缘断点)
val kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(3.0, 3.0))
val dilated = Mat()
Imgproc.dilate(edges, dilated, kernel)

// 5. 查找轮廓
val contours = mutableListOf<MatOfPoint>()
val hierarchy = Mat()
Imgproc.findContours(dilated, contours, hierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE)

// 6. 按面积降序排序,找最大四边形
var maxQuad: MatOfPoint? = null
var maxArea = 0.0

for (contour in contours) {
// 计算近似多边形
val perimeter = Imgproc.arcLength(MatOfPoint2f(*contour.toArray()), true)
val approxCurve = MatOfPoint2f()
Imgproc.approxPolyDP(MatOfPoint2f(*contour.toArray()), approxCurve, 0.02 * perimeter, true)

// 必须是四边形 且 面积足够大
if (approxCurve.total() == 4L) {
val area = Imgproc.contourArea(contour)
if (area > maxArea && area > srcMat.rows() * srcMat.cols() * 0.1) { // 至少占图10%
maxArea = area
// 转回 MatOfPoint 用于后续处理
maxQuad = MatOfPoint(*approxCurve.toArray().map { Point(it.x, it.y) }.toTypedArray())
}
}
}

if (maxQuad == null) {
// 未找到有效四边形
releaseMat(srcMat, grayMat, blurred, edges, dilated, hierarchy)

Log.i("OpenCV", "未找到有效四边形")
return srcBitmap
}

// 7. 对四边形角点排序(左上、右上、右下、左下)
val quadPoints = sortCorners(maxQuad.toList())

Log.i("OpenCV", Gson().toJson(quadPoints))

// 8. 透视校正
val corrected = correctPerspective(
srcBitmap,
quadPoints,
)


// 10. 释放资源
releaseMat(srcMat, grayMat, blurred, edges, dilated, hierarchy)

return corrected
}

/**
* 释放多个 Mat 资源
*/
private fun releaseMat(vararg mats: Mat?) {
mats.forEach { it?.release() }
}

/**
* 对四边形的四个角点进行排序:[左上, 右上, 右下, 左下]
*/
private fun sortCorners(points: List<Point>): List<Point> {
// 按 x+y 排序:最小的是左上,最大的是右下
// 按 x-y 排序:最小的是右上,最大的是左下
val sortedBySum = points.sortedBy { it.x + it.y }
val sortedByDiff = points.sortedBy { it.x - it.y }

val topLeft = sortedBySum.first()
val bottomRight = sortedBySum.last()
val topRight = sortedByDiff.first()
val bottomLeft = sortedByDiff.last()

return listOf(topLeft, topRight, bottomRight, bottomLeft)
}
}