無論你是否從事計算機視覺相關的工做,瞭解這方面的內容老是好的,由於即便你如今的工做與 AI 無關,採用一些開放的 API 仍然有可能讓你的應用作得更好。好比,百度開發平臺就提供了許多 AI 相關的 API,像當下比較受歡迎的「白描」等應用,其實就是使用了百度的 API。因此,你也能夠考慮一下可否藉助一些語音和文字識別等功能來賦能本身的應用。java
由於咱們所作的計算機視覺的東西更多的是對圖片進行處理,這就涉及到 OpenCV 和 Tensorflow 在 Android 端的應用,以及相機和 Android 端的其餘圖片處理邏輯。這不可避免地要用到 JNI 和 NDK 當中的一些內容。所以,在本篇文章中,咱們想要討論的內容主要包括如下幾個方面的:android
其實以前的文章中咱們也提到過一些今天咱們想討論的內容。因此在這裏相關的技術底層的知識能帶過的就直接帶過。咱們會給出相關的技術文章的連接,若是感興趣的能夠到指定的文章下面查看更具體的知識。git
爲何要作圖像壓縮呢?由於太大的圖片上傳速度比較慢,會影響程序的用戶體驗;而過度的壓縮圖片會致使程序識別出來的效率比較低。識別的效率每提升 1 個百分點,標註團隊可能就要多標註幾萬張圖片。通過測試發現,把圖片的短邊控制在 1100 左右是最合適的,那麼此時咱們就須要制定一個本身的壓縮策略。github
這個在以前的文章中咱們已經討論過,而且對 Android 端 Bitmap 相關的壓縮的知識都作了介紹。您能夠到下面的文章下面瞭解下咱們是如何對圖片壓縮庫進行封裝的,以及 Android 中圖片壓縮的底層原理:算法
固然,上面的文章在介紹的這方面的東西的時候,基於的是咱們庫的第一個版本,那個版本能夠知足基本的功能。在後來的版本中,咱們又對本身的庫作了完善,增長了更多的 feature。這裏咱們主要介紹下新的框架相關的 API 以及後來咱們如何作了兼容性的設計,以在第一個版本的基礎之上進行了功能性的拓展。api
在實際的使用過程當中,咱們發現更多的時候你須要對 Bitmap 進行處理而不是 File. 在這個時候,第一個版本的庫就應用不上了。想了想,咱們但願可以對本身的庫進行拓展以支持更多的應用場景。這包括:數組
byte[]
做爲參數進行壓縮而不是先寫入到 File 中,而後對文件進行讀取和壓縮:這個是存在具體的應用場景,好比當你從相機當中獲取數據的時候,實際獲取到的是 byte[]
,在連續拍攝的狀況下,不斷寫入到文件再讀取並進行壓縮很是影響程序性能。最初,咱們但願可以像 Glide 那樣支持對自定義的數據結構進行壓縮,而後自定義圖片獲取的邏輯,然而考慮到時間和兼容的問題,直接放棄了這個想法,轉而採用更加簡單、直接的方式:緩存
get()
方法把壓縮的中間結果返回給調用者;asBitmap()
方法,轉換輸出參數類型爲 Bitmap 而不是 File 類型。所以,在後來的版本中,你能夠像下面這樣直接獲取到 Bitmap 的結果:安全
Bitmap result = Compress.with(this, bytes)
.setQuality(80)
.strategy(Strategies.compressor())
.setScaleMode(ScaleMode.SCALE_SMALLER) // 指定放縮的邊
.setMaxHeight(1100f)
.setMaxWidth(1100f)
.asBitmap() // 調用這個方法表示指望的返回類型是 Bitmap 而不是 File
.asFlowable()
.subscribe(b -> {
// do somthing
})
複製代碼
這裏關於 asBitmap()
方法的設計能夠簡單說明一下。
在第一個版本中,咱們採用的上圖中第一個圖的設計。在這裏兩種壓縮策略均繼承自 AbstractStrategy,其實上述的 strategy()
方法,你能夠把它理解成拐了一個彎,也就是它返回的具體的策略,你下面能調用的方法都侷限在具體的策略中。
在後來的設計中要在 asBitmap()
方法處返回一個具體的構建者繼續按照返回 Bitmap 的邏輯進行處理。此時咱們直接返回的是第二張圖中的 BitmapBuilder 對象,而 Abstrategy 則依然按照返回 File 類型的邏輯走。這樣咱們能夠輕易地在原來的基礎上,經過拐一個彎的形式把後續的構建邏輯轉移到了 BitmapBuilder 對象上面。同時,爲了達到代碼複用的目的,咱們引入了泛型的 RequestBuilder<T>
。這樣 AbstractStrategy 和 BitmapBuilder 只須要實現它,並指定各自的資源類型便可。又由於按照以前的邏輯,咱們一直在構建的都是 AbstractStrategy 對象,所以,咱們只須要把 AbstractStrategy 做爲參數傳入到具體的 RequestBuilder 裏面就能夠從它上面直接獲取 Bitmap 了。(Bitmap 是在之間串聯的「通用貨幣」。)這樣咱們既複用了大量的代碼又在兼容原始版本的基礎上進行了功能的拓展,妙極!
對於一個 ToC 的應用來講,用戶體驗直觀重要。按照咱們的業務場景,若是使用拍照識別的效率比人工操做的效率還要低的話,那麼人工智能彷佛就沒有存在的必要了。畢竟咱們的目標是提高別人的工做效率,因此在相機這塊就必須作到快速響應。
在項目的初期咱們使用的是 Google 開源的 CameraView. 然而在實際的應用過程當中,咱們逐漸發現這個庫存在大量很差的設計,影響了咱們程序的應用性能:
配圖是咱們使用 TraceView 對程序執行過程進行的性能分析
不必的數據結構構建,影響相機啓動速率:首先,當從相機的屬性當中讀取相機支持的尺寸的時候它會使用這些參數構建一個尺寸的寬高比到尺寸列表的哈希表結構。而後具體的運算的時候從這個哈希表中讀取尺寸再進行計算。這樣設計很很差!由於當須要計算尺寸的時候遍歷一遍尺寸列表可能並不會佔用太多的時間,而且構建的哈希表結構使用並不頻繁,而在相機啓動階段進行沒必要要的計算反而影響了相機的啓動速率。
打開相機的操做在主線程當中執行,影響界面響應速率:前提是界面可以快速響應用戶,即便打開的是一個黑色的等待界面也比按下沒有響應更容易接受。經過 TraceView 咱們發現相機 open()
的過程大概佔用了相機啓動速率的 25%。所以把這個方法的調用放在主線程中是不太合適的。
相機不支持視頻拍攝和預覽:這個庫是不支持相機的視頻拍攝和預覽的。畢竟做爲計算機視覺的一部分的實時處理也是很重要的一部分。就算當前的項目中沒有這方面的功能,咱們也應該考慮對這方面的功能進行支持。(這方面的內容基本就是 OpenGL + Camera)
因而乎,咱們本身開發的一款相機庫就誕生了。固然當初開發的一個緣由也是但願可以支持 OpenGL。只是時間太有限,暫時尚未太多時間去關注這些問題:
關於這個庫,我只是把它全部的邏輯實現了一遍,而且在個人手機上面調試沒有什麼問題。若是具體應用到生存環境當中還須要更多的測試和驗證。關於 Android 相機開發的知識,主要覆蓋 Camera1 和 Camera2 兩塊內容。一個方法的實現邏輯看懂了,其餘方法的實現與之相似,具體的內容能夠參考項目的源碼。由於本人當前時間和精力有限,因此暫時沒法詳細講解相機 API 的使用。
咱們能夠簡單歸納一下這份設計圖當中的一些內容:
三種主要設計模式:
門面模式:考慮到兼容 Camera1 和 Camera2 的問題,咱們須要對外提供統一的 API 調用,因此,咱們考慮了使用門面模式來作一個統一的封裝。這裏定義了 Camera1Manager 和 Camera2Manager 兩個實現,分別對應於兩種不一樣的相機 API. 它們統一繼承自 CameraManager 這個門面接口。這種設計模式的好處是對外是統一的,這樣結合具體的工廠+策略模式,咱們可讓用戶在 Camera1 和 Camera2 之間自由選擇。
策略模式+工廠模式:由於考慮到各類不一樣應用場景的兼容,咱們但願可以用戶提供最大的自由度。因此,咱們採用了策略的方式,對外提供了接口給用戶來計算最終想要獲得的相機尺寸等參數。因此這裏咱們定義了一個名爲 ConfigurationProvider
的類,它是一個單例的類,除了負責獲取相機參數的計算策略,同時肩負着內存緩存的責任。這樣對於不少參數的計算,包括預覽尺寸、照片尺寸、視頻尺寸等可讓用戶自由來指定具體的大小。
三個主要的優化點:
內存緩存優化:實際上做爲相機屬性的相機所支持尺寸等信息是不變的,使用內存緩存緩存這些數據以後下次就無需再次獲取並進行處理,這樣能夠在下次相機啓動的時候顯著提高程序響應的速率。
延遲初始化,不使用不計算:爲了提高程序的響應速率,咱們甚至對數字的計算也進行了優化,固然這個優化點可能效果沒有那麼明顯,可是若是你願意精益求精的話,這也能夠看成一個優化點。目前程序裏面仍是使用了浮點數進行計算,在早期對於做爲哈希表映射的鍵的字段,咱們甚至直接使用移位預算。固然這種優化的效果還要取決於總體的數據量,而且數據量越大的時候優化效果越明顯。
異步線程優化:在早期的版本中,咱們使用的是私有鎖進行線程優化。由於要把線程的 open()
和設置參數放在兩個線程當中進行,所以不可避免地要遇到線程安全問題。而所謂的私有鎖其實就相似於 Collections.syncXXX()
所返回的同步容器。其實就是對容器的每一個方法進行加鎖。這樣雖然能夠解決問題,可是程序的結構很差看。因此,後來的版本中,咱們直接使用 HandlerThread 來進行異步的調用。所謂的 HandlerThread,顧名思義,就是 Handler 和 Thread 的結合。
(更多的內容能夠參考:Android 相機庫開發實踐)
在以前咱們想在在 Java 或者 Android 中調用 C++ 代碼是比較複雜的。這須要進行動態或者靜態的註冊。對於靜態註冊的方式,你須要一步一步地進行編譯;對於動態註冊的方式,你須要把方法一個個地在 native 層進行註冊。不事後來有了 CMake 以後一切都變得簡單了。固然對於 CMake,若是作過 native 的同窗確定不會陌生。對於通常應用層開發的同窗,其實也能夠了解下它。由於,有了它以後你能夠很容易地把你的一部分實現邏輯放在 native 層裏,而 native 層相對於 Java 層比較安全,並且藉助 C++ 和 NDK 你能夠作出更多有趣的東西。
要在 Android 端使用 CMake 而是很簡單的,你須要首先在 AS 裏面安裝下相關的 SDK 工具:
而後,你須要在 Gradle 裏面作簡單的配置:
固然,雖然咱們這樣將其來容易,可是進行配置的時候可能須要不少的相關的專業知識。
這裏面會配置到一個 CMake path,它就是指向的 CMake 的配置文件 CMakeLists.txt
。一般咱們程序中要用到的一些三方的 native 庫就須要在這個地方進行配置。好比下面的就是以前項目裏面的 CMake 的配置。這裏面配置了一些 OpenCV 的庫以及咱們本身的代碼所在的位置,而且引用了 NDK 裏面的一些相關的庫:
# 設置要求的 CMake 的最低版本
cmake_minimum_required(VERSION 3.4.1)
# 指定頭文件的目錄
include_directories(opencv/jni/include
src/main/cpp/include
../../common)
add_library(opencv_calib3d STATIC IMPORTED)
add_library(opencv_core STATIC IMPORTED)
# ....
#if(EXISTS ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libtbb.a)
# add_library(tbb STATIC IMPORTED)
#endif()
set_target_properties(opencv_calib3d PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_calib3d.a)
set_target_properties(opencv_core PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_core.a)
set_target_properties(opencv_features2d PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_features2d.a)
# ....
add_library(everlens
SHARED
src/main/cpp/img_proc.cpp
src/main/cpp/img_cropper.cpp
src/main/cpp/android_utils.cpp
../../common/EdgeFinder.cpp
../../common/ImageFilter.cpp)
find_library(log-lib
log)
find_library(jnigraphics-lib
jnigraphics)
if(EXISTS ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libtbb.a)
target_link_libraries(
my_library
opencv_stitching
opencv_features2d
# ....
${log-lib}
${jnigraphics-lib})
else()
target_link_libraries(
my_library
opencv_stitching
opencv_features2d
# ....
${log-lib}
${jnigraphics-lib})
endif()
複製代碼
對於 CMake 的一些指令,以前也進行過一些總結,而且對指定了官方文檔的地址。若是想要了解的話,能夠到文章下面瞭解更多的內容:
使用 CMake 的好處主要是 AS 支持得比較好:
能夠根據 native 層代碼到 Java 層代碼之間的關係,鼠標左鍵 + Ctrl 便可直接完成 native 層方法和 Java 層方法之間的跳轉;
無需進行繁瑣的動態註冊和靜態註冊,只須要在 CMake 和 Gradle 當中進行配置,能夠把注意力更多地放在本身的代碼的邏輯實現上。
固然,就算使用了 CMake 有時候仍是須要了解一些 JNI 中動態註冊的內容,由於有時候當你在 native 層中從 Java 層傳入的對象上面獲取信息的時候仍是須要進行動態註冊。好比,
#include <jni.h>
#include <string>
#include <android_utils.h>
// 定義一個結構體及實例 gPointInfo
static struct {
jclass jClassPoint;
jmethodID jMethodInit;
jfieldID jFieldIDX;
jfieldID jFieldIDY;
} gPointInfo;
// 初始化 Class 信息,注意下映射關係是如何表達的,其實就相似於反編譯以後的註釋
static void initClassInfo(JNIEnv *env) {
gPointInfo.jClassPoint = reinterpret_cast<jclass>(env -> NewGlobalRef(env -> FindClass("android/graphics/Point")));
gPointInfo.jMethodInit = env -> GetMethodID(gPointInfo.jClassPoint, "<init>", "(II)V");
gPointInfo.jFieldIDX = env -> GetFieldID(gPointInfo.jClassPoint, "x", "I");
gPointInfo.jFieldIDY = env -> GetFieldID(gPointInfo.jClassPoint, "y", "I");
}
// 動態註冊,在這裏初始化
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
return JNI_FALSE;
}
initClassInfo(env);
return JNI_VERSION_1_4;
}
複製代碼
(更多的內容能夠參考:在 Android 中使用 JNI 的總結)
固然,不引用 OpenCV 的 C++ 庫,直接使用別人封裝好的 Java 庫也是能夠的,這取決於具體的應用場景。好比,若是你不須要實現特別複雜的功能,只須要簡單的圖像處理便可,那麼別人包裝過的 Java 庫已經能夠徹底知足你的需求。但若是你像咱們同樣,自己須要包裝和編譯來自算法同窗的 C++ 算法,甚至還須要使用 OpenCV 的拓展庫,那麼使用 Java 包裝後的庫可能沒法知足你的需求。
下面是 OpenCV 及其拓展庫的 Github 地址:
有了這些庫你仍是沒法直接將其應用到程序當中的。由於上述項目獲得的是 OpenCV 的源碼,主要是源代碼以及一些頭文件,還須要對它們進行編譯而後再應用到本身的項目當中。
Build OpenCV 3.3 Android SDK on Mac OSX
固然也有一些已經編譯完成的 OpenCV 及其拓展庫,咱們能夠在 CMake 中配置以後直接將其引用到咱們的項目中:
opencv3-android-sdk-with-contrib
因此最終項目的結構以下:
左邊圈出的部分是 OpenCV 及 CMake 的一些配置,右邊是封裝以後的 Java 方法。
OpenCV 能夠用來處理作不少圖片處理的工做,不少工做是使用 Android 原生的 Bitmap 沒法完成的。好比,圖片不規則裁剪以後的透視變換、灰度化處理等。其實,不論你在 native 層如何對圖片進行處理,在 Android 當中,對 native 層的輸入和 native 層的輸出都是 Bitmap. 而 OpenCV::Mat
就像是 native 層圖片處理的通用貨幣。因此,一個完整的圖片處理的流程大體是:
將 Java 層的 Bitmap 轉換成 native 層的 Mat 你可使用下面的方法:
#include <jni.h>
#include <android/bitmap.h>
#include "android_utils.h"
void bitmap_to_mat(JNIEnv *env, jobject &srcBitmap, Mat &srcMat) {
void *srcPixels = 0;
AndroidBitmapInfo srcBitmapInfo;
try {
// 調用 AndroidBitmap 中的方法獲取 bitmap 信息
AndroidBitmap_getInfo(env, srcBitmap, &srcBitmapInfo);
AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels);
uint32_t srcHeight = srcBitmapInfo.height;
uint32_t srcWidth = srcBitmapInfo.width;
srcMat.create(srcHeight, srcWidth, CV_8UC4);
// 根據 bitmap 的格式構建不一樣通道的 Mat
if (srcBitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
Mat tmp(srcHeight, srcWidth, CV_8UC4, srcPixels);
tmp.copyTo(srcMat);
cvtColor(tmp, srcMat, COLOR_RGBA2RGB);
} else {
Mat tmp = Mat(srcHeight, srcWidth, CV_8UC2, srcPixels);
cvtColor(tmp, srcMat, COLOR_BGR5652RGBA);
}
AndroidBitmap_unlockPixels(env, srcBitmap);
return;
} catch (cv::Exception &e) {
AndroidBitmap_unlockPixels(env, srcBitmap);
// 構建一個 Java 層的異常並將其拋出
jclass je = env->FindClass("java/lang/Exception");
env -> ThrowNew(je, e.what());
return;
} catch (...) {
AndroidBitmap_unlockPixels(env, srcBitmap);
jclass je = env->FindClass("java/lang/Exception");
env -> ThrowNew(je, "unknown");
return;
}
}
複製代碼
這裏主要是先從 Bitmap 中獲取圖片具體的信息,這裏調用了 NDK 裏面的圖像相關的一些方法。而後利用獲得的圖片尺寸信息和顏色信息構建 OpenCV 裏面的 Mat. Mat 就相似於 MATLAB 裏面的矩陣,它包含了圖像的像素等信息,而且也提供了相似於 eye()
, zeros()
等相似的方法用來構建特殊的矩陣。
將 Bitmap 轉換成 Mat 以後就是如何使用它們了。下面是一份用來對圖片進行裁剪和透視變換的算法:
// 將 Java 層的頂點轉換成 native 層的 Point 對象
static std::vector<Point> pointsToNative(JNIEnv *env, jobjectArray points_) {
int arrayLength = env->GetArrayLength(points_);
std::vector<Point> result;
for(int i = 0; i < arrayLength; i++) {
jobject point_ = env -> GetObjectArrayElement(points_, i);
int pX = env -> GetIntField(point_, gPointInfo.jFieldIDX);
int pY = env -> GetIntField(point_, gPointInfo.jFieldIDY);
result.push_back(Point(pX, pY));
}
return result;
}
// 裁剪而且透視變化
extern "C" JNIEXPORT void JNICALL Java_xxxx_MyCropper_nativeCrop(JNIEnv *env, jclass type, jobject srcBitmap, jobjectArray points_, jobject outBitmap) {
std::vector<Point> points = pointsToNative(env, points_);
if (points.size() != 4) {
return;
}
// 取出四個頂點
Point leftTop = points[0], rightTop = points[1], rightBottom = points[2], leftBottom = points[3];
// 獲取源圖和結果圖對應的 Mat
Mat srcBitmapMat, dstBitmapMat;
bitmap_to_mat(env, srcBitmap, srcBitmapMat);
AndroidBitmapInfo outBitmapInfo;
AndroidBitmap_getInfo(env, outBitmap, &outBitmapInfo);
int newHeight = outBitmapInfo.height, newWidth = outBitmapInfo.width;
dstBitmapMat = Mat::zeros(newHeight, newWidth, srcBitmapMat.type());
// 將圖片的頂點放進集合當中,用來調用透視的方法
std::vector<Point2f> srcTriangle, dstTriangle;
srcTriangle.push_back(Point2f(leftTop.x, leftTop.y));
srcTriangle.push_back(Point2f(rightTop.x, rightTop.y));
srcTriangle.push_back(Point2f(leftBottom.x, leftBottom.y));
srcTriangle.push_back(Point2f(rightBottom.x, rightBottom.y));
dstTriangle.push_back(Point2f(0, 0));
dstTriangle.push_back(Point2f(newWidth, 0));
dstTriangle.push_back(Point2f(0, newHeight));
dstTriangle.push_back(Point2f(newWidth, newHeight));
// 獲取一個映射的轉換矩陣
Mat transform = getPerspectiveTransform(srcTriangle, dstTriangle);
warpPerspective(srcBitmapMat, dstBitmapMat, transform, dstBitmapMat.size());
// 將 Mat 轉換成 Bitmap 輸出到 Java 層
mat_to_bitmap(env, dstBitmapMat, outBitmap);
}
複製代碼
算法最終的輸出結果:
在以前對圖片的邊緣進行檢測的時候,由於發現 OpenCV 算法效果不太理性,因此後來選擇使用 TensorFlow 對圖片進行邊緣檢測。這就涉及到在 Android 端集成 Tensorflow Lite。前段時間也看到愛奇藝的 SmartVR 的介紹。藉助一些官方的資料,在 Android 端使用 TF 並不難。在 Tensorflow 的開源倉庫中已經有一些 Sample 可供參考:
作邊緣檢測固然有些大材小用的味道,可是對於咱們 Android 開發者來講,借這個機會了解如何在 Android 端集成一些 Tensorflow 也能夠拓展一下。畢竟這種東西屬於當下比較熱門的東西,說不定哪天心血來潮本身訓練一個模型呢 :)
在 Android 端引入 Tensorflow 並不複雜,只須要添加相關的倉庫以及依賴:
allprojects {
repositories {
jcenter()
maven { url 'https://google.bintray.com/tensorflow' }
}
}
dependencies {
// ...
// tensorflow
api 'org.tensorflow:tensorflow-lite:0.0.0-nightly'
}
複製代碼
困難的地方在於如何對訓練的模型的輸入和輸出進行處理。由於所謂的模型,你能夠將其理解成鍛煉出來的一個函數,你只須要按照要求的輸入,它就能夠按照固定的格式給你返回一個輸出。因此,具體的輸入和輸出是什麼樣的還要取決於鍛鍊模型的同窗。
在咱們以前開發的時候,最初是訓練模型的同窗使用 Python 代碼調用的模型。使用 Python 雖然代碼簡潔,可是對於客戶端開發簡直就是噩夢。由於像 NumPy 和 Pillow 這種函數庫,一行代碼的任務,你可能要「翻譯」好久。後來,咱們使用的是 C++ + OpenCV
的形式。對於 iOS 開發,由於他們可使用 Object-C 與 C++ 混編,因此比較輕鬆。對於 Android 開發則須要作一些處理。
下面是加載模型以及在調用 Tensorflow 以前在 Java 層所作的一些處理:
public class TFManager {
private static TFManager instance;
private static final float IMAGE_MEAN = 128.0f;
private static final float IMAGE_STD = 128.0f;
private Interpreter interpreter;
private int[] intValues;
private ByteBuffer imgData;
private int inputSize = 256;
public static TFManager create(Context context) { // DCL
if (instance == null) {
synchronized (TFManager.class) {
if (instance == null) {
instance = new TFManager(context);
}
}
}
return instance;
}
// 從 Assets 中加載模型,用來初始化 TF
private static MappedByteBuffer loadModelFile(AssetManager assets, String modelFilename) throws IOException {
AssetFileDescriptor fileDescriptor = assets.openFd(modelFilename);
FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
FileChannel fileChannel = inputStream.getChannel();
long startOffset = fileDescriptor.getStartOffset();
long declaredLength = fileDescriptor.getDeclaredLength();
return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
}
// 初始化 TF
private TFManager(Context context) {
try {
interpreter = new Interpreter(loadModelFile(context.getAssets(), "Model.tflite"));
interpreter.setNumThreads(1);
interpreter.resizeInput(0, new int[]{1, 256, 256, 3});
intValues = new int[inputSize * inputSize];
imgData = ByteBuffer.allocateDirect(inputSize * inputSize * 4 * 3);
imgData.order(ByteOrder.nativeOrder());
} catch (IOException e) {
e.printStackTrace();
}
}
// 邊緣識別
public EdgePoint[] recognize(Bitmap bitmap) {
long timeStart = System.currentTimeMillis();
imgData.rewind();
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true);
scaledBitmap.getPixels(intValues, 0, scaledBitmap.getWidth(), 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight());
for (int i = 0; i < inputSize; ++i) {
for (int j = 0; j < inputSize; ++j) {
int pixelValue = intValues[i * inputSize + j];
// 取 RBG 作歸一化處理,結果在 -1 到 1 之間
imgData.putFloat((((pixelValue >> 16) & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // R
imgData.putFloat((((pixelValue >> 8) & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // G
imgData.putFloat(((pixelValue & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // B
}
}
LogUtils.d("----------TFManager prepare imgData cost : " + (System.currentTimeMillis() - timeStart));
timeStart = System.currentTimeMillis();
Map<Integer, Object> outputMap = new HashMap<>();
outputMap.put(0, new float[1][256][256][5]);
Object[] inputArray = {imgData};
// 調用 TF 進行識別
interpreter.runForMultipleInputsOutputs(inputArray, outputMap);
// 對識別的結果進行處理,主要是對圖片的像素進行處理
float[][][][] arr = (float[][][][]) outputMap.get(0);
int[][] colors = new int[5][256 * 256];
for (int i=0; i<5; i++) {
for (int j=0; j<256; j++) {
for (int k=0; k<256; k++) {
colors[i][j*256 + k] = (int) (arr[0][j][k][i] * 256);
}
}
}
LogUtils.d("----------TFManager handle TF result cost : " + (System.currentTimeMillis() - timeStart));
timeStart = System.currentTimeMillis();
// 將獲得的圖片像素按照固定的格式交給 native 層繼續進行邊緣識別
EdgePoint[] points = ImgProc.findEdges(bitmap, colors);
LogUtils.d("----------TFManager" + Arrays.toString(points));
LogUtils.d("----------TFManager find edges cost : " + (System.currentTimeMillis() - timeStart));
return points;
}
}
複製代碼
這裏程序的主要執行流程是:
從 Assets 的模型文件中獲取打開輸入流,而後從輸入流中打開一個管道,這裏用到了 NIO 中的一些類。而後,從管道中獲取一個字節緩存區。文件讀寫的時候管道直接與緩衝區進行交互。除了做爲一個緩存區,這個緩存區還具備內存映射的功能。相似於 mmap 吧,主要是爲了提高文件讀寫的效率。
初始化並配置 Tensorflow,上面的一些參數用來設置線程數量等信息,比較簡單。後面等一些參數主要用來按照模型等要求對 TF 進行調整。好比,咱們使用模型來判斷圖片的頂點的時候使用的是隻包含 RGB 三個緯度的 256 * 256 的圖片。因此,這裏使用了下面幾行代碼來進行設置:
// inputSize = 256;
interpreter.resizeInput(0, new int[]{1, 256, 256, 3});
intValues = new int[inputSize * inputSize];
// 256 * 256 的圖片,3 個緯度,四張圖
imgData = ByteBuffer.allocateDirect(inputSize * inputSize * 4 * 3);
複製代碼
對要識別對圖片進行處理。這裏須要先對圖片進行放縮,將其控制到 256 * 256 的大小。而後,使用 Bitmap 的方法獲取圖片的像素。下面的幾行代碼是對圖片對 RBG 三種色彩提取,並分別對其進行歸一化處理。處理以後對結果統一寫入到 imgData 當中,做爲模型對輸入。
按照模型對輸出對文件的格式構建一個 Java 對象做爲模型的輸出參數。調用模型的方法進行識別。
對模型對輸出結果進行處理。根據咱們上述定義對模型輸出 new float[1][256][256][5]
,這裏實際對含義是 256 * 256 的 5 張圖片。由於模型輸出的數據並非原始的像素信息,因此須要乘以 256 來獲得真正的圖片的像素。最後就是使用這些像素以及 Bitmap 的方法來獲得最終的 Bitmap.
上面調用完了模型可是整個流程尚未結束。由於只是調用模型獲得了五張識別以後的圖片。這五張圖片就是隻留下了圖片邊緣的邊框,因此想要獲得圖片的頂點還須要繼續對這五張圖進行處理。這部分須要一些算法,雖然在 Java 層去判斷也是能夠的,可是在 native 層,藉助 OpenCV 的一些庫可使整個過程更加簡單。所以,這裏又要涉及一個 JNI 調用:
extern "C"
JNIEXPORT void JNICALL Java_com_xxx_ImgProc_nativeFindEdges(JNIEnv *env, jclass type, jintArray mask1_, jintArray mask2_, jintArray mask3_, jintArray mask4_, jintArray mask5_, jobject origin, jobjectArray points) {
// 從 Java 中傳入的數組元素
jint *mask1 = env->GetIntArrayElements(mask1_, NULL);
jint *mask2 = env->GetIntArrayElements(mask2_, NULL);
jint *mask3 = env->GetIntArrayElements(mask3_, NULL);
jint *mask4 = env->GetIntArrayElements(mask4_, NULL);
jint *mask5 = env->GetIntArrayElements(mask5_, NULL);
// 原圖轉換成 Native 層的 mat
Mat originMat;
bitmap_to_mat(env, origin, originMat);
// 構建一個集合
std::vector<jint*> jints;
jints.push_back(mask1);
jints.push_back(mask2);
jints.push_back(mask3);
jints.push_back(mask4);
jints.push_back(mask5);
// 從像素點中獲得對應的 Mat 並將其放到一個集合當中
std::vector<cv::Mat> masks;
for (int k = 0; k < 5; ++k) {
Mat mask(256, 256, CV_8UC1);
for (int i = 0; i < 256; ++i) {
for (int j = 0; j < 256; ++j) {
mask.at<uint8_t>(i, j) = (char)(*(jints[k] + i * 256 + j));
}
}
masks.push_back(mask);
}
try {
// 調用算法進行邊緣檢測
EdgeFinder finder = ImageEngine::EdgeFinder(
originMat, masks[0], masks[1], masks[2], masks[3], masks[4]);
vector<cv::Point2d> retPoints = finder.FindBorderCrossPoint();
// 將獲得的「點」轉換成 Java 層的對象
jclass class_point = env->FindClass("com/xxx/EdgePoint");
jmethodID method_point = env->GetMethodID(class_point, "<init>", "(FF)V");
// 將頂點組成一個 Java 數組返回
for (int i = 0; i < 4; ++i) {
jobject point = env->NewObject(class_point, method_point, retPoints[i].x, retPoints[i].y);
env->SetObjectArrayElement(points, i, point);
}
} catch (cv::Exception &e) {
jclass je = env->FindClass("java/lang/Exception");
env -> ThrowNew(je, e.what());
return;
} catch (...) {
jclass je = env->FindClass("java/lang/Exception");
env -> ThrowNew(je, "unknown");
return;
}
// 釋放資源
env->ReleaseIntArrayElements(mask1_, mask1, 0);
env->ReleaseIntArrayElements(mask2_, mask2, 0);
env->ReleaseIntArrayElements(mask3_, mask3, 0);
env->ReleaseIntArrayElements(mask4_, mask4, 0);
env->ReleaseIntArrayElements(mask5_, mask5, 0);
}
複製代碼
這裏的主要邏輯是把以前獲得的像素以及原始圖片的 Bitmap 統一傳入到 native 層,而後這些像素中獲得 OpenCV 對應的 Mat,再一塊兒做爲算法的參數調用算法來獲得頂點信息。最後把獲得的頂點信息映射成 Java 層的類,並將其放在數組中返回便可。
從上面的流程中也能夠看出,整個調用流程實際上進行了屢次的 JNI 調用:
JNI 調用的時候須要進行額外的轉換操做,須要在函數開始的時候把 Java 層的對象轉換成 native 層的對象,在算法調用完畢以後再將 native 層的對象轉換成 Java 層的對象。這是咱們之後能夠優化的一個地方。
以上就是計算機視覺在 Android 中的應用,主要涉及 JNI 的一些內容,以及 OpenCV 和 Tensorflow 的一些應用。再這以前介紹了圖片壓縮和相機庫的一些封裝,若是你的程序中須要一些圖片處理的功能的話,我想這些東西確定是對你有用的 :D
關注做者獲取更多知識:
更多知識請參考 Github, Android-notes。