学习 Android (十六) 学习 OpenCV (一)
在前几个章节中,我们对 NDK 相关的开发有了一定的了解,所谓磨刀不误砍柴工,有了这些基础的知识储备之后,我们可以来简单上手一下 OpenCV 相关的知识,接下来跟随作者一起来学习吧:
-
什么是 OpenCV ?
-
搭建 OpenCV Android SDK 环境
-
Mat 类详解
1. 什么是 OpenCV?
OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和机器学习软件库,由英特尔(Intel)于 1999 年首次发布,现由开源社区维护。它提供了丰富的工具和算法,用于处理图像和视频数据,广泛应用于图像处理、物体检测、人脸识别、增强现实(AR)、自动驾驶、医学影像分析等领域。
1.1 OpenCV 的核心特点
-
跨平台支持:
-
支持 Windows、Linux、macOS、Android 和 ios。
-
提供 C++、Python、Java 等语言的接口。
-
-
丰富的功能模块:
-
图像处理(滤波、边缘检测、色彩空间转换等)
-
特做检测(SIFT、SURF、ORB、角色检测)
-
目标检测(Haar 级联、YOLD、SSD)
-
机器学习(SVM、KNN、神经网络)
-
摄像头标定与 3D 重建
-
视频分析(光流、背景减除)
-
-
高性能优化:
-
底层使用 C/C++ 编写、支持多线程和 GPU 加速(如 CUDA、OpenCL)。
-
提供预训练模型(如 DNN 模块支持 TensorFlow、PyTorch 模型)。
-
-
开源免费:
- 基于 BSD 许可证,可自由用于商业和研究用途。
1.2 OpenCV 的典型应用场景
领域 | 应用示例 |
---|---|
人脸识别 | 人脸检测、表情分析、活体检测(如手机解锁) |
自动驾驶 | 车道检测、交通标志识别、行人检测 |
医学影像 | 肿瘤检测、X 光分析、显微镜图像处理 |
工业检测 | 缺陷检测、二维码识别、物体测量 |
增强现实(AR) | 虚拟贴纸、3D 物体叠加(如 Snapchat 滤镜) |
机器人 | SLAM(同步定位与地图构建)、避障 |
1.3 OpenCV 的架构
-
核心模块(Core)
-
基础数据结构(如
Mat
存储图像矩阵)。 -
数学运算、文件 I/O。
-
-
Imgproc(图像处理)
- 滤波(高斯模糊、中值滤波)、边缘检测(Canny)、几何变换(旋转、缩放)。
-
HighGUI(图形界面)
- 显示图像、视频捕获、滑动条交互。
-
DNN(深度学习)
- 支持加载 TensorFlow、PyTorch 模型进行推理。
-
Calib3d(相机标定)
- 相机畸变校正、3D 重建。
2. 搭建 OpenCV Android SDK
在对 OpenCV 有了初步的了解之后,我们开始进行 Android 相关的 SDK 搭建,我们可以从官方下载 OpenCV Android SDK 包 Releases - OpenCV,这里作者选择了最新的 OpenCV - 4.12.0 Android 的包
将下载好的压缩包解压至自己的目录下,然后我们在 Android Studio 中进行操作,File -> New Project -> Native C++
至此我们创建一个了 Andorid Native 项目,因为从官方下载的 Android SDK 包中,提供给我的是 .a
静态库,而不是 .so
动态库,不过这都无所谓,对目前我们来说,只是去了解一下 OpenCV 而已,之后作者进行处理交叉编译获取到 .so
文件的,所以我现在就直接导入我们下载的文件中的 SDK 模块至项目中
选择下载后解压的目录至 sdk 目录下,这里作者的目录是 F:\OpenCV_SDK\opencv-4.12.0-android-sdk\OpenCV-android-sdk\sdk,读者根据自己的来替换
点击 FINISH,会卡顿一会,等待模块的导入即可,接下来我们将添加的 OpenCV 模块添加到我们的 app 模块中
点击ok即可,完成添加,在 MainActivity 代码中尝试是否有 opencv 相关的库
有就说明我们添加成功,至此,OpenCV 的 Android SDK 环境就已经搭建好了,接下来我们将了解并学习 OpenCV 的基础操作
3. Mat 类详解
在Android OpenCV SDK中,Mat
类是用于存储图像数据的核心数据结构。理解 Mat
是高效使用 OpenCV 的关键。
3.1 Mat 类概述
-
定义:
Mat
是一个智能指针类,用于存储 n 维的矩阵(数组),特别适合存储和处理图像数据,所以Mat
类也用于表示图像,支持多种数据类型和通道数。它是 OpenCV 中最基本的数据结构之一。 -
构造函数:
import org.opencv.core.CvType; import org.opencv.core.Mat;// 空构造函数 Mat emptyMat = new Mat();// 创建指定大小的矩阵 Mat mat480x640 = new Mat(480, 640, CvType.CV_8UC3); // 480行,640列,8位无符号3通道// 从文件加载图像 Mat redMat = Imgcodecs.imread("path/to/image.jpg");
-
数据类型:
Mat
支持多种数据类型数据类型 描述 范围 示例 CV_8U
8位无符号整数 0-255 CvType.CV_8UC1
(灰度图)CV_8S
8位有符号整数 -128-127 很少使用 CV_16U
16位无符号整数 0-65535 深度图像 CV_16S
16位有符号整数 -32768-32767 梯度图像 CV_32S
32位有符号整数 标签图 CV_32F
32位浮点数 处理中间结果 CV_64F
64位浮点数 高精度计算 -
通道数:
Mat
可以有不同的通道数,例如:CV_8UC1
单通道:灰度图像CV_8UC3
三通道:彩色图像(如BGR)CV_8UC4
四通道:带Alpha通道的图像(如BGRA)
3.2 Mat 属性和方法
-
基本属性
Mat image = new Mat(480, 640, CvType.CV_8UC3);int rows = image.rows(); // 480 int cols = image.cols(); // 640 int channels = image.channels(); // 3 int depth = image.depth(); // CvType.CV_8U int type = image.type(); // CvType.CV_8UC3 long total = image.total(); // 480 * 640 = 307200 boolean empty = image.empty(); // 是否为空
-
方法
方法名 描述 Mat()
/Mat(rows, cols, type)
等不同构造函数,用于创建空 Mat 或指定大小和类型的矩阵 clone()
深拷贝 Mat,包括数据内容 copyTo(Mat dst)
/copyTo(Mat dst, Mat mask)
将 Mat 数据拷贝到另一个 Mat 或指定掩码区域 convertTo(Mat m, int rtype, double alpha=..., double beta=...)
数据类型转换,并可进行线性变换 create(rows, cols, type)
/create(Size size, type)
重新分配 Mat 内存,如果已有空间可复用 release()
释放 Mat 内部数据(引用计数机制) empty()
判断 Mat 是否为空(未分配数据) rows()
/cols()
/channels()
/type()
/depth()
获取 Mat 的基本属性,如大小、通道数、类型、深度等 total()
Mat 中元素总数(行×列) size()
/size(int i)
获取 Mat 的尺寸信息(宽度、高度或多维信息) elemSize()
/elemSize1()
返回每个元素占用的字节数(全部通道或单通道) step1()
/step1(int i)
获取按元素计算的步长(列间隔) setTo(Scalar value, Mat mask)
设置所有或掩码部分的像素值 get(...)
/put(...)
获取或写入指定像素或通道数据(支持多种数据类型) reshape(int cn, int rows)
改变 Mat 的维度或通道数,不复制数据 row(int y)
/col(int x)
/rowRange(...)
/colRange(...)
/submat(...)
获取子矩阵或区域,支持 ROI 操作 diag(int d)
/diag()
获取主对角线或指定对角元素生成的矩阵 mul(Mat m, double scale=1)
/dot(Mat m)
/cross(Mat m)
元素乘法、点积、叉积等矩阵运算 eye(rows, cols, type)
/ones(...)
/zeros(...)
创建特殊矩阵:单位、全一、全零 push_back(...)
在 Mat 底部追加元素或另一个 Mat reshape(...)
更改通道或形状(与 reshape 同) dump()
/toString()
文本形式输出 Mat 内容(调试专用) getNativeObjAddr()
获取本地 Mat 对象地址,用于 JNI 互操作 -
Mat 运算和算术运算实现
-
矩阵加法
通常的矩阵加法被定义在两个相同大小的矩阵。两个m×n矩阵A和B的和,标记为A+B,一样是个m×n矩阵,其内的各元素为其相对应元素相加后的值。例如:
-
矩阵减法
矩阵的减法,只要其大小相同的话。A-B内的各元素为其相对应元素相减后的值,且此矩阵会和A、B有相同大小。例如:
-
-
矩阵乘法
-
当矩阵A的列数(column)等于矩阵B的行数(row)时,A与B可以相乘。
-
矩阵C的行数等于矩阵A的行数,C的列数等于B的列数。
-
乘积C的第m行第n列的元素等于矩阵A的第m行的元素与矩阵B的第n列对应元素乘积之和。
-
示例
public class MainActivity extends AppCompatActivity {private ActivityMainBinding mBinding; // 视图绑定对象,用于访问布局中的控件private Mat bgr = new Mat(); // 存储BGR格式的Mat对象private Mat source = new Mat(); // 存储RGB格式的Mat对象static {System.loadLibrary("opencv_java4"); // 加载OpenCV的Java库}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityMainBinding.inflate(getLayoutInflater()); // 初始化视图绑定setContentView(mBinding.getRoot()); // 设置布局try {// 从资源文件中加载lena图片到bgr Mat对象(默认BGR格式)bgr = Utils.loadResource(this, R.drawable.lena);// 将图像转换为RGB图像,并保存到sourceImgproc.cvtColor(bgr, source, Imgproc.COLOR_BGR2RGB);// 在ivSource控件中显示原始图片(资源文件)mBinding.ivSource.setImageResource(R.drawable.lena);// 创建一个与source相同尺寸的Bitmap,用于显示MatBitmap bitmap = Bitmap.createBitmap(source.width(), source.height(), Bitmap.Config.ARGB_8888);// 将bgr Mat对象转换为BitmapUtils.matToBitmap(bgr, bitmap);// 在ivBgr控件中显示转换后的BitmapmBinding.ivBgr.setImageBitmap(bitmap);} catch (IOException e) {throw new RuntimeException(e); // 图片加载失败时抛出运行时异常}// 为各个按钮绑定点击事件,调用对应的图像处理方法mBinding.bitwiseNot.setOnClickListener(view -> bitwiseNot(source));mBinding.bitwiseAnd.setOnClickListener(view -> bitwiseAnd(source, bgr));mBinding.bitwiseOr.setOnClickListener(view -> bitwiseOr(source, bgr));mBinding.bitwiseXor.setOnClickListener(view -> bitwiseXor(source, bgr));mBinding.add.setOnClickListener(view -> add(source, bgr));mBinding.subtract.setOnClickListener(view -> subtract(source, bgr));mBinding.multiply.setOnClickListener(view -> multiply(source, bgr));mBinding.divide.setOnClickListener(view -> divide(source, bgr));}/*** 将处理结果Mat转换为Bitmap并显示到ivResult*/private void showResult(Mat dst) {Bitmap bitmap = Bitmap.createBitmap(dst.width(), dst.height(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(dst, bitmap);mBinding.ivResult.setImageBitmap(bitmap);}/*** 按位取反操作*/private void bitwiseNot(Mat source) {Mat dst = new Mat();Core.bitwise_not(source, dst);showResult(dst);dst.release(); // 释放内存}/*** 按位与操作*/private void bitwiseAnd(Mat source, Mat attach) {Mat dst = new Mat();Core.bitwise_and(source, attach, dst);showResult(dst);dst.release();}/*** 按位或操作*/private void bitwiseOr(Mat source, Mat attach) {Mat dst = new Mat();Core.bitwise_or(source, attach, dst);showResult(dst);dst.release();}/*** 按位异或操作*/private void bitwiseXor(Mat source, Mat attach) {Mat dst = new Mat();Core.bitwise_xor(source, attach, dst);showResult(dst);dst.release();}/*** 图像加法*/private void add(Mat source, Mat attach) {Mat dst = new Mat();Core.add(source, attach, dst);showResult(dst);dst.release();}/*** 图像减法*/private void subtract(Mat source, Mat attach) {Mat dst = new Mat();Core.subtract(source, attach, dst);showResult(dst);dst.release();}/*** 图像乘法*/private void multiply(Mat source, Mat attach) {Mat dst = new Mat();Core.multiply(source, attach, dst);showResult(dst);dst.release();}/*** 图像除法(额外缩放系数为50.0,通道不变)*/private void divide(Mat source, Mat attach) {Mat dst = new Mat();Core.divide(source, attach, dst, 50.0, -1); // 参数:src1, src2, dst, scale, dtypeCore.convertScaleAbs(dst, dst); // 转换为8位无符号整型并取绝对值showResult(dst);dst.release();}}
原图与 Mat 默认处理图
按位非
按位与
按位或
按位异或
矩阵加法
矩阵减法
矩阵乘法
矩阵除法
4. Imgproc 类详解
在Android OpenCV中,Imgproc
类是一个非常重要的工具,专门用于图像处理。它提供了多种功能,包括图像转换、滤波、边缘检测、形态学操作等。以下是对Imgproc
类的详细讲解:
4.1 模块概述
Imgproc(Image Processing Module)模块包含:
-
图像滤波与平滑
// 高斯模糊 Imgproc.GaussianBlur(src, dst, new Size(5, 5), 0);// 中值滤波 Imgproc.medianBlur(src, dst, 5);// 双边滤波(保边去噪) Imgproc.bilateralFilter(src, dst, 9, 75, 75);// 自定义卷积核 Mat kernel = Imgproc.getGaussianKernel(5, 1.5); Imgproc.filter2D(src, dst, -1, kernel);
-
几何变换
// 图像缩放 Imgproc.resize(src, dst, new Size(width*0.5, height*0.5));// 图像旋转 Mat rotMat = Imgproc.getRotationMatrix2D(new Point(width/2, height/2), 45, 1.0); Imgproc.warpAffine(src, dst, rotMat, src.size());// 透视变换 Mat perspectiveMat = Imgproc.getPerspectiveTransform(srcPoints, dstPoints); Imgproc.warpPerspective(src, dst, perspectiveMat, src.size());
-
形态学操作
// 创建结构元素 Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5,5));// 膨胀 Imgproc.dilate(src, dst, kernel);// 腐蚀 Imgproc.erode(src, dst, kernel);// 开运算(先腐蚀后膨胀) Imgproc.morphologyEx(src, dst, Imgproc.MORPH_OPEN, kernel);// 闭运算(先膨胀后腐蚀) Imgproc.morphologyEx(src, dst, Imgproc.MORPH_CLOSE, kernel);
-
阈值处理
// 简单阈值 Imgproc.threshold(src, dst, 127, 255, Imgproc.THRESH_BINARY);// 自适应阈值 Imgproc.adaptiveThreshold(src, dst, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY, 11, 2);// Otsu阈值 Imgproc.threshold(src, dst, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
-
边缘检测
// Canny边缘检测 Imgproc.Canny(src, edges, 50, 150); 55 // Sobel算子 Mat gradX = new Mat(); Imgproc.Sobel(src, gradX, CvType.CV_16S, 1, 0, 3); Core.convertScaleAbs(gradX, gradX);// Laplacian算子 Mat laplacian = new Mat(); Imgproc.Laplacian(src, laplacian, CvType.CV_16S, 3); Core.convertScaleAbs(laplacian, laplacian);
-
轮廓分析
List<MatOfPoint> contours = new ArrayList<>(); Mat hierarchy = new Mat();// 查找轮廓 Imgproc.findContours(binaryImage, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE);// 绘制轮廓 Mat drawing = Mat.zeros(binaryImage.size(), CvType.CV_8UC3); for (int i = 0; i < contours.size(); i++) {Scalar color = new Scalar(0, 255, 0);Imgproc.drawContours(drawing, contours, i, color, 2); }// 轮廓近似 MatOfPoint2f approxCurve = new MatOfPoint2f(); for (MatOfPoint contour : contours) {MatOfPoint2f contour2f = new MatOfPoint2f(contour.toArray());double epsilon = 0.02 * Imgproc.arcLength(contour2f, true);Imgproc.approxPolyDP(contour2f, approxCurve, epsilon, true); }
-
直方图处理
// 计算直方图 List<Mat> images = new ArrayList<>(); images.add(src); MatOfInt channels = new MatOfInt(0); // 通道0 Mat hist = new Mat(); MatOfInt histSize = new MatOfInt(256); MatOfFloat ranges = new MatOfFloat(0, 256); Imgproc.calcHist(images, channels, new Mat(), hist, histSize, ranges);// 直方图均衡化 Mat gray = new Mat(); Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY); Mat equalized = new Mat(); Imgproc.equalizeHist(gray, equalized);
-
霍夫变换
// 霍夫直线检测 Mat lines = new Mat(); Imgproc.HoughLinesP(edges, lines, 1, Math.PI/180, 50, 50, 10);// 绘制检测到的直线 for (int i = 0; i < lines.rows(); i++) {double[] data = lines.get(i, 0);double x1 = data[0], y1 = data[1], x2 = data[2], y2 = data[3];Imgproc.line(dst, new Point(x1, y1), new Point(x2, y2), new Scalar(0, 0, 255), 3); }// 霍夫圆检测 Mat circles = new Mat(); Imgproc.HoughCircles(edges, circles, Imgproc.HOUGH_GRADIENT, 1, 30, 100, 30, 10, 100);
-
图像金字塔等
// 高斯金字塔降采样 Mat down = new Mat(); Imgproc.pyrDown(src, down);// 高斯金字塔上采样 Mat up = new Mat(); Imgproc.pyrUp(down, up);// 拉普拉斯金字塔 Mat downUp = new Mat(); Imgproc.pyrUp(down, downUp); Mat laplacian = new Mat(); Core.subtract(src, downUp, laplacian);
Imgproc
类是进行图像处理的核心工具,提供了丰富的功能来进行图像转换、滤波、边缘检测、形态学操作等。掌握这些方法将帮助我们在Android OpenCV中高效处理图像数据。由于篇幅问题,这里不进行全部模块的详细讲解,这里就讲解一下各个模块的简单的示例,在之后的文章中我们在分别对模块进行单独的了解。
5. 亮度与对比度
亮度和对比度是图像处理和视觉表现中两个重要的概念。它们直接影响图像的质量和视觉效果。以下是对这两个概念的详细介绍。
5.1 亮度
5.1.1 定义
亮度是指图像中光的强度或明亮程度。它影响到图像的整体明暗程度。
5.1.2 影响因素
-
光源:光源的类型和强度会直接影响亮度
-
图像内容:图像中的颜色和纹理对亮度感知也有影响
5.1.3 调整亮度
-
增加亮度:使图像看起来更亮,可能会导致细节丢失,尤其是在高光区域。
-
降低亮度:使图像变暗,可能会导致细节丢失,尤其是在阴影区域。
5.1.4 应用场景
-
在照片编辑软件中,亮度调整通常用于改善图像的可视性和清晰度。
-
在视频制作中,亮度条左右帮助创造不同的氛围和情绪。
5.1.5 数学公式
g(x,y)=f(x,y)+β
-
f(x,y):原始图像像素值
-
g(x,y):输出图像像素值
-
β:亮度调节参数(偏移量),可正可负
5.2 对比度
5.2.1 定义
对比度是指图像中最亮和最暗部分之间的差异。高对比度意味图像中有很强的明暗对比,而低对比度则表现为图像的亮度差异较小。
5.2.2 影响因素
-
色彩:鲜艳的颜色通常会增加对比度,而灰色或相似色的组合会降低对比度。
-
光照条件:光照的变化也会影响对比度的感知。
5.2.3 调整对比度
-
增加对比度:使亮部更亮、暗部更暗,增强图像的清晰度和层次感。
-
降低对比度:使图像亮度差异减少,图像看起来更加平坦,适合某些艺术效果。
5.2.4 应用场景
-
在摄影和图像处理中,通过调整对比度可以突出主要对象,使其更具吸引力。
-
在设计中,高对比度可以用于吸引注意力,而低对比度则适合柔和的视觉效果。
5.2.5 数学公式
g(x,y)=α⋅f(x,y)
α:对比度参数(缩放系数),一般 >0
-
α>1:对比度增强
-
0<α<1:对比度降低
5.3 亮度与对比度的关系
-
相辅相成:亮度和对比度往往是相互影响的。调整亮度可能会影响对比度,反之亦然。
-
视觉效果:适当的亮度和对比度调整可以极大改善图像的视觉效果,增加细节的可见性。
-
结合公式:在 OpenCV 中,通常同时调节亮度和对比度:
g(x,y)=α⋅f(x,y)+β
-
α:对比度控制(缩放因子)
-
β:亮度控制(偏移量)
-
5.4 示例
这里我是实现一个滑动调整图片亮度和对比度的示例 Demo,我们知道,同时调节亮度和对比度的公式实际上是 对比度控制(缩放因子)先和原始图像像素值相乘,然后再加上亮度控制(偏移量)
布局代码就不是提供了,直接上代码
public class ContrastBrightnessActivity extends AppCompatActivity {// 视图绑定private ActivityContrastBrightnessBinding mBinding;// 加载 OpenCV 本地库static {System.loadLibrary("opencv_java4");}// 存储图像的 Mat 对象private Mat source;// 存储原始亮度、当前亮度和对比度的变量private double originBrightness = 0.0;private double brightness = 0.0;private double contrast = 100.0;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 使用视图绑定来膨胀布局mBinding = ActivityContrastBrightnessBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {// 从资源中加载源图像Mat bgr = Utils.loadResource(this, R.drawable.lena);// 将图像转换为灰度 Matsource = new Mat();Imgproc.cvtColor(bgr, source, Imgproc.COLOR_BGR2RGB);// 从 Mat 创建 Bitmap,并设置到 ImageViewBitmap bitmap = Bitmap.createBitmap(source.width(), source.height(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(source, bitmap);mBinding.ivSource.setImageBitmap(bitmap);// 计算图像的原始亮度originBrightness = Core.mean(source).val[0];mBinding.sbBrightness.setProgress((int) originBrightness);// 设置亮度 SeekBar 的监听器mBinding.sbBrightness.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {// 更新亮度值并调整图像brightness = progress;adjustBrightnessContrast();}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {// 开始滑动时无需操作}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {// 停止滑动时无需操作}});} catch (IOException e) {// 处理加载资源时可能出现的 IOExceptionthrow new RuntimeException(e);}// 初始化对比度 SeekBarmBinding.sbContrast.setProgress(100);mBinding.sbContrast.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {// 更新对比度值并调整图像contrast = progress;adjustBrightnessContrast();}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {// 开始滑动时无需操作}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {// 停止滑动时无需操作}});}@Overrideprotected void onDestroy() {// 释放 Mat 资源以防止内存泄漏source.release();super.onDestroy();}private void adjustBrightnessContrast() {// 创建一个新的 Mat 用于最终输出Mat dst = new Mat();source.convertTo(dst, -1, contrast / 100, brightness - originBrightness);// 从最终 Mat 创建 Bitmap 并设置到 ImageViewBitmap bitmap = Bitmap.createBitmap(dst.cols(), dst.rows(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(dst, bitmap);mBinding.ivSource.setImageBitmap(bitmap);// 释放中间的 Mat 以释放内存dst.release();}
}
6. 颜色模型及转换
6.1 基础总览
-
OpenCV 默认是 BGR (不是 RGB) ,
Utils.loadResource/Imgcodecs.imread
得到的 3 通道图一般是 CV_8UC3(B,G,R),带透明通道是 BGRA。 -
常用 API
-
颜色空间转换:
Imgproc.cvtColor(src, dst, code)
-
拆分/合并通道:
Core.split(mat, mats) / Core.merge(mats, mat)
-
通道重排:
Core.mixChannels(srcMats, dstMats, fromTo)
-
区间阈值(颜色分割):
Core.inRange(hsv, lower, upper, mask)
-
-
数据类型与范围(8 位 vs 浮点)
-
CV_8U
:每通道 0–255 -
CV_32F/64F
:通常归一化到 0–1(部分颜色空间如 HSV 的 H 通道会用角度制 0–360)
-
6.2 常用颜色空间与范围
6.2.1 BGR / RGB / BGRA
-
含义:设备/显示友好的颜色空间。OpenCV 默认用 BGR。
-
范围(8位):B,G,R ∈ [0,255](BGRA 多一个 A∈[0,255])
-
常用转换:
COLOR_BGR2RGB
,COLOR_BGR2GRAY
,COLOR_BGRA2BGR
,COLOR_BGR2HSV
, …
6.2.2 GRAY (灰度)
-
含义:亮度通道(近似人眼明暗感知)。
-
范围(8位):Y ∈ [0,255]
-
用途:阈值、边缘、角点等传统 CV 算法的前置处理。
Imgproc.cvtColor(src, gray, COLOR_BGR2GRAY)
6.2.3 HSV / HLS
-
含义:把颜色拆成色调(H)、饱和度(S) 和 亮度/明度(V/L)。对光照变化更稳。
-
范围:
-
8位 HSV/HLS:H ∈ [0,179](不是 360!),S,V/L ∈ [0,255]
-
浮点 HSV/HLS:H ∈ [0,360],S,V/L ∈ [0,1]
-
-
用途:颜色分割(如检测红色/绿色物体)、颜色跟踪。
-
转换:
COLOR_BGR2HSV
,COLOR_BGR2HLS
6.2.4 YCrCb (YCbCr)
-
含义:Y 为亮度(luma),Cr/Cb 为色度(红差/蓝差)。图像/视频压缩常用。
-
范围(8位):Y,Cr,Cb 一般按 0–255 表示(中性色度 ≈ 128)。
(JPEG/视频存储可能使用“缩放范围”,但在 OpenCV 中通常是全幅 0–255 表示) -
用途:肤色检测、受光照影响较小的颜色阈值。
-
转换:
COLOR_BGR2YCrCb
6.2.5 YUV (以及相机 NV21 / I420 / YV12 等)
-
含义:Y + UV 色度,移动端相机常见 YUV420 采样(NV21 等)。
-
转换(示例):
COLOR_YUV2BGR_NV21
,COLOR_YUV2RGB_NV21
-
用途:Camera 预览帧到 BGR/RGB 的实时转换。
6.2.6 CLE Lab / Luv / XYZ
-
含义:感知均匀的颜色空间,Lab 的 L* 接近人眼亮度感觉,a*, b* 为对手色轴。
-
范围:
-
8位 Lab:L ∈ [0,255](OpenCV 把真实 L∈[0,100]缩放到 0–255),a,b 以 128 为中性(即 a=0→128,b*=0→128)
-
浮点 Lab:L* ∈ [0,100],a*,b* 约在 [-127,127]
-
-
用途:亮度/颜色分离增强(只在 L* 做锐化/对比度增强),颜色转移等。
-
转换:
COLOR_BGR2Lab
,COLOR_BGR2Luv
,COLOR_BGR2XYZ
(默认为 D65 白点)
6.2.7 CMYK (说明)
OpenCV 没有直接的 CMYK 图像类型;若需处理印刷流程的 CMYK/ICC,需要借助其他库(如 LittleCMS)或自写公式做近似转换。
6.2.8 Bayer / RAW (说明)
传感器马赛克数据 → cvtColor
的 Bayer 代码:COLOR_BayerBG2BGR
等。
6.2.9 速查表
颜色空间 | 通道含义 | 8位范围 | 典型用途 | 转换代码 |
---|---|---|---|---|
BGR | B,G,R | 0–255 | 显示、绘制、通用 | COLOR_BGR2… |
GRAY | Y | 0–255 | 阈值、边缘 | COLOR_BGR2GRAY |
HSV | H,S,V | H:0–179; S,V:0–255 | 颜色分割/追踪 | COLOR_BGR2HSV |
HLS | H,L,S | H:0–179; L,S:0–255 | 颜色分割(亮度分离) | COLOR_BGR2HLS |
YCrCb | Y,Cr,Cb | 0–255(Cr/Cb≈128为中性) | 肤色、压缩域处理 | COLOR_BGR2YCrCb |
Lab | L,a,b | L:0–255; a,b:0–255(128为中性) | 感知均匀处理、增强 | COLOR_BGR2Lab |
6.2.10 示例
示例 Demo 可以让我们对各个颜色空间的效果有着根据直观的认知,这里只提供实现代码,布局不提供
public class ColorTransferActivity extends AppCompatActivity {private ActivityColorTransferBinding mBinding;static {System.loadLibrary("opencv_java4");}private Mat mBgr;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityColorTransferBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {mBgr = Utils.loadResource(this, R.drawable.lena);showMat(mBgr);} catch (IOException e) {throw new RuntimeException(e);}mBinding.btnBgr.setOnClickListener(view -> {showMat(mBgr);});mBinding.btnRgb.setOnClickListener(view -> {Mat rgb = new Mat();Imgproc.cvtColor(mBgr, rgb, Imgproc.COLOR_RGB2BGR);showMat(rgb);rgb.release();});mBinding.btnYuv.setOnClickListener(view -> {Mat yuv = new Mat();Imgproc.cvtColor(mBgr, yuv, Imgproc.COLOR_BGR2YUV);showMat(yuv);yuv.release();});mBinding.btnHsv.setOnClickListener(view -> {Mat hsv = new Mat();Imgproc.cvtColor(mBgr, hsv, Imgproc.COLOR_BGR2HSV);showMat(hsv);hsv.release();});mBinding.btnLab.setOnClickListener(view -> {Mat lab = new Mat();Imgproc.cvtColor(mBgr, lab, Imgproc.COLOR_BGR2Lab);showMat(lab);lab.release();});mBinding.btnGray.setOnClickListener( view -> {Mat gray = new Mat();Imgproc.cvtColor(mBgr, gray, Imgproc.COLOR_BGR2GRAY);showMat(gray);gray.release();});}private void showMat(Mat source) {Bitmap bitmap = Bitmap.createBitmap(source.cols(), source.rows(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(source, bitmap);mBinding.ivSource.setImageBitmap(bitmap);}@Overrideprotected void onDestroy() {mBgr.release();super.onDestroy();}
}
7. 多通道分离与合并
7.1 什么是通道(Channel)
-
图像在 OpenCV 中通常用 Mat 表示。
-
彩色图像往往包含多个通道(Channel),比如:
-
灰度图像(Grayscale):只有 1 个通道,范围 [0, 255],表示亮度。
-
BGR 彩色图像:有 3 个通道,分别表示蓝色(B)、绿色(G)、红色(R)。
-
RGBA 彩色图像:有 4 个通道,B、G、R 加上透明通道 A
-
在 OpenCV (默认 BGR 顺序)中,彩色图像存储结构是 矩阵的每个像素包含多个通道值 , 为此在处理某些图像时,我们可以进行通道的分离或者合并来达到我们需要的效果。
public class ChannelSplitMergeActivity extends AppCompatActivity {private ActivityChannelSplitMergeBinding mBinding;static {System.loadLibrary("opencv_java4");}private Mat mBgr;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityChannelSplitMergeBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {mBgr = Utils.loadResource(this, R.drawable.lena);showMat(mBgr);} catch (IOException e) {throw new RuntimeException(e);}mBinding.btnBgr.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();Mat rgb = new Mat();Imgproc.cvtColor(mBgrClone, rgb, Imgproc.COLOR_BGR2RGB);showMat(rgb);rgb.release();showMat(mBgrClone);});mBinding.btnRgb.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();Mat rgb = new Mat();Imgproc.cvtColor(mBgrClone, rgb, Imgproc.COLOR_RGB2BGR);showMat(rgb);rgb.release();});mBinding.btnB.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();List<Mat> channels = new ArrayList<>();Core.split(mBgrClone, channels);Core.add(channels.get(0), new Scalar(50), channels.get(0));Mat merged = new Mat();Core.merge(channels, merged);showMat(merged);merged.release();for (Mat channel : channels) {channel.release();}});mBinding.btnG.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();List<Mat> channels = new ArrayList<>();Core.split(mBgrClone, channels);Core.add(channels.get(1), new Scalar(50), channels.get(1));Mat merged = new Mat();Core.merge(channels, merged);showMat(merged);merged.release();for (Mat channel : channels) {channel.release();}});mBinding.btnR.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();List<Mat> channels = new ArrayList<>();Core.split(mBgrClone, channels);Core.add(channels.get(2), new Scalar(50), channels.get(2));Mat merged = new Mat();Core.merge(channels, merged);showMat(merged);merged.release();for (Mat channel : channels) {channel.release();}});mBinding.btnBgZero.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();Mat zero = Mat.zeros(mBgrClone.rows(), mBgrClone.cols(), CvType.CV_8UC1);List<Mat> channels = new ArrayList<>();Core.split(mBgrClone, channels);Mat mChannelR = channels.get(2);List<Mat> list = new ArrayList<>();list.add(zero);list.add(zero);list.add(mChannelR);Mat result = new Mat();Core.merge(list, result);showMat(result);result.release();});mBinding.btnBrZero.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();Mat zero = Mat.zeros(mBgrClone.rows(), mBgrClone.cols(), CvType.CV_8UC1);List<Mat> channels = new ArrayList<>();Core.split(mBgrClone, channels);Mat mChannelG = channels.get(1);List<Mat> list = new ArrayList<>();list.add(zero);list.add(mChannelG);list.add(zero);Mat result = new Mat();Core.merge(list, result);showMat(result);result.release();});mBinding.btnGrZero.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();Mat zero = Mat.zeros(mBgrClone.rows(), mBgrClone.cols(), CvType.CV_8UC1);List<Mat> channels = new ArrayList<>();Core.split(mBgrClone, channels);Mat mChannelB = channels.get(0);List<Mat> list = new ArrayList<>();list.add(mChannelB);list.add(zero);list.add(zero);Mat result = new Mat();Core.merge(list, result);showMat(result);result.release();});mBinding.btnBrRb.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();Mat zero = Mat.zeros(mBgrClone.rows(), mBgrClone.cols(), CvType.CV_8UC1);List<Mat> channels = new ArrayList<>();Core.split(mBgrClone, channels);Mat mChannelB = channels.get(0);Mat mChannelG = channels.get(1);Mat mChannelR = channels.get(2);List<Mat> list = new ArrayList<>();list.add(mChannelB);list.add(mChannelG);list.add(mChannelR);Mat result = new Mat();Core.merge(list, result);showMat(result);result.release();});}private void showMat(Mat source) {Bitmap bitmap = Bitmap.createBitmap(source.cols(), source.rows(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(source, bitmap);mBinding.ivSource.setImageBitmap(bitmap);}@Overrideprotected void onDestroy() {mBgr.release();super.onDestroy();}
}
8. 图像二值化
8.1 什么是图像二值化
图像二值化(Image Binarization)顾名思义,就是将图像上的灰度值设置为只有两个可能的值:通常是0(黑色)或 255(白色)。整个图像呈现出明显的只有黑和白的视觉效果。
本质: 通过设定一个阈值 T
,将图像中的所有像素点分为两类:
-
大于阈值 T 的像素点: 被设置为 255(白色,代表前景或目标)
-
小于等于阈值 T 的像素点: 被置为 0 (黑色,代表背景)
原始图像(灰度图)的每个像素值在 0 - 255 之间,经过二值化处理后,图像矩形中只剩下 0 和 255 两种数值。这极大的简化了图像数据,减少了计算量,并且能够凸显出目标的轮廓和结构。
8.2 为什么需要二值化
二值化是很多高级图像处理任务的预处理步骤,其主要目的和优势包括:
-
简化信息,突出目标: 将感兴趣的目标(前景)与背景分离开,忽略不必要的细节(如颜色、渐变灰度)。
-
大幅减少数据量: 图像从 256 个灰度级别减少到 2 两个,数据量显著减少,后续处理速度更快。
-
为后续操作做准备: 它是很多操作的前提,例如:
-
轮廓查找: OpenCV 的轮廓查找函数通常需要再二值图像上进行。
-
字符识别(OCR): 识别文字前,需要现将包含文字的图像二值化,将文字与纸张背景分离。
-
图像分割: 将物体从背景中分离出来
-
计算物体的面积、周长、位置等形态学分析
-
8.3 OpenCV 中的阈值化(二值化)方法
OpenCV 提供了 cv::threshold()
函数来进行类型的阈值化操作,其中最核心的就是二值化。
函数原型
double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type);
参数详解:
-
src: 输入图像,必须是单通道灰度图像。如果是彩色图像,需要先转换为灰度图(
cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
)。 -
dst: 输出图像(二值化后的结果),与输入图像具有相同的大小和类型。
-
thresh: 设定的阈值(0~255)。
-
maxval: 当像素值超过(或小于,根据 type 决定)阈值时,所赋予的新值。对于二值化,通常设为 255。
-
type: 阈值化的类型,它决定了如何应用阈值。这是最关键的参数:
-
cv2.THRESH_BINARY
(最常用) -
cv2.THRESH_BINARY_INV
-
cv2.THRESH_TRUNC
-
cv2.THRESH_TOZERO
-
cv2.THRESH_TOZERO_INV
-
cv2.THRESH_OTSU
(通常与上述类型组合使用,如cv2.THRESH_BINARY + cv2.THRESH_OTSU
) -
cv2.THRESH_TRIANGLE
-
8.4 关键的阈值化类型详解
我们重点看与二值化最相关的几种类型。假设阈值为 T
,最大值为 M
(通常是255)。
-
简单阈值(全局阈值)
这种方法对整个图像使用同一个固定阈值
T
。-
cv2.THRESH_BINARY
(标准二值化)-
公式:
dst(x, y) = M if src(x, y) > T else 0
-
解释:像素值大于阈值
T
的设为M
(白色),否则设为 0(黑色)。 -
适用场景:光照均匀,背景和前景对比度非常明显的图像。
-
-
cv2.THRESH_BINARY_INV
(反二值化)-
公式:
dst(x, y) = 0 if src(x, y) > T else M
-
解释:与
BINARY
相反。像素值大于阈值T
的设为 0(黑色),否则设为M
(白色)。 -
适用场景:想要目标为白色,背景为黑色的情况,但与
BINARY
的结果相反。
-
-
-
自适应阈值(局部阈值)
简单阈值法的一个巨大缺陷是:它需要一个全局阈值
T
,但对于光照不均或者背景复杂的图像,很难找到一个合适的T
来很好地分割整个图像。解决方案:自适应阈值(Adaptive Thresholding)。它不再是使用一个全局阈值,而是根据图像上每一个小区域(邻域)内的像素值,独自计算该区域的阈值。
函数原型
void cv::adaptiveThreshold(Mat src, Mat& dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)
-
src: 输入灰度图像。
-
dst: 输出图像。
-
maxValue: 满足条件的像素被赋予的最大值(通常是255)。
-
adaptiveMethod: 计算阈值的方法。
-
cv2.ADAPTIVE_THRESH_MEAN_C
: 阈值是邻域区域的平均值减去常数C
。 -
cv2.ADAPTIVE_THRESH_GAUSSIAN_C
: 阈值是邻域区域的加权和(高斯窗口),权重是一个高斯核,再减去常数C
。
-
-
thresholdType: 必须是
cv2.THRESH_BINARY
或cv2.THRESH_BINARY_INV
。 -
blockSize: 邻域大小(用来计算阈值的区域大小),必须是奇数(如 3, 5, 7, …)。
-
C: 从平均值或加权平均值中减去的常数。这是一个微调参数,用于优化结果。
适用场景:光照不均、背景颜色变化较大的图像(如文档扫描、拍摄的纸张等)。
简单阈值 vs 自适应阈值效果对比:
(左:原图 | 中:简单阈值效果差 | 右:自适应阈值效果好) -
-
Otsu’s 方法(大津算法)
简单阈值法另一个问题是:如何选择最佳阈值
T
?手动尝试非常低效。解决方案:Otsu’s 方法。这是一种自动确定图像最佳全局阈值的算法。其原理是最大化前景和背景两类之间的类间方差(inter-class variance)。方差越大,说明两部分差别越大,分割效果越好。
使用方法:Otsu‘s 算法不是单独使用的,而是与
cv2.THRESH_BINARY
等组合使用。并且,cv2.threshold
函数会返回两个值,第一个就是 Otsu 算法计算出的最佳阈值。适用场景:图像具有双峰直方图(即图像的像素值分布可以明显看出有两个高峰)时,效果最好。
8.5 示例
public class ImageBinarizationActivity extends AppCompatActivity {private ActivityImageBinarizationBinding mBinding;static {System.loadLibrary("opencv_java4");}private Mat mBgr;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityImageBinarizationBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {mBgr = Utils.loadResource(this, R.drawable.lena);Imgproc.cvtColor(mBgr, mBgr, Imgproc.COLOR_BGR2GRAY);showMat(mBgr);} catch (IOException e) {throw new RuntimeException(e);}mBinding.btnBinary.setOnClickListener(view -> {threshold(Imgproc.THRESH_BINARY);});mBinding.btnBinaryInv.setOnClickListener(view -> {threshold(Imgproc.THRESH_BINARY_INV);});mBinding.btnTrunc.setOnClickListener(view -> {threshold(Imgproc.THRESH_TRUNC);});mBinding.btnTozero.setOnClickListener(view -> {threshold(Imgproc.THRESH_TOZERO);});mBinding.btnTozeroInv.setOnClickListener(view -> {threshold(Imgproc.THRESH_TOZERO_INV);});mBinding.btnOtsu.setOnClickListener(view -> {threshold2(Imgproc.THRESH_BINARY + Imgproc.THRESH_OTSU);});mBinding.btnTrlangle.setOnClickListener(view -> {threshold2(Imgproc.THRESH_BINARY + Imgproc.THRESH_TRIANGLE);});mBinding.btnOtsuInv.setOnClickListener(view -> {threshold2(Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_OTSU);});mBinding.btnTriangleInv.setOnClickListener(view -> {threshold2(Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_TRIANGLE);});mBinding.btnMeanC.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();Mat result = new Mat();// 自适应阈值 - 均值方法Imgproc.adaptiveThreshold(mBgrClone, result, 255,Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 11, 2);showMat(result);mBgrClone.release();result.release();});mBinding.btnGaussianC.setOnClickListener(view -> {Mat mBgrClone = mBgr.clone();Mat result = new Mat();// 自适应阈值 - 高斯方法Imgproc.adaptiveThreshold(mBgrClone, result, 255,Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY, 11, 2);showMat(result);mBgrClone.release();result.release();});}private void threshold(int type) {Mat mBgrClone = mBgr.clone();Imgproc.threshold(mBgrClone, mBgrClone, 127, 255, type);showMat(mBgrClone);mBgrClone.release();}private void threshold2(int type) {Mat mBgrClone = mBgr.clone();Imgproc.threshold(mBgrClone, mBgrClone, 0, 255, type);showMat(mBgrClone);mBgrClone.release();}private void showMat(Mat source) {Bitmap bitmap = Bitmap.createBitmap(source.cols(), source.rows(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(source, bitmap);mBinding.ivSource.setImageBitmap(bitmap);}@Overrideprotected void onDestroy() {mBgr.release();super.onDestroy();}
}