Triển khai phân loại hình ảnh trên thiết bị Android
1. Giới thiệu
Bài viết này sẽ hướng dẫn bạn cách triển khai một ứng dụng Android có khả năng phân loại hình ảnh, sử dụng mô hình học sâu. Chúng ta sẽ tìm hiểu cách tích hợp mô hình đã huấn luyện mobilenet vào ứng dụng, đồng thời tối ưu hiệu suất cho thiết bị di động.
Ứng dụng sẽ tải model đã được huấn luyện, lần lượt duyệt qua các ảnh trong thư mục trong thiết bị sau đó xác định đối tượng chính xuất hiện trong ảnh và hiển thị ảnh và thông tin đối tượng đó lên màn hình.
2. Cấu trúc dự án
Sử dụng phần mềm lập trình Android studio, dùng ngôn ngữ Java (phiên bản ngôn ngữ Kotin xem tại đây), các tệp và thư mục chính được tổ chức như sau:
root@aicandy:/aicandy/projects/AIcandy_Android_ImageClassification_smdkrohy/app/src//main# tree
.
├── AndroidManifest.xml
├── assets
│ ├── image_test
│ │ ├── ...
│ └── mobilenet-v2.pt
├── java
│ └── com
│ └── aicandy
│ └── imageclassification
│ └── mobilenet
│ ├── Classifier.java
│ ├── Constants.java
│ ├── MainActivity.java
│ ├── Result.java
│ └── Utils.java
└── res
├── layout
│ ├── activity_main.xml
│ ├── activity_result.xml
│ ├── content_main.xml
│ └── content_result.xml
└── ...
└── ...
Trong đó:
- Thư mục image_test chứa các cảnh cần kiểm tra.
- Tệp mobilenet-v2.pt là model đã được huấn luyện
- Tệp Classifier.java chứa mã nguồn để xử lý hình ảnh
- Tệp Constants.java chứa thông tin về tất cả các đối tượng được dự đoán trong mô hình.
- Tệp MainActivity.java chứa chương trình chính của ứng dụng.
- Thư mục layout chứa các tệp cấu hình giao diện hiển thị.
3. Cấu hình AndroidManifest
Tệp AndroidManifest là nơi cấu hình thông tin quan trọng về ứng dụng, bao gồm các activity (màn hình), biểu tượng, tên ứng dụng, và các tính năng hỗ trợ khác.
Một số cấu hình chính trong tệp:
android:allowBackup=”true”
Cho phép hệ thống sao lưu và phục hồi dữ liệu ứng dụng khi cần (như khi cài lại app).
android:icon=”@mipmap/ic_launcher”
Chỉ định biểu tượng chính của ứng dụng, thường được hiển thị trên màn hình chính.
android:label=”@string/app_name”
Đặt tên của ứng dụng, được định nghĩa trong tệp string resources (res/values/strings.xml).
android:theme=”@style/AppTheme”
Chỉ định theme (giao diện) chung cho ứng dụng, được định nghĩa trong tệp styles (res/values/styles.xml).
android:name=”.Result”
Đây là tên class của activity, là màn hình được điều hướng đến khi cần hiển thị kết quả (nằm trong gói chính của ứng dụng).
android:name=”.MainActivity”
Đây là tên class của MainActivity, là màn hình khởi động chính của ứng dụng.
<action android:name=”android.intent.action.MAIN” />
Định nghĩa đây là “main entry point”, tức là điểm vào chính của ứng dụng.
<category android:name=”android.intent.category.LAUNCHER” />
Chỉ định activity này xuất hiện trong danh sách ứng dụng (launcher) của thiết bị, là nơi người dùng có thể khởi động ứng dụng từ màn hình chính.
<activity
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
4. Giao diện hiển thị
Ứng dụng có giao diện với các thành phần chính để hiển thị thông tin trạng thái chương trình, hiển thị hình ảnh và hiển thị kết quả.
Tạo TextView để hiển thị trạng thái chương trình, các trạng thái như “Đang xử lý ảnh”, “Đang tải model”, “Kết quả” …
<TextView
android:id="@+id/statusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar" />
Tạo ImageView để hiển thị hình ảnh trên màn hình
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="@drawable/ic_launcher_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/image"/>
Và cuối cùng, chúng ta tạo TextView để hiển thị tên của đối tượng có trong ảnh mà mô hình đã phát hiện được.
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World"
android:id="@+id/label"
android:textSize="16pt"
app:layout_constraintStart_toStartOf="@id/image"
app:layout_constraintEnd_toEndOf="@id/image"
app:layout_constraintTop_toBottomOf="@id/image"
5. Model và tiền xử lý
Chú ý rằng các đoạn code trong bài là các key chính, code đầy đủ cần xem phần source code ở mục 7.
Bước 1: Tải model
Để tải model đã huấn luyện, sử dụng phương thức Module trong pytorch và load model với tham số đầu vào là đường dẫn của model.
model = Module.load(modelPath);
Cũng cần chú ý là để sử dụng được thư viện pytorch, chúng ta cần khai báo thư viện pytorch trong build.gradle
dependencies {
implementation 'org.pytorch:pytorch_android:2.1.0'
...
}
Bước 2: Chuẩn hóa hình ảnh
Model huấn luyện đã sử dụng bộ tham số chuẩn hóa (mean và std), do đó trong quá trình đánh giá (kiểm tra/dự đoán) chúng ta cũng cần chuẩn hóa tương tự. Việc chuẩn hóa có tác dụng:
- Đưa các giá trị pixel về một phạm vi dễ quản lý: Hình ảnh gốc có các giá trị pixel từ 0 đến 255. Chuẩn hóa làm cho các giá trị pixel nằm trong khoảng [-1, 1] hoặc gần với nó, điều này giúp các mô hình học sâu dễ dàng tối ưu hơn.
- Tăng tốc độ hội tụ của mô hình: Khi dữ liệu đầu vào có cùng phân phối, các thuật toán tối ưu như gradient descent sẽ hoạt động hiệu quả hơn, dẫn đến việc mô hình huấn luyện nhanh hơn.
- Giảm sự phụ thuộc vào các biến đổi về ánh sáng: Nhờ chuẩn hóa, mô hình ít bị ảnh hưởng bởi sự thay đổi về độ sáng, màu sắc trong dữ liệu ảnh thực tế, giúp mô hình tổng quát tốt hơn khi triển khai trên các hình ảnh khác nhau.
float[] mean = {0.485f, 0.456f, 0.406f};
float[] std = {0.229f, 0.224f, 0.225f};
bitmap = Bitmap.createScaledBitmap(bitmap,size,size,false);
return TensorImageUtils.bitmapToFloat32Tensor(bitmap,this.mean,this.std);
Bước 3: Tạo dự đoán
Ở bước này, chúng ta tạo hàm xử lý dữ liệu ảnh bitmap đầu vào để tra ra kết quả. Việc đầu tiên là chuyển dữ liệu đầu vào sang dạng tensor để phù hợp với tính toán trong pytorch. Sau đó đưa tensor đầu vào qua model và lấy điểm số ở đầu ra.
Tensor tensor = preprocess(bitmap,224);
IValue inputs = IValue.from(tensor);
Tensor outputs = model.forward(inputs).toTensor();
float[] scores = outputs.getDataAsFloatArray();
6. Chương trình chính
Trong chương trình này sẽ thực hiện các công việc như duyệt và lấy tên các file ảnh trong thư mục, chuyển đổi sang bitmap (là cấu trúc dữ liệu được sử dụng phổ biến để lưu trữ và quản lý các hình ảnh. Nó đại diện cho một ma trận các pixel với mỗi pixel chứa thông tin về màu sắc. Bitmap cho phép bạn dễ dàng truy xuất giá trị pixel để thực hiện chuyển ảnh sang dạng tensor), dự đoán đối tượng từ ảnh, hiển thị ảnh và hiển thị tên đối tượng lên màn hình.
Bước 1: Duyệt ảnh
Để duyệt ảnh được lưu trong thư mục “asset” cần sử dụng AssetManager
List imageFiles = new ArrayList<>();
AssetManager assetManager = this.getAssets();
String[] files = assetManager.list(folderPath);
Bước 2: Tải ảnh và chuyển đổi sang bitmap
inputStream = getAssets().open(fileName);
getAssets() là một phương thức của Context (hoặc lớp con của nó, như Activity) để truy cập thư mục assets của ứng dụng.
BitmapFactory.Options options = new BitmapFactory.Options();
Tạo một đối tượng BitmapFactory.Options để thiết lập các tùy chọn cấu hình khi giải mã tệp hình ảnh. BitmapFactory là một lớp tiện ích dùng để chuyển đổi các tệp hình ảnh (như PNG, JPEG) thành đối tượng Bitmap có thể được hiển thị trong các giao diện Android.
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Thiết lập cấu hình cho Bitmap. Cấu hình này quyết định cách mà pixel trong hình ảnh sẽ được lưu trữ. ARGB_8888 cung cấp chất lượng hình ảnh tốt với độ chi tiết màu sắc cao (32-bit), nhưng sẽ chiếm nhiều bộ nhớ hơn so với các cấu hình khác.
inputStream = getAssets().open(fileName);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
return BitmapFactory.decodeStream(inputStream, null, options);
Bước 3: Dự đoán
String prediction = classifier.predict(originalBitmap);
Log.d(TAG, "AIcandy.vn - detected: " + prediction);
Bước 4: Hiển thị ảnh và thông tin đối tượng lên màn hình
mainHandler.post(new Runnable()
mainHandler là một đối tượng của lớp Handler, thường được sử dụng để gửi và xử lý các đối tượng Runnable. Phương thức post()
giúp đăng một công việc (Runnable
) lên hàng đợi, để nó được thực thi trên thread mà Handler
liên kết (ở đây thường là main thread).
Intent resultView = new Intent(MainActivity.this, Result.class);
Tạo một đối tượng Intent
để chuyển từ MainActivity
sang Result
activity. Intent
giúp truyền dữ liệu và bắt đầu một activity mới.
resultView.putExtra(“image_path”, imagePath);
Sử dụng putExtra()
để đính kèm thêm dữ liệu vào Intent
dưới dạng cặp khóa-giá trị (key-value
).
mainHandler.post(new Runnable() {
@Override
public void run() {
statusText.setVisibility(View.GONE);
Intent resultView = new Intent(MainActivity.this, Result.class);
resultView.putExtra("image_path", imagePath);
resultView.putExtra("pred", prediction);
startActivity(resultView);
}
});
7. Demo và source code
Video demo có tại đây
Source code phiên bản sử dụng ngôn ngữ Java có tại đây
Source code phiên bản sử dụng ngôn ngữ Kotin có tại đây