OpenCV在Android上虽然有自己的开源库,能够处理很多的图像问题,但是一旦涉及到一些需要使用算法方面的问题比如骨架化或者像素点操作的问题时,其处理速度会变得很满,且处理效果并不是十分完美。
例如我最近需要实现书法字的骨架化问题,对于使用导入的OpenCV库,如果使用像素点的逐个操作,要是再放在主线程肯定会导致ANR,毕竟这样的操作太耗时了。而改用其他的骨架化算法效果不佳:
public static void skeletonProcess(Bitmap bitmap, int value) {
org.opencv.android.Utils.bitmapToMat(bitmap, sSrc);
Imgproc.cvtColor(sSrc, sSrc, Imgproc.COLOR_BGRA2GRAY);
Imgproc.threshold(sSrc, sSrc, 0, 255,
Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);
Mat ske = new Mat(sSrc.size(), CvType.CV_8UC1, new Scalar(0, 0, 0));
Mat temp = new Mat(sSrc.size(), CvType.CV_8UC1);
Mat erode = new Mat();
sStrElement = Imgproc.getStructuringElement(Imgproc.MORPH_CROSS, new Size(3, 3));
boolean done;
do {
Imgproc.erode(sSrc, erode, sStrElement);
Imgproc.dilate(erode, temp, sStrElement);
Core.subtract(sSrc, temp, temp);
Core.bitwise_or(ske, temp, ske);
erode.copyTo(sSrc);
done = (Core.countNonZero(sSrc) == 0);
} while (!done);
Imgproc.GaussianBlur(ske, ske, new Size(5, 5), 0, 0, 4);
Imgproc.threshold(ske, ske, 0, 255,
Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);
org.opencv.android.Utils.matToBitmap(ske, bitmap);
ske.release();
temp.release();
erode.release();
sStrElement.release();
sSrc.release();
}
先腐蚀,再膨胀;后减操作,最后与操作,这样的算法相比使用像素化其速度还是可以保证的,但是细化效果却不是最好的。
如图所示,骨架化的图片存在一定是细节缺失,其次存在大量的噪声点,让整个细化后的图片显得不是最好的,对后续其他的操作也会带来不好的影响。
因此,还是需要使用像素点的操作方式。实现像素点方式的骨架化有很多,但都是基于C++的。好在Android拥有JNI方式,可以通过实现native方法来实现。
要实现如此的方法,具体有如下的一些方法:
1. 导入OpenCV库,这里不再赘述。
2. 通过Cmake将OpenCV的so库导入到工程中
这里我的实现其实并不好,使用的绝对路径,这样的操作对以后的重新下载工程是不好的,未来会改进
如下方法实现是基于已经在工程中添加了C++支持。
1. CMakeLists.txt
这个是在app目录下的CMakeLists.txt
#工程目录
set(pathToProject D:\\Android\\workplace\\calligraphyRecognize)
#OpenCV目录
set(pathToOpenCv D:\\Android\\OpenCV-android-sdk)
#CMake版本信息
cmake_minimum_required(VERSION 3.4.1)
#支持-std=gnu++11
set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
#配置加载native依赖
include_directories(${pathToOpenCv}/sdk/native/jni/include)
#CPP文件夹下带编译的cpp文件
add_library( native-lib SHARED src/main/cpp/native-lib.cpp )
#动态方式加载
add_library( lib_opencv SHARED IMPORTED )
#引入libopencv_java3.so文件
set_target_properties(lib_opencv
PROPERTIES IMPORTED_LOCATION
${pathToProject}/app/src/main/jniLibs/${ANDROID_ABI}/libopencv_java3.so)
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( native-lib
${log-lib}
lib_opencv)
上述的地址有些是绝对地址,如果在自己电脑上实现需要修改。
2. Build.Gradle
需要在Android的根目录下修改或添加两处:
externalNativeBuild {
cmake {
cppFlags "-std=c++11 -frtti -fexceptions"
abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'mips', 'mips64'
}
}
sourceSets {
main {
jni.srcDirs = ['D:\\Android\\workplace\\calligraphyRecognize\\app\\src\\main\\jniLibs']
}
}
3. 将OpenCV-Android-SDK中的lib都复制到jniLibs下
其实我认为放在libs下也是可以的,不过这样放区分度会好一些。
点编译按钮,基本上能够实现项目的构建了。接下来就是实现native方法了。
在此实现的方式是在Java层上将Mat传入jni,操作后将其返回到Java层。
native是无法将Mat传过去的,其实现实际传入地址,通过指针指向该区域,对该区域进行操作,再返回其地址从而完成了整个操作。
3. 实现
首先Java层书写Native方法:
public static native void gThin(long matSrcAddr, long matDstAddr);
转向jni在native上实现:
/*
* 实现图像骨架化的Native方法:Rosenfeld细化算法
* @param src:原图片
* @return dst:细化后图片
*
* Rosenfeld细化算法描述如下:
* 1. 扫描所有像素,如果像素是北部边界点,且是8simple,但不是孤立点和端点,删除该像素。
* 2. 扫描所有像素,如果像素是南部边界点,且是8simple,但不是孤立点和端点,删除该像素。
* 3. 扫描所有像素,如果像素是东部边界点,且是8simple,但不是孤立点和端点,删除该像素。
* 4. 扫描所有像素,如果像素是西部边界点,且是8simple,但不是孤立点和端点,删除该像素。
*
* 执行完上面4个步骤后,就完成了一次迭代,我们重复执行上面的迭代过程,
* 直到图像中再也没有可以删除的点后,退出迭代循环。
*
*/
extern "C"
JNIEXPORT void JNICALL
Java_yangchengyu_shmtu_edu_cn_calligraphyrecognize_utils_ImageProcessUtils_gThin(JNIEnv *env,
jclass type,
jlong matSrcAddr,
jlong matDstAddr) {
Mat &src = *(Mat *) matSrcAddr;//通过指针获取Java层对应空间的原始图片mat
Mat &dst = *(Mat *) matDstAddr;//通过指针返回Java层的处理后图片mat
if (dst.data != src.data) {
src.copyTo(dst);
}
int i, j, n;
int width, height;
//方便处理8邻域,防止越界
width = src.cols - 1;
height = src.rows - 1;
int step = src.step;
int p2, p3, p4, p5, p6, p7, p8, p9;
uchar *img;
bool ifEnd;
cv::Mat tmpimg;
int dir[4] = {-step, step, 1, -1};
while (1) {
//分四个子迭代过程,分别对应北,南,东,西四个边界点的情况
ifEnd = false;
for (n = 0; n < 4; n++) {
dst.copyTo(tmpimg);
img = tmpimg.data;
for (i = 1; i < height; i++) {
img += step;
for (j = 1; j < width; j++) {
uchar *p = img + j;
//如果p点是背景点或者且为方向边界点,依次为北南东西,继续循环
if (p[0] == 0 || p[dir[n]] > 0) continue;
p2 = p[-step] > 0 ? 1 : 0;
p3 = p[-step + 1] > 0 ? 1 : 0;
p4 = p[1] > 0 ? 1 : 0;
p5 = p[step + 1] > 0 ? 1 : 0;
p6 = p[step] > 0 ? 1 : 0;
p7 = p[step - 1] > 0 ? 1 : 0;
p8 = p[-1] > 0 ? 1 : 0;
p9 = p[-step - 1] > 0 ? 1 : 0;
//8 simple判定
int is8simple = 1;
if (p2 == 0 && p6 == 0) {
if ((p9 == 1 || p8 == 1 || p7 == 1) && (p3 == 1 || p4 == 1 || p5 == 1))
is8simple = 0;
}
if (p4 == 0 && p8 == 0) {
if ((p9 == 1 || p2 == 1 || p3 == 1) && (p5 == 1 || p6 == 1 || p7 == 1))
is8simple = 0;
}
if (p8 == 0 && p2 == 0) {
if (p9 == 1 && (p3 == 1 || p4 == 1 || p5 == 1 || p6 == 1 || p7 == 1))
is8simple = 0;
}
if (p4 == 0 && p2 == 0) {
if (p3 == 1 && (p5 == 1 || p6 == 1 || p7 == 1 || p8 == 1 || p9 == 1))
is8simple = 0;
}
if (p8 == 0 && p6 == 0) {
if (p7 == 1 && (p3 == 9 || p2 == 1 || p3 == 1 || p4 == 1 || p5 == 1))
is8simple = 0;
}
if (p4 == 0 && p6 == 0) {
if (p5 == 1 && (p7 == 1 || p8 == 1 || p9 == 1 || p2 == 1 || p3 == 1))
is8simple = 0;
}
int adjsum;
adjsum = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9;
//判断是否是邻接点或孤立点,0,1分别对于那个孤立点和端点
if (adjsum != 1 && adjsum != 0 && is8simple == 1) {
//满足删除条件,设置当前像素为0
dst.at<uchar>(i, j) = 0;
ifEnd = true;
}
}
}
}
//已经没有可以细化的像素了,则退出迭代
if (!ifEnd) break;
}
}
最后编写完整代码实现
//Native层方法骨架化
public static Bitmap skeletonFromJNI(Bitmap bitmap) {
org.opencv.android.Utils.bitmapToMat(bitmap, sSrc);
Imgproc.cvtColor(sSrc, sSrc, Imgproc.COLOR_BGRA2GRAY);
Imgproc.threshold(sSrc, sSrc, 0, 255,
Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);
gThin(sSrc.getNativeObjAddr(), sDst.getNativeObjAddr());
Imgproc.threshold(sDst, sDst, 0, 255,
Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);
org.opencv.android.Utils.matToBitmap(sDst, bitmap);
sSrc.release();
sDst.release();
return bitmap;
}
可以看到这样处理后实现效果很不错,从而达到了目标。