上一篇中记录了如何在安卓项目配置OpenCV4,并且运行了一个示例程序,可以将一张预设的照片灰度处理。在实际使用过程中可能会从相册选取图片或者拍照之后马上处理,本文记录一下如何实现这两种功能。菜鸟水平,有问题还请各位大佬指正。
首先要搞清楚Bitmap和Mat,其分别是安卓平台和OpenCV中的图像对象,所以当我们在安卓平台使用OpenCV处理图片,需要先将Bitmap转为Mat,OpenCV处理完成后再将Mat转回Bitmap。
该APP需要调用摄像头、读写图像数据,所以要在AndroidManifest中声明三项权限。并且记得申请运行时权限。
再说一句,都2020年了,安卓版本最低支持到7.0,也就是API 24。
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
大致分为两个步骤,点击“相册”按钮后,执行以下方法,其中CHOOSE_PHOTO为相册的requestCode,当选好图片返回到MainActivity界面,onActivityResult()方法根据requestCode做出响应。
后面的handleImageOnKitKat()和displayImage()是用的郭霖《第一行代码 Android 第二版》里的示例,Uri这东西实在搞得人头昏。。。
private void selectPic() {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent, "图像选择。。。"), CHOOSE_PHOTO );
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(resultCode == RESULT_OK && requestCode == CHOOSE_PHOTO) {
handleImageOnKitKat(data);
}
if(resultCode == RESULT_OK && requestCode == TAKE_PHOTO) {
//该uri就是照片文件夹对应的uri
Bitmap bit = null;
try {
bit = BitmapFactory.decodeStream(getContentResolver().openInputStream(pictureUri));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 给相应的ImageView设置图片 未裁剪
iv.setImageBitmap(bit);
}
}
private void handleImageOnKitKat(Intent data) {
Uri uri = data.getData();
Log.d("TAG", "handleImageOnKitKat: uri is " + uri);
if (DocumentsContract.isDocumentUri(this, uri)) {
// 如果是document类型的Uri,则通过document id处理
String docId = DocumentsContract.getDocumentId(uri);
if("com.android.providers.media.documents".equals(uri.getAuthority())) {
String id = docId.split(":")[1]; // 解析出数字格式的id
String selection = MediaStore.Images.Media._ID + "=" + id;
imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
} else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
imagePath = getImagePath(contentUri, null);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// 如果是content类型的Uri,则使用普通方式处理
imagePath = getImagePath(uri, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
// 如果是file类型的Uri,直接获取图片路径即可
imagePath = uri.getPath();
}
displayImage(imagePath); // 根据图片路径显示图片
}
private void displayImage(String imagePath) {
if (imagePath != null) {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
iv.setImageBitmap(bitmap);
} else {
Toast.makeText(this, "failed to get image", Toast.LENGTH_SHORT).show();
}
}
private String getImagePath(Uri uri, String selection) {
String path = null;
// 通过Uri和selection来获取真实的图片路径
Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
}
return path;
}
因为在选图时已经获取了其路径,可以直接用imread()来加载Mat对象
private void convert2Gray2() {
Mat src = Imgcodecs.imread(imagePath);
if(src.empty()) {
return;
}
Mat dst = new Mat();
Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2GRAY);
Bitmap bitmap = grayMat2Bitmap(dst);
ImageView iv = this.findViewById(R.id.sample_img);
iv.setImageBitmap(bitmap);
src.release();
dst.release();
}
该方法grayMat2Bitmap()主要增加了一个降低分辨率的步骤,在后续使用中发现不降也行,还没有遇到图太大导致OOM(Out Of Memory)的情况。
private Bitmap grayMat2Bitmap(Mat result) {
Mat image = null;
if(result.cols() > 1000 || result.rows() > 1000) {
image = new Mat();
Imgproc.resize(result, image, new Size(result.cols() / 4, result.rows() / 4));
} else {
image = result;
}
Bitmap bitmap = Bitmap.createBitmap(image.cols(),image.rows(), Bitmap.Config.ARGB_8888);
Imgproc.cvtColor(image, image, Imgproc.COLOR_GRAY2RGBA);
Utils.matToBitmap(image, bitmap);
image.release();
return bitmap;
}
这里需要使用到content provider,先在AndroidManifest中声明一下,并在res目录下新建一个xml目录,在xml中新建一个file_paths.xml文件。
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".FirstActivity"></activity>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:authorities="com.nuaa.lrqopencvdemo.provider"
android:name="androidx.core.content.FileProvider"
android:grantUriPermissions="true"
android:exported="false" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
</application>
file_paths.xml文件
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="my_images"
path=""/>
</paths>
按下拍照按钮执行该方法。TAKE_PHOTO是拍照的requestCode。
private void takePhoto() {
//创建File对象,用于存储拍照后的图片。getExternalCacheDir 得到专门用于存放当前应用缓存数据的目录
outputImage = new File(getExternalCacheDir(), "output_image.jpg");
try {
if(outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
pictureUri = FileProvider.getUriForFile(this, "com.nuaa.lrqopencvdemo.provider", outputImage);
//启动相机程序
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
// 使用startActivityForResult来启动活动,引测拍完照后会有结果返回到onActivityResult中
startActivityForResult(intent, TAKE_PHOTO);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(resultCode == RESULT_OK && requestCode == CHOOSE_PHOTO) {
handleImageOnKitKat(data);
}
if(resultCode == RESULT_OK && requestCode == TAKE_PHOTO) {
//该uri就是照片文件夹对应的uri
Bitmap bit = null;
try {
bit = BitmapFactory.decodeStream(getContentResolver().openInputStream(pictureUri));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 给相应的ImageView设置图片 未裁剪
iv.setImageBitmap(bit);
}
}
这里是直接根据Uri获取Bitmap对象。
private void convert2Gray3() {
try {
ContentResolver cs = getContentResolver();
Bitmap bp = BitmapFactory.decodeStream(cs.openInputStream(pictureUri));
Mat src = new Mat();
Mat dst = new Mat();
Utils.bitmapToMat(bp, src);
Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGRA2GRAY);
Utils.matToBitmap(dst, bp);
ImageView iv = this.findViewById(R.id.sample_img);
iv.setImageBitmap(bp);
src.release();
dst.release();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
自己瞎写的,因为会处理预设图像、相册图像、拍照图像,来源不一样所以写了三个不同的处理方法,毕竟不熟练导致代码复杂了,不过功能还是能实现。
点击“相册”或者“拍照”按钮都会给标志数album_or_camera 赋值,这样在点击“灰度”按钮后,只需要进行简单判断即可知道该调用哪个方法了。
BTW,打开相册也没申请运行时权限,不知道为什么没报错。。。
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.process_btn:
if(album_or_camera == 0) {
convert2Gray2();
} else if(album_or_camera == 1) {
convert2Gray3();
} else {
convert2Gray();
}
break;
case R.id.select_btn:
album_or_camera = 0;
selectPic();
break;
case R.id.shoot_btn:
album_or_camera = 1;
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE
)!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 300);
} else {
takePhoto();
}
break;
}
}