Camera và Rotation trong Flutter

Mấy ngày trước, team có quay lại một dự án Flutter, trong đó khi test một tính năng liên quan đến nhận dạng hình ảnh với Flow khá “đơn giản” là chụp cái hình từ camera, detect trong hình có người hay không, nếu có người trong hình với độ chính xác >75% thì là hình hợp lệ và có thể upload lên server. Kết quả thì thấy mọi thiết bị trong công ty vẫn hoạt động như lúc nghiệm thu (iPhone, Oppo, android cùi..), duy chỉ có con SamSung A50 là bị lỗi nhận dạng trọng khi con SamSung này lúc test nghiệm thu khoảng 3 tháng trước hoạt động bình thường. 

Hình minh họa hệ thống nhận dạng với độ chính xác 92% (hợp lệ)

Tìm kiếm nguyên nhân

Chỉ có một thông tin là con SamSung này vừa nâng cấp OS mấy cái minor change gì đó không đang kể trước đó vài tuần.

Bước đầu nghi ngờ có thể thư viện nhận dạng bị lỗi trên SamSung khiến cho tỷ lệ chính xác không cao, nhưng giả thuyết này không thuyết phục lắm bởi sử dụng plugin tflite của Flutter và sử model POSTNET nên không thể nào lúc trước pass và giờ thì lại không. 

Đối với một chương trình Machine Learning(ML) với 3 thành phần: INPUT –> PROGRAM –> OUTPUT. Nếu cái OUTPUT bị vấn đề, cái PROGRAM không thay đổi thì khả năng cao là cái INPUT có vấn đề. Trong trường hợp này INPUT chính là cái hình chụp từ Camera thông qua plugin camera trên pub.dev.

Camera trên Flutter

Trước đó đã có sụp hầm một lần vụ camera và có post issue lên github của Flutter để thảo luận, nên mình nghi ngờ khả năng này là cao vì lúc trước “bệnh” này cũng gặp trên iPhone (iOS). Lần này mình nghĩ là có thể bệnh này đã lây từ iOS sang con SamSung, bỏ qua giả thuyết nó lây sang Android bởi vì mấy con android khác vẫn hoạt động bình thường. Khá chắc chắn là đợt nâng cấp OS vừa rồi thì ông SamSung đã thay đổi (cải tiến/cải lùi) cái gì đó liên quan đến camera và hình ảnh xuất ra.

Mình đã thử áp dụng nhanh biện pháp trị bệnh như lúc trị cho iOS là sử dụng thư viện `FlutterImageCompress` để loại bỏ thông tin Orientation để PROGRAM nhận được 1 cái INPUT thì kết quả đúng trở lại. Voila!

Đoạn code minh họa việc sử dụng thư viện để tạo một hình mới với thông tin Orientation bị loại bỏ và hợp lệ cho ML Program của mình:

import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'dart:io';
...
Future<File> fixOrientationOfPhoto(File file) async {
String targetPath = file.absolute.path.replaceFirst('.jpg', '-fix-orientation.jpg');

var result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
targetPath,
quality: 95,
autoCorrectionAngle: true,
);

return result;
}

Sử dụng hàm ở trên sẽ ảnh hưởng đến performance cho toàn bộ nền tảng (iOS & Android) nên lúc trước mình chỉ giới hạn trên nền tảng iOS mà thôi (`Platform.isIOS`), nay phải thêm trường hợp ông SamSung này, chả lẽ lại if…else tùm lum, nhưng giải pháp này cũng tạm thời mà thôi vì biết đâu trong đống Android khác sau này lại nâng cấp và bị trường hợp camera như SamSung. Nên lần này quyết định refactoring phần này, không dựa vào nền tảng hay device cụ thể mà hy vọng sẽ tìm thấy sự trùng hợp nào đó trong…thông tin EXIF của file hình chụp từ Camera.

Xử lý dựa vào EXIF của hình chụp từ Camera

Bên dưới là thông tin EXIF mình lấy được bằng plugin exif của hình chụp từ camera trên 3 thiết bị mà mình test thì quả thật hình của SamSung chụp được có thông tin Orientation khá tương đồng với iPhone. (Lưu ý các thông tin EXIF mình in đâm cho dễ coi)

  1. EXIF lấy được từ hình chụp trên iPhone SMAX, iOS 13.1.1
flutter: Image Orientation (Short): Rotated 90 CW
flutter: Image ExifOffset (Long): 38
flutter: EXIF ColorSpace (Short): sRGB
flutter: EXIF ExifImageWidth (Long): 640
flutter: EXIF ExifImageLength (Long): 480
(Chỉ vỏn vẹn mấy thông tin Exif)

2. EXIF lấy được từ hình chụp trên SAMSUNG A50, Android 9.0

I/flutter (17638): Image ImageWidth (Long): 720
I/flutter (17638): Image ImageLength (Long): 480
I/flutter (17638): Image Orientation (Short): Rotated 90 CW
I/flutter (17638): Image YCbCrPositioning (Short): Centered
...
I/flutter (17638): EXIF ColorSpace (Short): sRGB
I/flutter (17638): EXIF ExifImageWidth (Long): 720
I/flutter (17638): EXIF ExifImageLength (Long): 480
...
(tổng cộng khoảng 50+ thông tin từ Exif)

3. EXIF lấy được từ hình chụp trên OPPO F9, Android 9

I/flutter (31899): Image ImageDescription (ASCII): 
I/flutter (31899): Image Make (ASCII): OPPO
I/flutter (31899): Image Model (ASCII): CPH1825
I/flutter (31899): Image Orientation (Short): 0
I/flutter (31899): Image XResolution (Ratio): 72
I/flutter (31899): Image YResolution (Ratio): 72
...
I/flutter (31899): EXIF SubSecTimeOriginal (ASCII): 23
I/flutter (31899): EXIF SubSecTimeDigitized (ASCII): 23
I/flutter (31899): EXIF FlashPixVersion (Undefined): 0100
I/flutter (31899): EXIF ColorSpace (Short): sRGB
I/flutter (31899): EXIF ExifImageWidth (Long): 480
I/flutter (31899): EXIF ExifImageLength (Long): 640
...
(tổng cộng có khoảng 50+ thông tin từ Exif)

Sau khi review thông tin EXIF thì giải pháp bây giờ có vẻ khá đơn giản là dựa vào thông tin Image Orientation để quyết định có áp dụng nén hình để hình xoay tự động lại và phù hợp với chương trình ML của mình. Bên dưới là một đoạn code của xử lý đọc EXIF và detect có cần fix hình lại hay không:

bool needFixRotate = true;
//get exif information (prepare for fix rotation)
Map<String, IfdTag> data = await readExifFromBytes(await file.readAsBytes());
if (data == null || data.isEmpty) {
print("No EXIF information found\n");
} else {
if (data.containsKey('Image Orientation')) {
String exifRotate = data['Image Orientation'].toString();
if (exifRotate != '0' && exifRotate != '') {
needFixRotate = true;
} else {
needFixRotate = false;
}
}
}
...

Như vậy, về sau với cách xử lý workaround này thì dù cho hình có thông tin Orientation hay không từ camera thì chương trình đều có thể nhận được INPUT hợp lệ. Hy vọng bài viết nhỏ này sẽ giúp bạn có một chút thông tin khi làm việc với thư viện camera trên Flutter.


CropCom vẫn đang tìm kiếm Flutter developer với kinh nghiệm tối thiểu 1 năm. Nếu bạn ở HCM và hứng thú với vị trí này thì có thể gửi CV tại support@teamcrop.com

Leave a Reply

Your email address will not be published. Required fields are marked *