目录
  1. 1. 一、OpenCV Android SDK 环境配置
    1. 1.1. 1.1 SDK 获取
    2. 1.2. 1.2 Gradle 集成
    3. 1.3. 1.3 OpenCVLoader —— 加载 Native 库
    4. 1.4. 1.4 使用回调方式的异步初始化
  2. 2. 二、Mat —— 核心图像数据结构
    1. 2.1. 2.1 Mat 的本质
    2. 2.2. 2.2 理解 CV_8UC3 —— 类型常量
    3. 2.3. 2.3 Mat 的创建与访问
    4. 2.4. 2.4 ROI(Region of Interest)
  3. 3. 三、图像读写与颜色空间转换
    1. 3.1. 3.1 imread 与 imwrite
    2. 3.2. 3.2 cvtColor —— 颜色空间转换
  4. 4. 四、图像滤波与平滑
    1. 4.1. 4.1 线性滤波
    2. 4.2. 4.2 非线性滤波
    3. 4.3. 4.3 边缘检测 —— Canny
    4. 4.4. 4.4 常用滤波效果对比
  5. 5. 五、特征检测与描述 —— ORB
    1. 5.1. 5.1 ORB 原理简述
    2. 5.2. 5.2 detectAndCompute
    3. 5.3. 5.3 KeyPoint 的属性
  6. 6. 六、特征匹配
    1. 6.1. 6.1 Brute-Force 匹配器
    2. 6.2. 6.2 RANSAC 与单应性矩阵
    3. 6.3. 6.3 FLANN 匹配器(适合大规模匹配)
  7. 7. 七、目标检测
    1. 7.1. 7.1 CascadeClassifier —— Haar / LBP 级联检测
    2. 7.2. 7.2 DNN 模块 —— 深度学习目标检测
    3. 7.3. 7.3 模型类型与 load 方法
    4. 7.4. 7.4 DNN 后端与目标选择
  8. 8. 八、CameraBridgeViewBase —— 实时相机处理
    1. 8.1. 8.1 JavaCameraView vs NativeCameraView
    2. 8.2. 8.2 实时图像处理示例
    3. 8.3. 8.3 layout XML
  9. 9. 九、完整实战:文档扫描仪
    1. 9.1. 9.1 文档边缘检测
    2. 9.2. 9.2 透视变换矫正
    3. 9.3. 9.3 图像增强(使扫描件清晰)
  10. 10. 十、性能优化与注意事项
    1. 10.1. 10.1 减少 Mat 分配
    2. 10.2. 10.2 使用多线程
    3. 10.3. 10.3 图像尺寸缩放
    4. 10.4. 10.4 OpenCL 加速
  11. 11. 十一、总结
【音视频、图像处理技术】视觉库OpenCV学习

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()) {
// 初始化失败(通常是 .so 文件缺失或 ABI 不匹配)
Log.e("OpenCV", "OpenCV initialization failed!");
// 降级策略:提示用户或使用纯 Java 图像处理
} 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 采用引用计数机制,因此复制头部是廉价的,只有数据被共享:

// Java 层
Mat src = new Mat(480, 640, CvType.CV_8UC3); // 640x480, 3 通道, uint8
Mat copy = src.clone(); // 深拷贝(复制数据)
Mat view = src; // 浅拷贝(共享数据,引用计数 +1)
Mat roi = src.submat(new Rect(0, 0, 320, 240)); // ROI(共享数据)

在 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)); // ROI,共享数据

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(用于某些算法的中间结果,如距离变换)
// C++ 层创建不同类型的 Mat
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); // 全1
cv::Mat img = cv::Mat::eye(3, 3, CV_32FC1); // 3x3 单位矩阵

// 从已有数据创建
uchar data[] = {0, 0, 255, 0, 255, 0, 255, 0, 0}; // B, G, R
cv::Mat img(1, 3, CV_8UC3, data); // 1行3列

// ---- 访问像素 ----
// 方式1:at<T>() 模板方法(推荐,有类型检查)
cv::Vec3b& pixel = img.at<cv::Vec3b>(row, col); // CV_8UC3 专用
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 图访问
float val = float_img.at<float>(row, col);

// 方式2:ptr 指针(高效,适合逐行处理)
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); // 全白
}
}

// 方式3:data 原始指针(最高效,注意步长)
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; // B
p[c * img.channels() + 1] = 128; // G
p[c * img.channels() + 2] = 64; // R
}
}

// ---- Mat 属性 ----
img.rows; // 行数(高度)
img.cols; // 列数(宽度)
img.channels(); // 通道数
img.type(); // 类型编码(如 CV_8UC3 = 16)
img.depth(); // 深度(如 CV_8U = 0)
img.step; // 每行的字节数(含 padding,通常为 cols * elemSize)
img.elemSize(); // 每个元素的字节数(每个像素所有通道的总字节数)
img.total(); // 元素总数 = rows * cols

2.4 ROI(Region of Interest)

ROI 允许在图像上指定一个矩形区域进行操作,该操作共享原始数据,无需额外分配内存:

// 提取 ROI
cv::Rect roi_rect(100, 50, 200, 150); // x, y, width, height
cv::Mat roi = src(roi_rect); // 浅拷贝,共享底层数据

// ROI 操作直接作用于原图
cv::GaussianBlur(roi, roi, cv::Size(5, 5), 1.5); // 仅对 ROI 区域模糊
cv::rectangle(src, roi_rect, cv::Scalar(0, 255, 0), 2); // 画框标记 ROI

// 将 ROI 拷贝到另一位置(需要深拷贝)
cv::Mat roi_copy = roi.clone();
roi_copy.copyTo(src(cv::Rect(400, 50, 200, 150)));

// 不规则 ROI:使用 mask
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); // 只复制 mask 非零区域的像素

三、图像读写与颜色空间转换

3.1 imread 与 imwrite

// 读取图像
cv::Mat img = cv::imread("/sdcard/photo.jpg", cv::IMREAD_COLOR);
// IMREAD_COLOR(默认): 三通道 BGR
// IMREAD_GRAYSCALE: 单通道灰度
// IMREAD_UNCHANGED: 保留原格式(含 Alpha 通道)
// IMREAD_ANYCOLOR / IMREAD_ANYDEPTH

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); // JPEG 质量 0-100
cv::imwrite("/sdcard/output.jpg", img, compression_params);

compression_params.clear();
compression_params.push_back(cv::IMWRITE_PNG_COMPRESSION);
compression_params.push_back(3); // PNG 压缩级别 0-9
cv::imwrite("/sdcard/output.png", img, compression_params);

3.2 cvtColor —— 颜色空间转换

// BGR → 灰度
cv::Mat gray;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);

// BGR → RGB
cv::Mat rgb;
cv::cvtColor(src, rgb, cv::COLOR_BGR2RGB);

// BGR → HSV(色调-饱和度-明度)
cv::Mat hsv;
cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);

// BGR → YUV(YCrCb)
cv::Mat yuv;
cv::cvtColor(src, yuv, cv::COLOR_BGR2YCrCb);

// BGR → LAB
cv::Mat lab;
cv::cvtColor(src, lab, cv::COLOR_BGR2Lab);

// Android 特殊情况:YUV_420_888 (NV21) → BGR
cv::Mat yuv_mat(height * 3/2, width, CV_8UC1, yuv_data);
cv::Mat bgr;
cv::cvtColor(yuv_mat, bgr, cv::COLOR_YUV2BGR_NV21);

// 带 Alpha 通道
cv::Mat bgra;
cv::cvtColor(src, bgra, cv::COLOR_BGR2BGRA); // BGR → BGRA

常用颜色空间转换码速查:

转换码 含义
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));
// Size(5,5) 表示 5x5 的核,值均为 1/25

// 方框滤波 —— 类似 blur,但可选择是否归一化
cv::Mat box_img;
cv::boxFilter(src, box_img, -1, cv::Size(5, 5), true); // normalize=true → 同 blur

// 高斯模糊 —— 权重按高斯分布,最常用的模糊
cv::Mat gauss_img;
cv::GaussianBlur(src, gauss_img, cv::Size(5, 5), 0);
// Size(5,5): 核大小(必须为奇数)
// sigmaX=0: 根据核大小自动计算 sigma
// 也可以指定 sigmaX 和 sigmaY
cv::GaussianBlur(src, gauss_img, cv::Size(0, 0), 3.0); // 自动计算核大小

// 梯度滤波 —— Sobel(一阶导数)
cv::Mat grad_x, grad_y;
cv::Sobel(gray, grad_x, CV_16S, 1, 0, 3); // x 方向梯度,核大小 3
cv::Sobel(gray, grad_y, CV_16S, 0, 1, 3); // y 方向梯度

// 取绝对值并转回 8 位
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); // 核大小 5(必须为奇数 > 1)

// 双边滤波 —— 同时考虑空间距离和像素值差异(保边滤波)
cv::Mat bilateral_img;
cv::bilateralFilter(src, bilateral_img, 9, // 核直径
75, // 颜色空间 sigma(像素差异的敏感度)
75); // 坐标空间 sigma(距离的敏感度)
// 用于磨皮美颜:保留边缘,平滑平坦区域

// 形态学操作(非线性)
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);

// Canny 边缘检测
double low_threshold = 50;
double high_threshold = 150;
int kernel_size = 3; // Sobel 核大小
cv::Canny(gray, edges, low_threshold, high_threshold, kernel_size);

// threshold1(低阈值):边缘连接的下限
// threshold2(高阈值):强边缘的阈值
// 经验法则:ratio = low:high = 1:2 或 1:3

4.4 常用滤波效果对比

// 自适应阈值 —— 对光照不均匀的图像有效
cv::Mat adaptive;
cv::adaptiveThreshold(gray, adaptive, 255,
cv::ADAPTIVE_THRESH_GAUSSIAN_C,
cv::THRESH_BINARY,
11, // 邻域大小
2); // 常数 C

// Otsu 阈值 —— 自动计算最佳阈值
cv::Mat otsu;
double thresh = cv::threshold(gray, otsu, 0, 255,
cv::THRESH_BINARY | cv::THRESH_OTSU);
// 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>

// 创建 ORB 检测器
cv::Ptr<cv::ORB> orb = cv::ORB::create(
500, // nfeatures:最多保留 500 个特征点
1.2f, // scaleFactor:图像金字塔每层的缩放比例
8, // nlevels:金字塔层数
31, // edgeThreshold:边界忽略宽度
0, // firstLevel:起始金字塔层级
2, // WTA_K:生成 BRIEF 描述符时比较的点数(2=成对比较)
cv::ORB::HARRIS_SCORE, // scoreType:角点评分方式
31, // patchSize:描述符计算邻域大小
20 // fastThreshold:FAST 角点阈值
);

// 检测特征点并计算描述符
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);
// DRAW_RICH_KEYPOINTS:画圆(大小表示尺度)+ 方向线

5.3 KeyPoint 的属性

struct KeyPoint {
Point2f pt; // 关键点在图像中的坐标 (x, y)
float size; // 关键点的尺度(直径)
float angle; // 方向(度,0-360),-1 表示无方向
float response; // 响应强度(角点质量分数)
int octave; // 检测到关键点的金字塔层级
int class_id; // 可分配的对象类别 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);

// 创建 BFMatcher(Brute-Force)
cv::BFMatcher matcher(cv::NORM_HAMMING); // 对 ORB 用 Hamming 距离
// cv::NORM_L2 用于 SIFT/SURF(浮点描述符)
// cv::NORM_HAMMING 用于 ORB/BRIEF/BRISK(二进制描述符)

// 方式 1:每个查询描述符返回 k 个最佳匹配
const int k = 2;
std::vector<std::vector<cv::DMatch>> knn_matches;
matcher.knnMatch(desc1, desc2, knn_matches, k);

// Lowe's ratio test —— 过滤不可靠的匹配
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]);
}
}

// 方式 2:直接匹配(不推荐,容易引入错误匹配)
// std::vector<cv::DMatch> matches;
// matcher.match(desc1, desc2, matches);

// 绘制匹配结果
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) {
// 用 RANSAC 计算单应性矩阵
cv::Mat H = cv::findHomography(points1, points2, cv::RANSAC, 3.0);

// 用 mask 标记 inlier / outlier
std::vector<uchar> mask;
cv::Mat H_with_mask = cv::findHomography(points1, points2,
cv::RANSAC, 3.0, mask);

// 统计 inlier 数量
int inliers = cv::countNonZero(mask);
LOGD("RANSAC inliers: %d / %zu", inliers, points1.size());

if (inliers > 20) {
// 用单应性矩阵将图 1 的四个角映射到图 2
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);

// 在图 2 上绘制映射后的四边形
cv::polylines(img2, corners2, true, cv::Scalar(0, 255, 0), 3);
}
}

6.3 FLANN 匹配器(适合大规模匹配)

// FLANN 对 ORB 二进制描述符需要用 LSH 索引
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>

// 加载预训练的 Haar 级联模型
cv::CascadeClassifier face_cascade;
// Android: 将 XML 文件放到 assets 或 res/raw,运行时拷贝到内部存储
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, // scaleFactor:每次图像缩小 10% 进行检测
3, // minNeighbors:至少 3 个重叠检测才确认
0, // flags
cv::Size(30, 30), // minSize:最小人脸尺寸
cv::Size() // maxSize:最大人脸尺寸(不限制)
);

// 绘制检测结果
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>

// 加载模型
// .prototxt: 网络结构定义文件(Caffe 格式)
// .caffemodel: 训练好的权重文件
cv::dnn::Net net = cv::dnn::readNetFromCaffe(
"/sdcard/MobileNetSSD_deploy.prototxt",
"/sdcard/MobileNetSSD_deploy.caffemodel"
);

// 设置后端和目标(Android 上推荐 OpenCL)
net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
net.setPreferableTarget(cv::dnn::DNN_TARGET_OPENCL);
// 如果没有 OpenCL 则降级到 CPU:
// net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);

// 准备输入 blob
cv::Mat blob = cv::dnn::blobFromImage(
src, // 输入图像
1.0, // scale factor
cv::Size(300, 300), // 网络要求的输入尺寸(MobileNet-SSD 用 300x300)
cv::Scalar(127.5, 127.5, 127.5), // mean subtraction
true, // swapRB (BGR → RGB)
false // crop
);
net.setInput(blob);

// 前向推理
cv::Mat detections = net.forward();

// 解析检测结果
// MobileNet-SSD 输出 shape: (1, 1, N, 7)
// 每行: [image_id, class_id, confidence, left, top, right, bottom]
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 语言后端
// 尝试使用 Vulkan
net.setPreferableBackend(cv::dnn::DNN_BACKEND_VULKAN);
net.setPreferableTarget(cv::dnn::DNN_TARGET_VULKAN);
// 如果失败,OpenCV 会自动回退到 CPU

八、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();

// 加载级联分类器(从 assets 复制到内部存储)
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();

// ---- 处理1:人脸检测 ----
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);
}

// ---- 处理2:Canny 边缘检测 ----
Imgproc.Canny(mGray, mCanny, 80, 200);

// 返回处理后的帧(RGBA 格式自动渲染到屏幕)
return mRgba; // 可以换为 mCanny 看边缘效果
}

@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;

// 1. 灰度 + 高斯模糊(降噪)
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);

// 2. Canny 边缘检测
cv::Canny(blurred, edges, 50, 150);

// 3. 膨胀边缘(连接断裂的边缘)
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);

// 4. 查找轮廓
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(edges, contours, hierarchy,
cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

// 5. 寻找最大的四边形轮廓
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; // 没找到文档

// 6. 排序四个角点(左上 → 右上 → 右下 → 左下)
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);
// 上方两个点:x 小的为左上,x 大的为右上
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];
}
// 下方两个点:x 小的为左下,x 大的为右下
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) {
// 计算目标矩形的宽度和高度
// 宽度 = max(上边长, 下边长)
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);

// 高度 = max(左边长, 右边长)
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);

// 目标点:A4 比例(或自定义)
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)}
};

// 源点(需要转换为 Point2f)
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;

// 1. 转灰度
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);

// 2. 自适应阈值(二值化,模拟扫描效果)
cv::adaptiveThreshold(gray, enhanced, 255,
cv::ADAPTIVE_THRESH_GAUSSIAN_C,
cv::THRESH_BINARY,
11, // blockSize(必须为奇数)
5); // C(常数调整)

// 3. 可选:去噪(中值滤波)
cv::medianBlur(enhanced, enhanced, 3);

return enhanced;
}

// 完整扫描流程
cv::Mat scan_document(const cv::Mat& input) {
// Step 1: 检测文档边缘
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);
}

// Step 2: 透视变换矫正
cv::Mat warped = four_point_transform(input, corners);

// Step 3: 图像增强
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 使用多线程

// OpenCV 内部多线程(通过 TBB 或 OpenMP,需要在编译时启用)
cv::setNumThreads(4); // 设置 OpenCV 使用的线程数
int num_threads = cv::getNumThreads();

10.3 图像尺寸缩放

处理大图时先缩小,提高速度:

cv::Mat resized;
double scale = 640.0 / src.cols; // 目标宽 640
cv::resize(src, resized, cv::Size(), scale, scale, cv::INTER_AREA);
// 处理 resized ...
// 如果需要,将坐标映射回原图尺寸

10.4 OpenCL 加速

OpenCV 的一些算法(DNN、滤波等)可以通过 OpenCL 加速:

// 检查 OpenCL 是否可用
if (cv::ocl::haveOpenCL()) {
cv::ocl::setUseOpenCL(true);
LOGD("OpenCL enabled: %s", cv::ocl::useOpenCL() ? "yes" : "no");
}

十一、总结

OpenCV 在 Android 上的集成路线清晰:

  1. SDK 集成:通过 Gradle module 导入 OpenCV Android SDK,用 OpenCVLoader 加载 Native 库。
  2. Mat 是核心:深度、通道、类型、ROI 的机制必须烂熟于心。
  3. 图像处理cvtColor 颜色空间转换、GaussianBlur / medianBlur / bilateralFilter 滤波、Canny 边缘检测是日常操作。
  4. 特征检测与匹配:ORB(cv::ORB::createdetectAndCompute)+ BFMatcher + Lowe’s ratio test 是移动端特征匹配的标准组合。
  5. 目标检测:CascadeClassifier 做 Haar/LBP 人脸检测,DNN 模块跑深度学习模型(MobileNet-SSD/YOLO 等)。
  6. 实时相机CameraBridgeViewBase + CvCameraViewListener2 提供即插即用的相机帧处理框架。
  7. 实战项目:文档扫描仪涵盖边缘检测、轮廓近似、透视变换、自适应阈值二值化等核心技术的组合应用。

掌握这些技能后,你可以在 Android 上实现多数常见的计算机视觉应用:滤镜相机、文档扫描、人脸识别、AR 标记跟踪、OCR 预处理等。

参考资料

打赏
  • 微信
  • 支付宝

评论