OpenCV(Open Source Computer Vision Library)是计算机视觉领域最广泛使用的开源库,提供了 2500+ 个优化算法,覆盖图像处理、特征检测、对象识别、深度学习和相机标定等全领域。在 Android 平台上,OpenCV 通过 NDK(Native C++)和 Java SDK 两种方式集成。本文从 Android 端 SDK 配置、Mat 核心数据结构、图像读写与颜色空间转换、滤波与边缘检测、ORB 特征检测与匹配、CascadeClassifier 与 DNN 目标检测、CameraBridgeViewBase 实时相机处理等角度系统讲解 OpenCV 在 Android 上的应用,并以完整的文档扫描仪作为实战示例收尾。
一、OpenCV Android SDK 环境配置
1.1 SDK 获取
OpenCV 官方为 Android 平台提供了预编译的 SDK,包含 Java 绑定和 Native 库:
OpenCV-android-sdk/ ├── sdk/ │ ├── java/ # Java 层 API(opencv-java.jar) │ ├── native/ │ │ ├── libs/ # 各 ABI 的 .so 文件 │ │ │ ├── arm64-v8a/libopencv_java4.so │ │ │ ├── armeabi-v7a/libopencv_java4.so │ │ │ └── x86_64/libopencv_java4.so │ │ └── jni/ # JNI 头文件 │ └── etc/ └── samples/ # 官方示例
|
1.2 Gradle 集成
将 SDK 的 java 目录作为模块导入项目:
settings.gradle:
include ':opencv' project(':opencv').projectDir = new File('path/to/OpenCV-android-sdk/sdk')
|
app/build.gradle:
dependencies { implementation project(':opencv') }
|
或者直接将 .so 文件和 .jar 放入项目中:
android { ... sourceSets { main { jniLibs.srcDirs = ['src/main/jniLibs'] } } }
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) }
|
1.3 OpenCVLoader —— 加载 Native 库
OpenCV 的 Java 层依赖 Native .so 库。在 Application 或首个 Activity 中初始化:
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); if (!OpenCVLoader.initDebug()) { Log.e("OpenCV", "OpenCV initialization failed!"); } else { Log.d("OpenCV", "OpenCV initialized successfully, version: " + Core.getVersionString()); } } }
|
initDebug() 适用于开发阶段(会输出详细日志),发布时使用 OpenCVLoader.initAsync() 异步加载以避免主线程阻塞。
1.4 使用回调方式的异步初始化
public class MainActivity extends AppCompatActivity implements BaseLoaderCallback { private BaseLoaderCallback mLoaderCallback;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mLoaderCallback = new BaseLoaderCallback(this) { @Override public void onManagerConnected(int status) { if (status == SUCCESS) { Log.i("OpenCV", "OpenCV loaded successfully"); onOpenCVReady(); } else { super.onManagerConnected(status); } } }; }
@Override protected void onResume() { super.onResume(); if (!OpenCVLoader.initDebug()) { OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, this, mLoaderCallback); } else { mLoaderCallback.onManagerConnected(BaseLoaderCallback.SUCCESS); } }
private void onOpenCVReady() { } }
|
二、Mat —— 核心图像数据结构
2.1 Mat 的本质
Mat(Matrix)是 OpenCV 中的核心类,用于存储图像和多维数组。它由两部分组成:
- 头部(Header):包含尺寸(rows, cols)、通道数(channels)、数据类型(depth/type)、步长(step)等元信息。
- 数据指针(Data pointer):指向实际像素/矩阵数据的指针。
Mat 采用引用计数机制,因此复制头部是廉价的,只有数据被共享:
Mat src = new Mat(480, 640, CvType.CV_8UC3); Mat copy = src.clone(); Mat view = src; Mat roi = src.submat(new Rect(0, 0, 320, 240));
|
在 C++ 层(NDK)中:
cv::Mat src(480, 640, CV_8UC3); cv::Mat copy = src.clone(); cv::Mat view = src; cv::Mat roi = src(cv::Rect(0, 0, 320, 240));
|
2.2 理解 CV_8UC3 —— 类型常量
OpenCV 的类型常量格式为 CV_<depth><type>C<channels>:
| 常量 |
depth |
位深 |
含义 |
CV_8U |
0 |
8 |
uint8(0-255) |
CV_8S |
1 |
8 |
int8(-128-127) |
CV_16U |
2 |
16 |
uint16 |
CV_16S |
3 |
16 |
int16 |
CV_32S |
4 |
32 |
int32 |
CV_32F |
5 |
32 |
float32 |
CV_64F |
6 |
64 |
float64 |
常见组合:
CV_8UC1:单通道灰度图(值为 0-255)
CV_8UC3:三通道 BGR 彩色图(每个通道 0-255)
CV_8UC4:四通道 BGRA 图(带 Alpha)
CV_32FC1:单通道 float32(用于某些算法的中间结果,如距离变换)
cv::Mat gray(480, 640, CV_8UC1); cv::Mat color(480, 640, CV_8UC3, cv::Scalar(255, 0, 0)); cv::Mat float_img(480, 640, CV_32FC3);
|
2.3 Mat 的创建与访问
cv::Mat img = cv::Mat::zeros(480, 640, CV_8UC3); cv::Mat img = cv::Mat::ones(480, 640, CV_32FC1); cv::Mat img = cv::Mat::eye(3, 3, CV_32FC1);
uchar data[] = {0, 0, 255, 0, 255, 0, 255, 0, 0}; cv::Mat img(1, 3, CV_8UC3, data);
cv::Vec3b& pixel = img.at<cv::Vec3b>(row, col); uchar blue = pixel[0]; uchar green = pixel[1]; uchar red = pixel[2]; pixel = cv::Vec3b(255, 0, 0);
uchar gray_val = img.at<uchar>(row, col);
float val = float_img.at<float>(row, col);
for (int r = 0; r < img.rows; r++) { cv::Vec3b* row_ptr = img.ptr<cv::Vec3b>(r); for (int c = 0; c < img.cols; c++) { row_ptr[c] = cv::Vec3b(255, 255, 255); } }
for (int r = 0; r < img.rows; r++) { uchar* p = img.ptr<uchar>(r); for (int c = 0; c < img.cols; c++) { p[c * img.channels() + 0] = 255; p[c * img.channels() + 1] = 128; p[c * img.channels() + 2] = 64; } }
img.rows; img.cols; img.channels(); img.type(); img.depth(); img.step; img.elemSize(); img.total();
|
2.4 ROI(Region of Interest)
ROI 允许在图像上指定一个矩形区域进行操作,该操作共享原始数据,无需额外分配内存:
cv::Rect roi_rect(100, 50, 200, 150); cv::Mat roi = src(roi_rect);
cv::GaussianBlur(roi, roi, cv::Size(5, 5), 1.5); cv::rectangle(src, roi_rect, cv::Scalar(0, 255, 0), 2);
cv::Mat roi_copy = roi.clone(); roi_copy.copyTo(src(cv::Rect(400, 50, 200, 150)));
cv::Mat mask = cv::Mat::zeros(src.size(), CV_8UC1); cv::circle(mask, cv::Point(300, 200), 120, cv::Scalar(255), -1); cv::Mat masked_img; src.copyTo(masked_img, mask);
|
三、图像读写与颜色空间转换
3.1 imread 与 imwrite
cv::Mat img = cv::imread("/sdcard/photo.jpg", cv::IMREAD_COLOR);
if (img.empty()) { LOGE("Failed to load image!"); return; }
std::vector<int> compression_params; compression_params.push_back(cv::IMWRITE_JPEG_QUALITY); compression_params.push_back(95); cv::imwrite("/sdcard/output.jpg", img, compression_params);
compression_params.clear(); compression_params.push_back(cv::IMWRITE_PNG_COMPRESSION); compression_params.push_back(3); cv::imwrite("/sdcard/output.png", img, compression_params);
|
3.2 cvtColor —— 颜色空间转换
cv::Mat gray; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
cv::Mat rgb; cv::cvtColor(src, rgb, cv::COLOR_BGR2RGB);
cv::Mat hsv; cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);
cv::Mat yuv; cv::cvtColor(src, yuv, cv::COLOR_BGR2YCrCb);
cv::Mat lab; cv::cvtColor(src, lab, cv::COLOR_BGR2Lab);
cv::Mat yuv_mat(height * 3/2, width, CV_8UC1, yuv_data); cv::Mat bgr; cv::cvtColor(yuv_mat, bgr, cv::COLOR_YUV2BGR_NV21);
cv::Mat bgra; cv::cvtColor(src, bgra, cv::COLOR_BGR2BGRA);
|
常用颜色空间转换码速查:
| 转换码 |
含义 |
COLOR_BGR2GRAY |
BGR → 灰度(Weighted: 0.114B + 0.587G + 0.299R) |
COLOR_BGR2HSV |
BGR → HSV(H: 0-180, S: 0-255, V: 0-255) |
COLOR_BGR2Lab |
BGR → Lab |
COLOR_GRAY2BGR |
灰度 → BGR(三通道相同值) |
COLOR_YUV2BGR_NV21 |
Android Camera NV21 → BGR |
COLOR_YUV2RGBA_NV21 |
Android Camera NV21 → RGBA |
COLOR_RGBA2mRGBA |
RGBA → mRGBA (Android Bitmap 格式) |
四、图像滤波与平滑
4.1 线性滤波
cv::Mat blur_img; cv::blur(src, blur_img, cv::Size(5, 5));
cv::Mat box_img; cv::boxFilter(src, box_img, -1, cv::Size(5, 5), true);
cv::Mat gauss_img; cv::GaussianBlur(src, gauss_img, cv::Size(5, 5), 0);
cv::GaussianBlur(src, gauss_img, cv::Size(0, 0), 3.0);
cv::Mat grad_x, grad_y; cv::Sobel(gray, grad_x, CV_16S, 1, 0, 3); cv::Sobel(gray, grad_y, CV_16S, 0, 1, 3);
cv::Mat abs_grad_x, abs_grad_y; cv::convertScaleAbs(grad_x, abs_grad_x); cv::convertScaleAbs(grad_y, abs_grad_y);
cv::Mat sobel; cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, sobel);
cv::Mat laplacian; cv::Laplacian(gray, laplacian, CV_16S, 3); cv::convertScaleAbs(laplacian, laplacian);
|
4.2 非线性滤波
cv::Mat median_img; cv::medianBlur(src, median_img, 5);
cv::Mat bilateral_img; cv::bilateralFilter(src, bilateral_img, 9, 75, 75);
cv::Mat eroded, dilated, opened, closed; cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::erode(binary, eroded, kernel); cv::dilate(binary, dilated, kernel); cv::morphologyEx(binary, opened, cv::MORPH_OPEN, kernel); cv::morphologyEx(binary, closed, cv::MORPH_CLOSE, kernel); cv::morphologyEx(binary, gradient, cv::MORPH_GRADIENT, kernel);
|
4.3 边缘检测 —— Canny
Canny 边缘检测是多级算法,综合了高斯模糊、梯度计算、非极大值抑制和双阈值连接:
cv::Mat gray, edges; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
double low_threshold = 50; double high_threshold = 150; int kernel_size = 3; cv::Canny(gray, edges, low_threshold, high_threshold, kernel_size);
|
4.4 常用滤波效果对比
cv::Mat adaptive; cv::adaptiveThreshold(gray, adaptive, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 11, 2);
cv::Mat otsu; double thresh = cv::threshold(gray, otsu, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
|
五、特征检测与描述 —— ORB
ORB(Oriented FAST and Rotated BRIEF)是一种高效的特征检测和描述算法,结合了 FAST 角点检测和 BRIEF 描述符,并增加了旋转不变性。它是 SIFT 的免专利替代方案,非常适合移动端。
5.1 ORB 原理简述
- FAST 角点检测:如果一个像素与周围 16 个像素中有 N 个连续像素的强度差异超过阈值,则该像素是角点。ORB 中默认 N=9(FAST-9)。
- Harris 度量:对 FAST 检测到的角点用 Harris 角点响应排序,选取最好的 N 个。
- 方向分配:通过图像矩(intensity centroid)计算每个关键点的方向,实现旋转不变性。
- rBRIEF 描述符:在关键点周围的 31x31 邻域内,按特定模式采样 256 对像素,比较亮度生成 256 位(32 字节)二进制描述符。
5.2 detectAndCompute
#include <opencv2/features2d.hpp>
cv::Ptr<cv::ORB> orb = cv::ORB::create( 500, 1.2f, 8, 31, 0, 2, cv::ORB::HARRIS_SCORE, 31, 20 );
std::vector<cv::KeyPoint> keypoints; cv::Mat descriptors; orb->detectAndCompute(gray, cv::noArray(), keypoints, descriptors);
cv::Mat img_keypoints; cv::drawKeypoints(src, keypoints, img_keypoints, cv::Scalar(0, 255, 0), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
|
5.3 KeyPoint 的属性
struct KeyPoint { Point2f pt; float size; float angle; float response; int octave; int class_id; };
|
六、特征匹配
6.1 Brute-Force 匹配器
#include <opencv2/features2d.hpp>
std::vector<cv::KeyPoint> kp1, kp2; cv::Mat desc1, desc2; orb->detectAndCompute(img1, cv::noArray(), kp1, desc1); orb->detectAndCompute(img2, cv::noArray(), kp2, desc2);
cv::BFMatcher matcher(cv::NORM_HAMMING);
const int k = 2; std::vector<std::vector<cv::DMatch>> knn_matches; matcher.knnMatch(desc1, desc2, knn_matches, k);
const float ratio_thresh = 0.75f; std::vector<cv::DMatch> good_matches; for (size_t i = 0; i < knn_matches.size(); i++) { if (knn_matches[i][0].distance < ratio_thresh * knn_matches[i][1].distance) { good_matches.push_back(knn_matches[i][0]); } }
cv::Mat img_matches; cv::drawMatches(img1, kp1, img2, kp2, good_matches, img_matches, cv::Scalar::all(-1), cv::Scalar::all(-1), std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
|
6.2 RANSAC 与单应性矩阵
当匹配点涉及平面场景(如文档、广告牌、画作)时,可以用 RANSAC 计算单应性矩阵(H),过滤外点:
std::vector<cv::Point2f> points1, points2; for (const auto& match : good_matches) { points1.push_back(kp1[match.queryIdx].pt); points2.push_back(kp2[match.trainIdx].pt); }
if (points1.size() >= 4) { cv::Mat H = cv::findHomography(points1, points2, cv::RANSAC, 3.0); std::vector<uchar> mask; cv::Mat H_with_mask = cv::findHomography(points1, points2, cv::RANSAC, 3.0, mask); int inliers = cv::countNonZero(mask); LOGD("RANSAC inliers: %d / %zu", inliers, points1.size()); if (inliers > 20) { std::vector<cv::Point2f> corners1 = { {0, 0}, {(float)img1.cols, 0}, {(float)img1.cols, (float)img1.rows}, {0, (float)img1.rows} }; std::vector<cv::Point2f> corners2; cv::perspectiveTransform(corners1, corners2, H); cv::polylines(img2, corners2, true, cv::Scalar(0, 255, 0), 3); } }
|
6.3 FLANN 匹配器(适合大规模匹配)
cv::FlannBasedMatcher flann_matcher( cv::makePtr<cv::flann::LshIndexParams>(12, 20, 2) ); std::vector<std::vector<cv::DMatch>> knn_matches; flann_matcher.knnMatch(desc1, desc2, knn_matches, 2);
|
七、目标检测
7.1 CascadeClassifier —— Haar / LBP 级联检测
经典的人脸检测方法,使用预训练的级联分类器:
#include <opencv2/objdetect.hpp>
cv::CascadeClassifier face_cascade;
if (!face_cascade.load("/data/data/com.example.app/files/haarcascade_frontalface_default.xml")) { LOGE("Failed to load cascade classifier!"); return; }
cv::Mat gray; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::equalizeHist(gray, gray);
std::vector<cv::Rect> faces; face_cascade.detectMultiScale(gray, faces, 1.1, 3, 0, cv::Size(30, 30), cv::Size() );
for (const auto& face : faces) { cv::rectangle(src, face, cv::Scalar(0, 255, 0), 2); cv::Mat face_roi = gray(face); std::vector<cv::Rect> eyes; eye_cascade.detectMultiScale(face_roi, eyes, 1.1, 3, 0, cv::Size(10, 10)); for (const auto& eye : eyes) { cv::Point center(face.x + eye.x + eye.width / 2, face.y + eye.y + eye.height / 2); int radius = cvRound((eye.width + eye.height) * 0.25); cv::circle(src, center, radius, cv::Scalar(255, 0, 0), 2); } }
|
7.2 DNN 模块 —— 深度学习目标检测
OpenCV 的 DNN 模块支持加载主流深度学习框架的模型(TensorFlow、Caffe、ONNX、DarkNet 等)。在 Android 上有显著的性能优势(尤其是使用 OpenCL 加速时):
#include <opencv2/dnn.hpp>
cv::dnn::Net net = cv::dnn::readNetFromCaffe( "/sdcard/MobileNetSSD_deploy.prototxt", "/sdcard/MobileNetSSD_deploy.caffemodel" );
net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV); net.setPreferableTarget(cv::dnn::DNN_TARGET_OPENCL);
cv::Mat blob = cv::dnn::blobFromImage( src, 1.0, cv::Size(300, 300), cv::Scalar(127.5, 127.5, 127.5), true, false ); net.setInput(blob);
cv::Mat detections = net.forward();
for (int i = 0; i < detections.size[2]; i++) { float confidence = detections.ptr<float>(0, 0)[i * 7 + 2]; if (confidence > 0.5) { int class_id = static_cast<int>(detections.ptr<float>(0, 0)[i * 7 + 1]); int left = static_cast<int>(detections.ptr<float>(0, 0)[i * 7 + 3] * src.cols); int top = static_cast<int>(detections.ptr<float>(0, 0)[i * 7 + 4] * src.rows); int right = static_cast<int>(detections.ptr<float>(0, 0)[i * 7 + 5] * src.cols); int bottom = static_cast<int>(detections.ptr<float>(0, 0)[i * 7 + 6] * src.rows); cv::Rect box(left, top, right - left, bottom - top); cv::rectangle(src, box, cv::Scalar(0, 255, 0), 2); cv::putText(src, class_names[class_id] + ": " + std::to_string(confidence), cv::Point(left, top - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1); } }
|
7.3 模型类型与 load 方法
| 模型格式 |
方法 |
| Caffe |
readNetFromCaffe(prototxt, caffemodel) |
| TensorFlow |
readNetFromTensorflow(pb, pbtxt) |
| ONNX |
readNetFromONNX(onnx_file) |
| DarkNet (YOLO) |
readNetFromDarknet(cfg, weights) |
| Torch |
readNetFromTorch(t7_file) |
7.4 DNN 后端与目标选择
在 Android 上,OpenCV DNN 支持的后端:
| 后端 |
常量 |
说明 |
| Default |
DNN_BACKEND_OPENCV |
OpenCV 内置实现 |
| OpenCL |
DNN_BACKEND_OPENCV |
需设备支持 OpenCL |
| Vulkan |
DNN_BACKEND_VULKAN |
Vulkan Compute(Android 7.0+) |
| Halide |
DNN_BACKEND_HALIDE |
Halide 语言后端 |
net.setPreferableBackend(cv::dnn::DNN_BACKEND_VULKAN); net.setPreferableTarget(cv::dnn::DNN_TARGET_VULKAN);
|
八、CameraBridgeViewBase —— 实时相机处理
8.1 JavaCameraView vs NativeCameraView
OpenCV Android SDK 提供了两种相机视图:
- JavaCameraView:基于 Android Camera API(已废弃,但兼容性好),通过 Java 层的
Camera 获取帧,回调到 onCameraFrame。
- NativeCameraView:基于 Camera2 API(Android 5.0+),帧更稳定。
两者都继承自 CameraBridgeViewBase,使用方式一致。
8.2 实时图像处理示例
public class CameraActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener2 {
private CameraBridgeViewBase mCameraView; private Mat mRgba, mGray, mCanny; private CascadeClassifier mFaceDetector;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_camera);
mCameraView = findViewById(R.id.camera_view); mCameraView.setVisibility(SurfaceView.VISIBLE); mCameraView.setCvCameraViewListener(this);
mCameraView.setMaxFrameSize(640, 480); mCameraView.enableFpsMeter(); }
@Override public void onCameraViewStarted(int width, int height) { mRgba = new Mat(); mGray = new Mat(); mCanny = new Mat();
try { InputStream is = getResources().openRawResource(R.raw.haarcascade_frontalface); File cascadeDir = getDir("cascade", Context.MODE_PRIVATE); File cascadeFile = new File(cascadeDir, "haarcascade_frontalface.xml"); FileOutputStream os = new FileOutputStream(cascadeFile); byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); } is.close(); os.close();
mFaceDetector = new CascadeClassifier(cascadeFile.getAbsolutePath()); cascadeFile.delete(); cascadeDir.delete(); } catch (IOException e) { Log.e("CameraActivity", "Failed to load cascade", e); } }
@Override public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) { mRgba = inputFrame.rgba(); mGray = inputFrame.gray();
MatOfRect faces = new MatOfRect(); if (mFaceDetector != null) { mFaceDetector.detectMultiScale(mGray, faces, 1.1, 3, 0, new Size(80, 80), new Size()); } for (Rect face : faces.toArray()) { Imgproc.rectangle(mRgba, face.tl(), face.br(), new Scalar(0, 255, 0), 3); }
Imgproc.Canny(mGray, mCanny, 80, 200);
return mRgba; }
@Override public void onCameraViewStopped() { if (mRgba != null) mRgba.release(); if (mGray != null) mGray.release(); if (mCanny != null) mCanny.release(); }
@Override protected void onPause() { super.onPause(); if (mCameraView != null) mCameraView.disableView(); }
@Override protected void onResume() { super.onResume(); if (!OpenCVLoader.initDebug()) { OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, this, mLoaderCallback); } else { mCameraView.enableView(); } } }
|
8.3 layout XML
<org.opencv.android.JavaCameraView android:id="@+id/camera_view" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" app:show_fps="true" app:camera_id="back" />
|
九、完整实战:文档扫描仪
文档扫描仪是一个经典的 OpenCV + Android 项目,流程包括:检测文档边缘、透视变换矫正、图像增强。以下是完整的 C++ (NDK) 实现。
9.1 文档边缘检测
cv::Mat detect_document_edges(const cv::Mat& src) { cv::Mat gray, blurred, edges; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0); cv::Canny(blurred, edges, 50, 150); cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3)); cv::dilate(edges, edges, kernel, cv::Point(-1, -1), 2); cv::erode(edges, edges, kernel, cv::Point(-1, -1), 1); std::vector<std::vector<cv::Point>> contours; std::vector<cv::Vec4i> hierarchy; cv::findContours(edges, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); std::vector<cv::Point> doc_contour; double max_area = 0; for (const auto& contour : contours) { double area = cv::contourArea(contour); if (area < src.rows * src.cols * 0.1) continue; std::vector<cv::Point> approx; double peri = cv::arcLength(contour, true); cv::approxPolyDP(contour, approx, 0.02 * peri, true); if (approx.size() == 4 && cv::isContourConvex(approx) && area > max_area) { max_area = area; doc_contour = approx; } } if (doc_contour.empty()) return src; std::sort(doc_contour.begin(), doc_contour.end(), [](const cv::Point& a, const cv::Point& b) { return a.y < b.y || (a.y == b.y && a.x < b.x); }); std::vector<cv::Point> sorted(4); if (doc_contour[0].x < doc_contour[1].x) { sorted[0] = doc_contour[0]; sorted[1] = doc_contour[1]; } else { sorted[0] = doc_contour[1]; sorted[1] = doc_contour[0]; } if (doc_contour[2].x < doc_contour[3].x) { sorted[3] = doc_contour[2]; sorted[2] = doc_contour[3]; } else { sorted[3] = doc_contour[3]; sorted[2] = doc_contour[2]; } cv::Mat result = src.clone(); for (int i = 0; i < 4; i++) { cv::line(result, sorted[i], sorted[(i+1)%4], cv::Scalar(0, 255, 0), 3); cv::circle(result, sorted[i], 8, cv::Scalar(0, 0, 255), -1); } return result; }
|
9.2 透视变换矫正
cv::Mat four_point_transform(const cv::Mat& src, const std::vector<cv::Point>& corners) { double width_top = cv::norm(corners[1] - corners[0]); double width_bottom = cv::norm(corners[2] - corners[3]); int max_width = std::max((int)width_top, (int)width_bottom); double height_left = cv::norm(corners[3] - corners[0]); double height_right = cv::norm(corners[2] - corners[1]); int max_height = std::max((int)height_left, (int)height_right); std::vector<cv::Point2f> dst_pts = { {0, 0}, {(float)(max_width - 1), 0}, {(float)(max_width - 1), (float)(max_height - 1)}, {0, (float)(max_height - 1)} }; std::vector<cv::Point2f> src_pts; for (const auto& pt : corners) { src_pts.push_back(cv::Point2f(pt.x, pt.y)); } cv::Mat M = cv::getPerspectiveTransform(src_pts, dst_pts); cv::Mat warped; cv::warpPerspective(src, warped, M, cv::Size(max_width, max_height)); return warped; }
|
9.3 图像增强(使扫描件清晰)
cv::Mat enhance_scanned_document(const cv::Mat& src) { cv::Mat gray, enhanced; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::adaptiveThreshold(gray, enhanced, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 11, 5); cv::medianBlur(enhanced, enhanced, 3); return enhanced; }
cv::Mat scan_document(const cv::Mat& input) { std::vector<cv::Point> corners = detect_and_sort_corners(input); if (corners.size() != 4) { LOGW("Document not detected, using full image"); return enhance_scanned_document(input); } cv::Mat warped = four_point_transform(input, corners); cv::Mat enhanced = enhance_scanned_document(warped); return enhanced; }
|
十、性能优化与注意事项
10.1 减少 Mat 分配
每帧处理避免频繁创建和销毁 Mat:
class FrameProcessor { cv::Mat gray_, blurred_, edges_, temp_; public: FrameProcessor(int w, int h) { gray_ = cv::Mat(h, w, CV_8UC1); blurred_ = cv::Mat(h, w, CV_8UC1); edges_ = cv::Mat(h, w, CV_8UC1); temp_ = cv::Mat(h, w, CV_8UC1); } void process(const cv::Mat& rgba) { cv::cvtColor(rgba, gray_, cv::COLOR_RGBA2GRAY); cv::GaussianBlur(gray_, blurred_, cv::Size(5, 5), 0); cv::Canny(blurred_, edges_, 50, 150); } };
|
10.2 使用多线程
cv::setNumThreads(4); int num_threads = cv::getNumThreads();
|
10.3 图像尺寸缩放
处理大图时先缩小,提高速度:
cv::Mat resized; double scale = 640.0 / src.cols; cv::resize(src, resized, cv::Size(), scale, scale, cv::INTER_AREA);
|
10.4 OpenCL 加速
OpenCV 的一些算法(DNN、滤波等)可以通过 OpenCL 加速:
if (cv::ocl::haveOpenCL()) { cv::ocl::setUseOpenCL(true); LOGD("OpenCL enabled: %s", cv::ocl::useOpenCL() ? "yes" : "no"); }
|
十一、总结
OpenCV 在 Android 上的集成路线清晰:
- SDK 集成:通过 Gradle module 导入 OpenCV Android SDK,用
OpenCVLoader 加载 Native 库。
- Mat 是核心:深度、通道、类型、ROI 的机制必须烂熟于心。
- 图像处理:
cvtColor 颜色空间转换、GaussianBlur / medianBlur / bilateralFilter 滤波、Canny 边缘检测是日常操作。
- 特征检测与匹配:ORB(
cv::ORB::create → detectAndCompute)+ BFMatcher + Lowe’s ratio test 是移动端特征匹配的标准组合。
- 目标检测:CascadeClassifier 做 Haar/LBP 人脸检测,DNN 模块跑深度学习模型(MobileNet-SSD/YOLO 等)。
- 实时相机:
CameraBridgeViewBase + CvCameraViewListener2 提供即插即用的相机帧处理框架。
- 实战项目:文档扫描仪涵盖边缘检测、轮廓近似、透视变换、自适应阈值二值化等核心技术的组合应用。
掌握这些技能后,你可以在 Android 上实现多数常见的计算机视觉应用:滤镜相机、文档扫描、人脸识别、AR 标记跟踪、OCR 预处理等。
参考资料: