源碼解析:顏色直方圖與工程實現(xiàn))
簡介這是基于Android Studio開發(fā)的花卉識別系統(tǒng)完整源碼項目面向有一定Android基礎或?qū)σ苿佣藞D像識別感興趣的開發(fā)者也可作為課程設計與畢業(yè)設計選題參考。項目采用Java實現(xiàn)工程結構規(guī)范涵蓋AndroidManifest配置、XML布局、圖片資源、核心Java邏輯及Gradle構建配置壓縮包僅6.84MB共63個文件其中包含6個Java源碼、17個XML文件、15張PNG圖片以及Gradle腳本、依賴JAR、properties配置和README說明從工程搭建到識別流程都清晰可循。系統(tǒng)針對圖像處理算法與用戶交互界面進行了優(yōu)化能夠在移動設備上較流暢地完成花卉圖像識別任務體現(xiàn)了移動計算與植物學領域結合的典型應用。目前已有49人學習下載對初學者來說既可借助完整源碼梳理Android應用的圖像識別實現(xiàn)思路也可直接導入Android Studio進行二次開發(fā)與功能拓展壓縮包內(nèi)的備份文件還保留了配置快照便于對比與糾錯。資源來自網(wǎng)絡分享僅用于學習交流。1. 移動端識別一朵花先從工程結構判斷識別方案拿到一套“基于 Android Studio 的花卉識別系統(tǒng)源碼”我先看它的libs目錄而不是 Activity。源碼里放著android-smart-image-view-1.0.0.jar說明它沒有引入 TensorFlow Lite 或 MNN 這類深度學習推理庫識別方案大概率是傳統(tǒng)圖像特征顏色直方圖、邊緣統(tǒng)計或者感知哈希。實際讀下來主流程是拍照或選圖、壓縮、取像素、與內(nèi)置樣本比對、返回最接近的花名。這種方式的好處是 APK 小、邏輯透明、代碼容易讀懂壞處是識別率受光照和背景干擾影響大。對打算研究 Android 圖像處理的開發(fā)者來說它比調(diào)用云 API 更接近原理也方便改造成自己的離線識別工具。2. Gradle 配置與源碼目錄讓 Android Studio 工程先跑起來2.1 從 build.gradle 看依賴與版本策略用 Android Studio 打開這個工程時第一件觸發(fā)的事就是 Gradle Sync。項目里gradle/wrapper目錄保存著 Wrapper 版本而build.gradle決定編譯依賴。以同類型項目的app/build.gradle為例結構大致是下面這個樣子代碼里的版本號只作講解用要以你手里源碼的真實值為準// app/build.gradle 典型配置 apply plugin: com.android.application android { compileSdkVersion 28 defaultConfig { applicationId com.example.flower minSdkVersion 19 targetSdkVersion 28 versionCode 1 versionName 1.0 } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile(proguard-android.txt), proguard-rules.pro } } } dependencies { implementation fileTree(dir: libs, include: [*.jar]) implementation com.android.support:appcompat-v7:28.0.0 }這里的compileSdkVersion是編譯時使用的 API Level它不需要手機系統(tǒng)達到這個數(shù)值只是告訴編譯器可以調(diào)用哪些接口。minSdkVersion 19表示最低支持 Android 4.4targetSdkVersion影響系統(tǒng)兼容模式比如運行時權限策略。implementation fileTree會把libs目錄下所有.jar文件加入編譯路徑源碼里那個android-smart-image-view-1.0.0.jar就是這樣被引入的。Gradle 版本是經(jīng)常出問題的環(huán)節(jié)。打開工程前先檢查gradle-wrapper.properties里的distributionUrl是否和當前 Android Studio 內(nèi)置版本接近。老項目用 Gradle 4.x新 Studio 自帶 8.x直接 Sync 大概率報錯。常見做法是改distributionUrl同時把項目根目錄build.gradle里的com.android.tools.build:gradle版本同步調(diào)整。對于單模塊項目升級成本并不高。2.2 app/src/main 下的職責劃分梳理源碼時我習慣先列一個目錄映射表知道每個目錄負責什么再讀代碼。這個項目的app/src/main下大概包含這些內(nèi)容路徑/文件職責java/com/example/flower/MainActivity.java識別入口處理拍照、相冊選擇、結果展示java/com/example/flower/ImageHelper.java圖像縮放、像素讀取、顏色直方圖計算java/com/example/flower/FlowerMatcher.java保存樣本特征執(zhí)行距離比對并返回花名res/layout/activity_main.xml布局預覽圖、識別按鈕、結果文本res/drawable/默認圖、按鈕背景等資源libs/android-smart-image-view-1.0.0.jar第三方圖片異步加載庫可以看到識別邏輯被拆成了幾個類而不是全堆在 Activity 里。這個分層帶來的好處是以后想把顏色直方圖替換成感知哈希只需要改ImageHelper和FlowerMatcher。MainActivity只負責獲取圖像和展示結果不用關心特征怎么算。源碼根目錄還有幾個容易讓人誤會的文件。settings.gradle、.gitignore、gradlew是工程骨架必須保留帶有.zbak后綴的比如gradle-wrapper.properties.zbak、settings.gradle.zbak是別人還原環(huán)境時留下的備份。release/output.json只是構建輸出記錄不影響運行。導入工程時如果遇到“SDK location not found”或“Failed to find target with hash string”打開 Android SDK Manager 安裝對應 Platform或在local.properties里指定 SDK 路徑。2.3 老工程遇到 AGP 版本不兼容怎么辦檢索熱詞里有一條 “android studio build 出現(xiàn)tag number over 30 is not supported”這是老工程配新 AGP 的典型問題。低版本圖片庫在資源 ID 上用了高位 tag新 AAPT 會限制資源 tag 數(shù)。解決路線有兩個升級第三方庫或關閉 Android Gradle Plugin 的嚴格資源檢查。對于這個花卉識別項目只用了一個較老的 SmartImageView它沒有復雜資源引用一般不會觸發(fā)該問題。萬一遇到優(yōu)先升級 SmartImageView 或換用 Glide不要依賴已被新版本移除的android.enableAapt2開關。注意Android Gradle Plugin 7.0 以后移除了很多舊版開關排錯時應先看當前 AGP 版本對應的官方遷移說明再決定改代碼還是改配置。3. 圖像識別主鏈路拍照、縮放、直方圖與相似度匹配3.1 識別入口與圖像獲取花卉識別從一張照片開始。MainActivity里的入口通常是點擊“選擇照片”按鈕跳轉(zhuǎn)到系統(tǒng)相冊或相機返回的數(shù)據(jù)在onActivityResult里接收。核心代碼如下private static final int REQUEST_PICK_IMAGE 100; Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode REQUEST_PICK_IMAGE resultCode RESULT_OK data ! null) { Uri imageUri data.getData(); try { Bitmap original MediaStore.Images.Media.getBitmap( getContentResolver(), imageUri); String result recognize(original); resultView.setText(識別結果 result); } catch (IOException e) { Log.e(TAG, 讀取圖片失敗, e); } } }這段代碼的問題很典型MediaStore.Images.Media.getBitmap()會直接解碼原圖?,F(xiàn)代手機一張照片少說 3000x4000 像素ARGB_8888 格式下內(nèi)存占用接近 48MB很可能觸發(fā) OOM。所以拿到Bitmap后不能直接做特征提取必須先進壓縮流程。recognize方法內(nèi)部會調(diào)用圖像縮放和特征提取我一般會把這段邏輯封裝成單獨方法方便放到異步線程執(zhí)行。3.2 圖像壓縮與 Bitmap 內(nèi)存管理壓縮的目的不是減小文件體積而是減少參與計算的像素數(shù)量。顏色直方圖只需要足夠的顏色分布信息3000 像素寬的照片和 256 像素寬的照片在統(tǒng)計結果上沒有本質(zhì)差異但計算量差距是上百倍。推薦等比縮放把最長邊限制到 256 或 512 像素private Bitmap scaleBitmap(Bitmap src, int maxSize) { int w src.getWidth(); int h src.getHeight(); float ratio Math.min((float) maxSize / w, (float) maxSize / h); if (ratio 1f) { w (int) (w * ratio); h (int) (h * ratio); src Bitmap.createScaledBitmap(src, w, h, true); } return src; }ratio取maxSize / w和maxSize / h的較小值保證縮放后圖像不會變形也不會有一側超出限制。如果原圖已經(jīng)小于maxSizeratio大于等于 1此時不縮放。注意createScaledBitmap()即使縮放比例相同也可能返回新對象如果原圖不再使用最好在原圖使用完后調(diào)用recycle()提前釋放。Android 8.0 以后系統(tǒng)會自動管理一部分原生內(nèi)存recycle()主要用來主動釋放不是必須。處理完縮略圖后用bitmap.getPixels()把位圖轉(zhuǎn)換成int[]數(shù)組方便逐像素讀取 RGB。這里stride參數(shù)直接傳寬度即可避免某些設備上出現(xiàn)像素排列偏移。3.3 顏色直方圖特征提取與相似度計算這個項目適合采用顏色直方圖作為特征。直方圖把圖像的顏色分布壓縮成一組數(shù)字忽略空間位置因此對花瓣旋轉(zhuǎn)、小范圍裁剪不敏感。實現(xiàn)時先統(tǒng)計 RGB 三個通道的分布再拼成一個一維數(shù)組public int[] extractHistogram(Bitmap bitmap, int bins) { int[] histogram new int[bins * 3]; int w bitmap.getWidth(); int h bitmap.getHeight(); int[] pixels new int[w * h]; bitmap.getPixels(pixels, 0, w, 0, 0, w, h); for (int i 0; i pixels.length; i) { int pixel pixels[i]; int r (pixel 16) 0xFF; int g (pixel 8) 0xFF; int b pixel 0xFF; histogram[r * bins / 256]; histogram[bins (g * bins / 256)]; histogram[bins * 2 (b * bins / 256)]; } return histogram; }bins表示每個顏色通道分成多少桶常見值為 8 或 16。bins16時直方圖數(shù)組長度是 48信息量適中bins太小會丟失區(qū)分度太大會讓距離值對噪聲敏感。為了減少光照影響后續(xù)可以轉(zhuǎn)成 HSV 色域再提取 H 通道直方圖但這會增加代碼復雜度。入門版里 RGB 直方圖已經(jīng)能處理背景干凈的單朵花照片。提取完特征下一步是距離計算。常見做法是歐氏距離public double euclideanDistance(int[] a, int[] b) { double sum 0; for (int i 0; i a.length; i) { double diff a[i] - b[i]; sum diff * diff; } return Math.sqrt(sum); }直方圖是離散計數(shù)直接相減沒有歸一化距離會隨像素總數(shù)變化。因為所有待識別圖像都縮放到接近相同的尺寸像素總數(shù)差異不大不歸一化也能得到相對穩(wěn)定的排序。如果你把maxSize改成可配置參數(shù)建議對直方圖做歸一化即每個桶值除以總像素數(shù)讓距離比較不受圖片尺寸影響。匹配時遍歷所有樣本選出距離最小的一項public String match(int[] feature) { String bestName 未知; double bestDistance Double.MAX_VALUE; for (FlowerSample sample : sampleList) { double d euclideanDistance(feature, sample.feature); if (d bestDistance) { bestDistance d; bestName sample.name; } } return bestName; }這里的FlowerSample對象在 App 啟動時預加載內(nèi)部保存花名和提前計算好的特征數(shù)組。為了調(diào)試可以把bestDistance也返回用來判斷當前識別結果是否可信。特征提取的關鍵參數(shù)可以根據(jù)實際場景調(diào)整參數(shù)推薦值影響maxSize256越大特征越精細計算越慢bins16直方圖桶數(shù)影響區(qū)分度縮放比例保持原始寬高比避免花形扭曲4. 界面交互與圖片展示異步任務和 SmartImageView 的配合4.1 用 AsyncTask 把識別放到后臺線程識別流程包含 Bitmap 解碼、縮放、遍歷樣本這些操作在低端機型上耗時幾百毫秒放在主線程會造成卡頓和 ANR。最傳統(tǒng)的寫法是 AsyncTask雖然已被標記為 deprecated但讀老源碼時你大概率會遇到它理解它依然有價值private class RecognizeTask extends AsyncTaskBitmap, Void, RecognitionResult { Override protected RecognitionResult doInBackground(Bitmap... params) { Bitmap original params[0]; Bitmap scaled scaleBitmap(original, 256); int[] feature extractHistogram(scaled, 16); return matcher.matchWithDistance(feature); } Override protected void onPreExecute() { progressBar.setVisibility(View.VISIBLE); } Override protected void onPostExecute(RecognitionResult result) { progressBar.setVisibility(View.GONE); resultView.setText(result.name 距離: result.distance); } }doInBackground里不能直接更新 UIonPostExecute會被系統(tǒng)切回主線程所以只在這里操作TextView或ListView。這套機制適合短耗時任務但 Activity 重建后AsyncTask 持有的舊 Activity 引用會泄漏?,F(xiàn)在的替代方案是ViewModel LiveData或者在onStop里取消任務。如果只是學原理理解“后臺計算、主線程顯示”的分工就夠了。4.2 用 SmartImageView 加載圖片并復用緩存libs/android-smart-image-view-1.0.0.jar是 SmartImageView 庫用法比 Glide 簡單適合這個項目的輕量需求。布局里直接聲明com.loopj.android.image.SmartImageView android:idid/smartImage android:layout_widthmatch_parent android:layout_height240dp android:scaleTypecenterCrop /在 Activity 里加載本地照片用setImageURI加載樣本圖用setImageUrlSmartImageView imageView findViewById(R.id.smartImage); imageView.setImageURI(imageUri); // 加載 assets 下的樣本圖 imageView.setImageUrl(file:///android_asset/sample_rose.jpg, R.drawable.ic_loading, R.drawable.ic_error);SmartImageView 內(nèi)部有一套簡單的異步加載和緩存邏輯圖片會先在非 UI 線程解碼再回到主線程設置到ImageView。需要注意當列表或界面復用ImageView時舊圖片可能在新圖加載完成前被顯示造成閃爍。解決辦法是加載前調(diào)用imageView.setImageResource(R.drawable.ic_loading)清掉舊圖或者在切換時用 tag 做校驗。4.3 運行時的幾個高頻問題現(xiàn)象可能原因排查/解決選圖后閃退大 Bitmap 導致 OOM在getBitmap后立即縮放不要持有原始引用識別結果總是“未知”樣本庫沒有這種花或特征差異過大打印bestDistance調(diào)大maxSize重新統(tǒng)計按鈕點擊幾次就卡頓多個 AsyncTask 并發(fā)識別時置灰按鈕或使用串行線程池SmartImageView 加載慢沒有觸發(fā)異步加載或緩存失效確認 jar 包已打入 APKlibs目錄被 build.gradle 正確引用遇到閃退先看 Logcat 中的OutOfMemoryError這是花卉識別項目最常見的崩潰。Android Studio Profiler 可以觀察內(nèi)存波動如果看到鋸齒狀上升并跌回基線說明縮放邏輯起到作用如果持續(xù)上升不回落就要檢查哪里把 Bitmap 引用長期保存了。4.4 結果頁的距離轉(zhuǎn)置信度識別結果不應該只顯示花名建議把匹配距離一起顯示出來。距離越小表示和樣本越接近但不同圖片尺寸會影響距離絕對值所以我會做一個簡單轉(zhuǎn)換double confidence 1.0 - Math.min(bestDistance / (feature.length * 255.0), 1.0);feature.length是直方圖長度feature.length * 255是理論最大距離的粗略估算這樣得到的confidence被限制在 0 到 1 之間。0.7以上可以認為是可靠結果0.5到0.7顯示“有一定可能”小于0.5則提示“請重新拍攝正對花朵的照片”。這個閾值對不同樣本庫需要重新標定因為顏色直方圖的距離分布高度依賴樣本差異。5. 把固定樣本改成 assets 資源擴展花卉識別庫的落地方案源碼自帶的樣本可能只有十幾種花特征在代碼里硬編碼想加一種花就要重新編譯。更可維護的做法是讓 App 運行時從assets目錄讀取樣本圖片和名稱。先在assets下創(chuàng)建flowers目錄放樣本圖配套一個flowers.csv玫瑰,flowers/rose.jpg 菊花,flowers/chrysanthemum.jpg 郁金香,flowers/tulip.jpg啟動時解析 CSV 并生成特征庫ListFlowerSample samples new ArrayList(); BufferedReader reader new BufferedReader( new InputStreamReader(getAssets().open(flowers.csv))); String line; while ((line reader.readLine()) ! null) { String[] parts line.split(,); InputStream is getAssets().open(parts[1]); Bitmap bmp BitmapFactory.decodeStream(is); is.close(); Bitmap scaled scaleBitmap(bmp, 256); samples.add(new FlowerSample(parts[0], extractHistogram(scaled, 16))); }這種方式把“訓練數(shù)據(jù)”變成了純資源文件以后擴充花卉種類只需要向assets/flowers里增加圖片再在flowers.csv里補一行。注意第一次啟動時預加載所有圖片并計算直方圖幾十張樣本的耗時在幾百毫秒內(nèi)放在啟動頁的異步任務里比較合適。如果想把啟動速度再壓一壓可以把計算好的特征序列化寫入本地文件第二次啟動直接讀取數(shù)組跳過 Bitmap 解碼和直方圖統(tǒng)計。進一步優(yōu)化可以在 CSV 里為同一種花配置多張不同光照、不同背景的樣本。匹配時取多張樣本的最小距離能明顯提升穩(wěn)定性。這套“資源文件驅(qū)動識別庫”的思路也適用于貨架商品檢測、停車位狀態(tài)判斷等場景核心是把業(yè)務數(shù)據(jù)和邏輯代碼解耦。最后一個小技巧每次新增樣本后在FlowerMatcher里打印一份“不同花種平均距離”的報告用來檢驗新圖片有沒有和既有品種混淆如果兩個花種的距離均值相差低于閾值就說明樣本選得太像了換一張拍攝角度差異更大的圖再試。本文還有配套的精品資源點擊獲取