一. 整体框架
跨年回来,终于感觉可以有点时间写点什么东西,于是把折腾了一阵子的一个用OpenCV搞的东西拿出来记录一下,搞这个的目的一是发现有的P图应用里面连个透视矫正都没有还P个什么图,看着不别扭么,另外一个原因就是对Adobe家的自动矫正技术还蛮好奇是怎样实现的,于是整个工程的整体框架基于github项目OpenCV_native ,该项目主要介绍了在Android中接入使用OpenCV的三种方式,但是感觉都不是很友好,可能是用惯了python这种语言,然后发现在Android里面用个OpenCV如此麻烦,也是感觉心累。
二. 界面的设计
由于主要介绍的是怎样实现,于是界面什么的就一切从简了,顺便能够节能减排。原型的话(呸,什么原型,分明就是实物图)大致就如下图所示了:
接下来复习一下控件的操作然后一些踩的坑。
三.控件的操作
这里用这些控件元素要注意的点有如下一些,好久没写Android界面的我也是遇坑无数:
1. 屏幕旋转之后,要防止Activity的资源被销毁
这里使用的是将公共元素放到Application类中以防止丢失的办法解决的
2. “选择图像”调用的逻辑,这里包括了存储权限的获取、获得回调返回的图像数据、获得图像数据后图像发生旋转的问题
下面来逐一说明一下以上几个要点,首先关于存储权限的获取,说到权限自然要在manifest里面加上以下二货:
1 2 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />然后要在Activity里面加上判断是否已经获取到了存储权限的代码,如果没有获得存储权限那就要向用户要权限,不然怎么拿到图片?
1 2 3 4 5 6 7 8 9 10 11 public static boolean isGrantExternalRW(Activity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && activity.checkSelfPermission( Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { activity.requestPermissions(new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }, PERMISSIONS_CODE); return false; } return true; }拿到权限之后要进行选择图片的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSIONS_CODE) { for (int i = 0; i < permissions.length; i++) { String permission = permissions[i]; int grantResult = grantResults[i]; if (permission.equals(Manifest.permission.READ_EXTERNAL_STORAGE)) { if (grantResult == PackageManager.PERMISSION_GRANTED) { Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); startActivityForResult(intent, RESULT_LOAD_IMAGE); } else { Toast.makeText(context, "You need to grant external rw permission first...", Toast.LENGTH_SHORT).show(); } } } } }接下来就是要获得回调返回的图像数据,在重载的onActivityResult方法里面要通过URL拿到图像数据,然后进行图像的解码,需要说明的是,如果直接解码可能会发生图像旋转的现象,这里我们用图像的EXIF信息来对原图的角度进行重置,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 try { ExifInterface exif = new ExifInterface(picturePath); int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1); Log.d("EXIF", "Exif: " + orientation); Matrix matrix = new Matrix(); if (orientation == 6) { matrix.postRotate(90); } else if (orientation == 3) { matrix.postRotate(180); } else if (orientation == 8) { matrix.postRotate(270); } myBitmap = Bitmap.createBitmap(myBitmap, 0, 0, myBitmap.getWidth(), myBitmap.getHeight(), matrix, true); // rotating bitmap } catch (Exception e) { Log.d(TAG, "something was wrong when dealing with exif orientation..."); }
3. “选择图像”,“对比”,以及图像处理的相关操作如果使用不当会导致ImageView显示的图像错乱,所以要准备好几个图像的对象分别进行各个状态下图像的存储
这里我使用了三个Bitmap对象对图像进行分状态的存取,它们分别是: result保存最终的结果; loadedPic保存原始图像用以“对比”; processingPic保存处理中的图像,用以在不同的图像处理状态间切换;
4. 之所以使用两个ImageView是因为另外一个ImageView需要显示透视矫正过程中的半透明参考线
5. SeekBar的UI配置也是错综复杂,很麻烦,我用了一个Animation来做刻度的显示
但是还有个Bug没有解决,就是我的刻度显示动画效果是用透明度实现的,现在的问题是如果在上一个动画没有执行完的情况下,马上调整SeekBar就会导致刻度的那个控件透明度直接干到0,无法显示了,只有当动画的时间过掉之后才能恢复正常,在setOnSeekBarChangeListener的三个重载方法里面周旋了好久还是没有解决这个问题,闹心,回头再看吧
四. 透视的算法
其实这里没有什么特别的,主要是放在JNI里面很蛋疼,那代码写的既不像python,又不像c++,首先碰到的就是JNI头文件的生成问题,就是如果java代码里面涉及到了Android的一些类,会提示找不到相关类的报错,于是需要手动指定一下Android SDK相关类文件的位置
1
javah -class Android_sdk_path/sdk/platforms/(version)/android.jar -d output_h_file_path java_class_with_package_name
然后要注意的就是关于透视的实现,透视的实现,之前是使用了传图像数组的方式,后来使用OpenCV透视变换出来的图会呈现雪花状,这里怀疑是图像通道的问题,后来改成传图像内存地址的方式进行操作,就正常了,从内存读图像的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// get image content from mem
AndroidBitmapInfo inBmpInfo, outBmpInfo;
void* inPixelsAddress;
void* outPixelsAddress;
int ret;
if ((ret = AndroidBitmap_getInfo(env, bmpIn, &inBmpInfo)) < 0) {
LOGE("AndroidBitmap_getInfo() bmpIn failed ! error=%d", ret);
return 0;
}
if ((ret = AndroidBitmap_getInfo(env, bmpOut, &outBmpInfo)) < 0) {
LOGE("AndroidBitmap_getInfo() bmpOut failed ! error=%d", ret);
return 0;
}
LOGI("original image :: width is %d; height is %d; stride is %d; format is %d;flags is %d", inBmpInfo.width, inBmpInfo.height, inBmpInfo.stride, inBmpInfo.format, inBmpInfo.flags, inBmpInfo.stride);
if ((ret = AndroidBitmap_lockPixels(env, bmpIn, &inPixelsAddress)) < 0) {
LOGE("AndroidBitmap_lockPixels() bmpIn failed ! error=%d", ret);
}
if ((ret = AndroidBitmap_lockPixels(env, bmpOut, &outPixelsAddress)) < 0) {
LOGE("AndroidBitmap_lockPixels() bmpOut failed ! error=%d", ret);
}
Mat inMat(inBmpInfo.height, inBmpInfo.width, CV_8UC4, inPixelsAddress);
Mat outMat(outBmpInfo.height, outBmpInfo.width, CV_8UC4, outPixelsAddress);
int w = inBmpInfo.width;
int h = inBmpInfo.height;
// after processing, you need unlock the mem content
AndroidBitmap_unlockPixels(env, bmpIn);
AndroidBitmap_unlockPixels(env, bmpOut);
具体的透视变换代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// max angle == 20, 50 / 20 == 2.5
float angle = abs(progress) / 2.5;
cv::Point2f src_points[] = {
cv::Point2f(0, 0),
cv::Point2f(w, 0),
cv::Point2f(0, h),
cv::Point2f(w, h)
};
cv::Point2f dst_points[4];
if (direction == 0) {
// horizontal
float dy = w * tan(angle * PI / 180) / (tan(30 * PI / 180) + tan(angle * PI / 180));
float dx = dy * tan(30 * PI / 180);
if (progress > 0) {
// right side
dst_points[0] = cv::Point2f(0, 0);
dst_points[1] = cv::Point2f(w + dy, 0 - dx);
dst_points[2] = cv::Point2f(0, h);
dst_points[3] = cv::Point2f(w + dy, h + dx);
} else {
// left side
dst_points[0] = cv::Point2f(0 - dy, 0 - dx);
dst_points[1] = cv::Point2f(w, 0);
dst_points[2] = cv::Point2f(0 - dy, h + dx);
dst_points[3] = cv::Point2f(w, h);
}
LOGE("h: %d\n", h);
LOGE("w: %d\n", w);
LOGE("dy: %f\n", dy);
LOGE("dx: %f\n", dx);
} else {
// vertical
float dy = h * tan(angle * PI / 180) / (tan(30 * PI / 180) + tan(angle * PI / 180));
float dx = dy * tan(30 * PI / 180);
if (progress > 0) {
// up side
dst_points[0] = cv::Point2f(0 - dx, 0 - dy);
dst_points[1] = cv::Point2f(w + dx, 0 - dy);
dst_points[2] = cv::Point2f(0, h);
dst_points[3] = cv::Point2f(w, h);
} else {
// down side
dst_points[0] = cv::Point2f(0, 0);
dst_points[1] = cv::Point2f(w, 0);
dst_points[2] = cv::Point2f(0 - dx, h + dy);
dst_points[3] = cv::Point2f(w + dx, h + dy);
}
}
cv::Mat M = cv::getPerspectiveTransform(src_points, dst_points);
for (int i = 0; i < 3; i ++) {
for (int j = 0; j < 3; j ++) {
LOGE("m[%d][%d] = %d", i, j, M.data[3 * i + j] - '0');
}
}
cv::warpPerspective(inMat, outMat, M, outMat.size(), cv::INTER_LINEAR);
关于基本的透视变换就是这么多了,貌似在load OpenCV库的时候还有时延问题,因为如果进入app操作透视变换的命令太快app就会crash,后续再跟一下看具体是什么问题导致的,下次写写自动透视矫正的内容。
哦对了,还有在JNI里面打Log的问题,首先需要在Android.mk里加上LOCAL_LDLIBS += -llog,然后需要在cpp文件里面引入#include <android/log.h>,最后可以定义一些宏方便打log:
1
2
3
4
5
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// log example
LOGE("m[%d][%d] = %d", i, j, M.data[3 * i + j] - '0');
-The End-