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;
}
可以看到這樣處理後實現效果很不錯,從而達到了目標。