【代码解析】opencv 安卓 SDK sample - 1 - HDR image
很久没有写安卓了,复习复习。用的是官方案例,详见opencv-Android-sdk
包
// 定义包名,表示该类的组织路径
package org.opencv.samples.tutorial1;// 导入所需的OpenCV和Android类库
import org.opencv.android.CameraActivity; // OpenCV相机活动基类
import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame; // 相机帧数据
import org.opencv.android.OpenCVLoader; // OpenCV加载器
import org.opencv.core.Mat; // OpenCV矩阵类,用于存储图像数据
import org.opencv.android.CameraBridgeViewBase; // 相机视图基类
import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2; // 相机监听接口
import org.opencv.android.Utils; // OpenCV工具类
import org.opencv.core.Core; // OpenCV核心功能
import org.opencv.imgproc.Imgproc; // 图像处理模块
import org.opencv.photo.Photo; // 照片处理模块
import org.opencv.core.*; // OpenCV核心模块// 导入Android相关类库
import android.os.Bundle; // 用于保存Activity状态
import android.util.Log; // 日志工具
import android.view.SurfaceView; // 表面视图
import android.view.WindowManager; // 窗口管理
import android.widget.Toast; // 提示消息
import android.widget.Button; // 按钮控件
import android.view.View; // 视图基类
import android.os.Environment; // 环境信息
import android.graphics.Bitmap; // 位图类// 导入Java工具类
import java.util.Collections; // 集合工具
import java.util.ArrayList; // 动态数组
import java.util.List; // 列表接口
import java.io.File; // 文件类
import java.io.FileOutputStream; // 文件输出流
import java.text.SimpleDateFormat; // 日期格式化
import java.util.Date; // 日期类
Tutorial1Activity 类
定义
public class Tutorial1Activity extends CameraActivity implements CvCameraViewListener2
继承自 CameraActivity
( 是Activity 基类,它封装了对摄像头的基本处理逻辑,比如打开摄像头、获取帧数据等。)
接口声明
OpenCV 摄像头帧回调接口 CvCameraViewListener2。
implements CvCameraViewListener2
表示这个类 实现了 接口 CvCameraViewListener2,必须实现该接口的全部方法。
这个接口是 OpenCV 提供的,用于处理来自摄像头的数据帧。它有几个关键方法:
public interface CvCameraViewListener2 {void onCameraViewStarted(int width, int height);void onCameraViewStopped();Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame);
}
方法 | 作用 |
---|---|
onCameraViewStarted() | 摄像头启动时初始化 Mat、变量等 |
onCameraViewStopped() | 摄像头停止时释放资源 |
onCameraFrame() | 每帧图像自动调用,可以处理图像 |
📌 各方法说明:
onCameraViewStarted(int width, int height):摄像头视图开始时调用。
onCameraViewStopped():摄像头视图停止时调用。
onCameraFrame(CvCameraViewFrame inputFrame):每帧图像采集时调用,可以在这里进行图像处理(如边缘检测、滤波等),返回 Mat 类型图像结果用于渲染显示。
构造函数
// 构造函数public Tutorial1Activity() {// 打印日志,记录实例化信息Log.i(TAG, "Instantiated new " + this.getClass());}
定义
// 定义日志标签常量定义一个日志标签(log tag),用于在 Android 的 Logcat 控制台中打印日志。便于调试、排查错误,推荐每个类定义一个 TAG。private static final String TAG = "OCVSample::Activity";// 声明相机视图对象声明一个 OpenCV 的摄像头视图控件,它负责打开摄像头、采集图像帧、并把图像送入 onCameraFrame() 回调处理。封装了摄像头的打开、预览、帧捕获、图像格式转换等功能。CameraBridgeViewBase 是 OpenCV for Android 提供的一个抽象类,常用的子类是 JavaCameraView。private CameraBridgeViewBase mOpenCvCameraView;// 声明拍照按钮在 onCreate 中设置利用 setOnClickListener 触发private Button mCaptureButton;// 声明存储RGBA图像的矩阵onCameraFrame() 回调函数中,OpenCV 会将摄像头捕获的帧传递进来private Mat mRgba;// HDR相关声明private Button mHdrButton; // HDR按钮private boolean mIsCapturingHDR = false; // HDR拍摄标志位private List<Mat> mExposureSequence = new ArrayList<>(); // 存储不同曝光图像的列表private static final float[] EXPOSURE_VALUES = {0.5f, 1.0f, 2.0f}; // 定义三种曝光值
函数解析
在 Android 中,Activity 有一个完整的生命周期管理,典型调用顺序如下:
onCreate() → 创建视图和变量
onStart() → Activity 对用户可见
onResume() → Activity 可交互(获取焦点)
onPause() → 准备进入后台
onStop() → Activity 不再可见
onDestroy() → Activity 被销毁
Activity 的构造函数 onCreate
系统会先调用隐藏的构造函数,然后调用 onCreate()。 当该 Activity 第一次被创建时(即启动时),系统会自动调用它。到 onCreate() 时,系统已经准备好了界面容器、上下文环境(Context)、资源引用、生命周期管理器。
这时才能用 findViewById
方法
在 onCreate() 中,通常执行以下操作:
- 设置界面布局
setContentView(R.layout.activity_main);
加载 XML 中定义的 UI 界面。
- 初始化 UI 控件和变量
mOpenCvCameraView = findViewById(R.id.java_camera_view);
mCaptureButton = findViewById(R.id.capture_button);
完成变量与界面元素的绑定,准备用户交互。
- 设置控件监听器
mCaptureButton.setOnClickListener(v -> captureImage());
为按钮或其他 UI 元件设置回调函数。
- 初始化 OpenCV 或其他第三方库
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, this, mLoaderCallback);
加载 OpenCV 库并处理初始化回调。
// Activity创建时的回调方法
@Override 对父类 Activity 中的 onCreate 方法的重写。
Bundle savedInstanceState:
是一个用于存储界面状态的数据结构。
当 Activity 被意外销毁后(如屏幕旋转、被系统回收等),可以通过它恢复先前的状态。public void onCreate(Bundle savedInstanceState) {// 调用父类onCreate方法super.onCreate(savedInstanceState);// 打印日志Log.i(TAG, "called onCreate");// 初始化OpenCV本地库if (OpenCVLoader.initLocal()) {// 初始化成功日志Log.i(TAG, "OpenCV loaded successfully");} else {// 初始化失败日志Log.e(TAG, "OpenCV initialization failed!");// 显示初始化失败提示(Toast.makeText(this, "OpenCV initialization failed!", Toast.LENGTH_LONG)).show();return;}// 设置窗口标志,保持屏幕常亮getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);// 设置布局文件setContentView(R.layout.tutorial1_surface_view);// 获取相机视图引用
将 XML 布局文件中声明的 OpenCV 摄像头预览控件绑定到 Java 代码中的变量 mOpenCvCameraView 上,以便后续调用方法控制摄像头。
强制类型转换,因为 findViewById() 返回的是通用类型 View,而你希望将它当作 CameraBridgeViewBase 来使用。
CameraBridgeViewBase 是 OpenCV 提供的抽象基类,定义了摄像头启动、停止、帧获取等功能。
JavaCameraView 是它的子类,真正实现了预览逻辑。mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.tutorial1_activity_java_surface_view);// 获取拍照按钮引用mCaptureButton = (Button) findViewById(R.id.capture_button);// 获取HDR按钮引用mHdrButton = (Button) findViewById(R.id.hdr_button);// 设置相机视图可见性mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);// 设置相机视图监听器mOpenCvCameraView.setCvCameraViewListener(this);// 设置拍照按钮点击监听器mCaptureButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 检查当前帧是否有效if (mRgba != null && !mRgba.empty()) {// 调用保存图像方法saveImage(mRgba);}}});// 设置HDR按钮点击监听器mHdrButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 设置HDR拍摄标志mIsCapturingHDR = true;// 清空曝光序列mExposureSequence.clear();// 显示提示信息Toast.makeText(Tutorial1Activity.this, "准备HDR拍摄,请保持手机稳定...",Toast.LENGTH_SHORT).show();}});}
CameraBridgeViewBase 是什么?
虽然 CameraBridgeViewBase 本质上是一个“视图组件”(View),但它不仅仅是用来显示摄像头图像,还内置了摄像头控制逻辑、帧采集机制和 OpenCV 回调接口对接。
它是 OpenCV for Android 提供的一个抽象类,负责:
功能 | 说明 |
---|---|
控制摄像头启动/停止 | 封装了打开摄像头、释放摄像头资源的流程 |
帧采集循环 | 自动开启后台线程采集图像帧 |
图像格式转换 | 将 NV21/YUV 等图像转换为 OpenCV 可处理的 Mat 类型 |
接入回调 | 让开发者通过接口拿到每一帧图像进行处理 |
帧采集 + 回调的完整机制
方法 | 功能 |
---|---|
setCvCameraViewListener(this) | 设置帧数据监听器,接收每一帧图像 |
enableView() | 内部会打开摄像头,启动采集线程,开始不断生成帧 |
OpenCV 内部怎么做的?
OpenCV 封装了以下逻辑:
-
打开摄像头(通过 Camera / Camera2 API)
后台线程循环采集图像帧(YUV 或 RGBA)
转换为 Mat 类型(OpenCV 可处理格式)
调用你实现的接口方法:
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {// 每帧图像都走这里,你可以处理它return inputFrame.rgba(); // 或处理后返回
}
对应
你的代码↓
CameraBridgeViewBase↓
JavaCameraView / NativeCameraView↓
系统 Camera API↓
图像帧采集 → 自动转换为 Mat → 回调 onCameraFrame()
// Activity暂停时的回调方法@Overridepublic void onPause() {// 调用父类方法super.onPause();// 如果相机视图存在,则禁用视图if (mOpenCvCameraView != null)mOpenCvCameraView.disableView();}// Activity恢复时的回调方法@Overridepublic void onResume() {// 调用父类方法super.onResume();// 如果相机视图存在,则启用视图if (mOpenCvCameraView != null)mOpenCvCameraView.enableView();}// 获取相机视图列表的方法@Overrideprotected List<? extends CameraBridgeViewBase> getCameraViewList() {// 返回包含单个相机视图的不可变列表return Collections.singletonList(mOpenCvCameraView);}// Activity销毁时的回调方法@Overridepublic void onDestroy() {// 调用父类方法super.onDestroy();// 如果相机视图存在,则禁用视图if (mOpenCvCameraView != null)mOpenCvCameraView.disableView();}// 相机视图启动时的回调方法@Overridepublic void onCameraViewStarted(int width, int height) {// 初始化RGBA矩阵mRgba = new Mat();}// 相机视图停止时的回调方法@Overridepublic void onCameraViewStopped() {// 如果RGBA矩阵存在,则释放资源if (mRgba != null) {mRgba.release();}}// 处理相机帧数据的回调方法@Overridepublic Mat onCameraFrame(CvCameraViewFrame inputFrame) {// 获取当前帧的RGBA数据mRgba = inputFrame.rgba();// 返回处理后的帧return mRgba;}
保存文件
// 保存图像到文件的方法private void saveImage(Mat mat) {// 创建与Mat相同尺寸的Bitmap对象Bitmap bmp = Bitmap.createBitmap(mat.cols(), mat.rows(), Bitmap.Config.ARGB_8888);// 将Mat转换为BitmapUtils.matToBitmap(mat, bmp);// 创建时间戳格式String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());// 生成文件名String imageFileName = "IMG_" + timeStamp + ".jpg";// 获取公共图片目录File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);// 创建文件对象File imageFile = new File(storageDir, imageFileName);try {// 创建文件输出流FileOutputStream fos = new FileOutputStream(imageFile);// 压缩Bitmap为JPEG格式并写入文件bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);// 关闭输出流fos.close();// 显示保存成功提示Toast.makeText(this, "照片已保存: " + imageFile.getAbsolutePath(),Toast.LENGTH_LONG).show();} catch (Exception e) {// 记录保存失败日志Log.e(TAG, "保存照片失败", e);// 显示保存失败提示Toast.makeText(this, "保存照片失败", Toast.LENGTH_SHORT).show();}}