image_ml_isolate.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import 'dart:async';
  2. import 'dart:isolate';
  3. import 'dart:typed_data' show Float32List, Uint8List;
  4. import 'dart:ui';
  5. import 'package:flutter_isolate/flutter_isolate.dart';
  6. import "package:logging/logging.dart";
  7. import "package:photos/face/model/box.dart";
  8. import 'package:photos/models/ml/ml_typedefs.dart';
  9. import "package:photos/services/face_ml/face_alignment/alignment_result.dart";
  10. import "package:photos/services/face_ml/face_detection/detection.dart";
  11. import "package:photos/utils/image_ml_util.dart";
  12. import "package:synchronized/synchronized.dart";
  13. enum ImageOperation {
  14. preprocessBlazeFace,
  15. preprocessYoloOnnx,
  16. preprocessFaceAlign,
  17. preprocessMobileFaceNet,
  18. preprocessMobileFaceNetOnnx,
  19. generateFaceThumbnail,
  20. generateFaceThumbnailsForImage,
  21. cropAndPadFace,
  22. }
  23. /// The isolate below uses functions from ["package:photos/utils/image_ml_util.dart"] to preprocess images for ML models.
  24. /// This class is responsible for all image operations needed for ML models. It runs in a separate isolate to avoid jank.
  25. ///
  26. /// It can be accessed through the singleton `ImageConversionIsolate.instance`. e.g. `ImageConversionIsolate.instance.convert(imageData)`
  27. ///
  28. /// IMPORTANT: Make sure to dispose of the isolate when you're done with it with `dispose()`, e.g. `ImageConversionIsolate.instance.dispose();`
  29. class ImageMlIsolate {
  30. // static const String debugName = 'ImageMlIsolate';
  31. final _logger = Logger('ImageMlIsolate');
  32. Timer? _inactivityTimer;
  33. final Duration _inactivityDuration = const Duration(seconds: 60);
  34. int _activeTasks = 0;
  35. final _initLock = Lock();
  36. final _functionLock = Lock();
  37. late FlutterIsolate _isolate;
  38. late ReceivePort _receivePort = ReceivePort();
  39. late SendPort _mainSendPort;
  40. bool isSpawned = false;
  41. // singleton pattern
  42. ImageMlIsolate._privateConstructor();
  43. /// Use this instance to access the ImageConversionIsolate service. Make sure to call `init()` before using it.
  44. /// e.g. `await ImageConversionIsolate.instance.init();`
  45. /// And kill the isolate when you're done with it with `dispose()`, e.g. `ImageConversionIsolate.instance.dispose();`
  46. ///
  47. /// Then you can use `convert()` to get the image, so `ImageConversionIsolate.instance.convert(imageData, imagePath: imagePath)`
  48. static final ImageMlIsolate instance = ImageMlIsolate._privateConstructor();
  49. factory ImageMlIsolate() => instance;
  50. Future<void> init() async {
  51. return _initLock.synchronized(() async {
  52. if (isSpawned) return;
  53. _receivePort = ReceivePort();
  54. try {
  55. _isolate = await FlutterIsolate.spawn(
  56. _isolateMain,
  57. _receivePort.sendPort,
  58. );
  59. _mainSendPort = await _receivePort.first as SendPort;
  60. isSpawned = true;
  61. _resetInactivityTimer();
  62. } catch (e) {
  63. _logger.severe('Could not spawn isolate', e);
  64. isSpawned = false;
  65. }
  66. });
  67. }
  68. Future<void> ensureSpawned() async {
  69. if (!isSpawned) {
  70. await init();
  71. }
  72. }
  73. @pragma('vm:entry-point')
  74. static void _isolateMain(SendPort mainSendPort) async {
  75. final receivePort = ReceivePort();
  76. mainSendPort.send(receivePort.sendPort);
  77. receivePort.listen((message) async {
  78. final functionIndex = message[0] as int;
  79. final function = ImageOperation.values[functionIndex];
  80. final args = message[1] as Map<String, dynamic>;
  81. final sendPort = message[2] as SendPort;
  82. try {
  83. switch (function) {
  84. case ImageOperation.preprocessBlazeFace:
  85. final imageData = args['imageData'] as Uint8List;
  86. final normalize = args['normalize'] as bool;
  87. final int normalization = normalize ? 2 : -1;
  88. final requiredWidth = args['requiredWidth'] as int;
  89. final requiredHeight = args['requiredHeight'] as int;
  90. final qualityIndex = args['quality'] as int;
  91. final maintainAspectRatio = args['maintainAspectRatio'] as bool;
  92. final quality = FilterQuality.values[qualityIndex];
  93. final (result, originalSize, newSize) =
  94. await preprocessImageToMatrix(
  95. imageData,
  96. normalization: normalization,
  97. requiredWidth: requiredWidth,
  98. requiredHeight: requiredHeight,
  99. quality: quality,
  100. maintainAspectRatio: maintainAspectRatio,
  101. );
  102. sendPort.send({
  103. 'inputs': result,
  104. 'originalWidth': originalSize.width,
  105. 'originalHeight': originalSize.height,
  106. 'newWidth': newSize.width,
  107. 'newHeight': newSize.height,
  108. });
  109. case ImageOperation.preprocessYoloOnnx:
  110. final imageData = args['imageData'] as Uint8List;
  111. final normalize = args['normalize'] as bool;
  112. final int normalization = normalize ? 1 : -1;
  113. final requiredWidth = args['requiredWidth'] as int;
  114. final requiredHeight = args['requiredHeight'] as int;
  115. final qualityIndex = args['quality'] as int;
  116. final maintainAspectRatio = args['maintainAspectRatio'] as bool;
  117. final quality = FilterQuality.values[qualityIndex];
  118. final (result, originalSize, newSize) =
  119. await preprocessImageToFloat32ChannelsFirst(
  120. imageData,
  121. normalization: normalization,
  122. requiredWidth: requiredWidth,
  123. requiredHeight: requiredHeight,
  124. quality: quality,
  125. maintainAspectRatio: maintainAspectRatio,
  126. );
  127. sendPort.send({
  128. 'inputs': result,
  129. 'originalWidth': originalSize.width,
  130. 'originalHeight': originalSize.height,
  131. 'newWidth': newSize.width,
  132. 'newHeight': newSize.height,
  133. });
  134. case ImageOperation.preprocessFaceAlign:
  135. final imageData = args['imageData'] as Uint8List;
  136. final faceLandmarks =
  137. args['faceLandmarks'] as List<List<List<double>>>;
  138. final List<Uint8List> result = await preprocessFaceAlignToUint8List(
  139. imageData,
  140. faceLandmarks,
  141. );
  142. sendPort.send(List.from(result));
  143. case ImageOperation.preprocessMobileFaceNet:
  144. final imageData = args['imageData'] as Uint8List;
  145. final facesJson = args['facesJson'] as List<Map<String, dynamic>>;
  146. final (
  147. inputs,
  148. alignmentResults,
  149. isBlurs,
  150. blurValues,
  151. originalSize
  152. ) = await preprocessToMobileFaceNetInput(
  153. imageData,
  154. facesJson,
  155. );
  156. final List<Map<String, dynamic>> alignmentResultsJson =
  157. alignmentResults.map((result) => result.toJson()).toList();
  158. sendPort.send({
  159. 'inputs': inputs,
  160. 'alignmentResultsJson': alignmentResultsJson,
  161. 'isBlurs': isBlurs,
  162. 'blurValues': blurValues,
  163. 'originalWidth': originalSize.width,
  164. 'originalHeight': originalSize.height,
  165. });
  166. case ImageOperation.preprocessMobileFaceNetOnnx:
  167. final imagePath = args['imagePath'] as String;
  168. final facesJson = args['facesJson'] as List<Map<String, dynamic>>;
  169. final List<FaceDetectionRelative> relativeFaces = facesJson
  170. .map((face) => FaceDetectionRelative.fromJson(face))
  171. .toList();
  172. final (
  173. inputs,
  174. alignmentResults,
  175. isBlurs,
  176. blurValues,
  177. originalSize
  178. ) = await preprocessToMobileFaceNetFloat32List(
  179. imagePath,
  180. relativeFaces,
  181. );
  182. final List<Map<String, dynamic>> alignmentResultsJson =
  183. alignmentResults.map((result) => result.toJson()).toList();
  184. sendPort.send({
  185. 'inputs': inputs,
  186. 'alignmentResultsJson': alignmentResultsJson,
  187. 'isBlurs': isBlurs,
  188. 'blurValues': blurValues,
  189. 'originalWidth': originalSize.width,
  190. 'originalHeight': originalSize.height,
  191. });
  192. case ImageOperation.generateFaceThumbnail:
  193. final imageData = args['imageData'] as Uint8List;
  194. final faceDetectionJson =
  195. args['faceDetection'] as Map<String, dynamic>;
  196. final faceDetection =
  197. FaceDetectionRelative.fromJson(faceDetectionJson);
  198. final Uint8List result =
  199. await generateFaceThumbnailFromData(imageData, faceDetection);
  200. sendPort.send(<dynamic>[result]);
  201. case ImageOperation.generateFaceThumbnailsForImage:
  202. final imageData = args['imageData'] as Uint8List;
  203. final faceBoxesJson =
  204. args['faceBoxesList'] as List<Map<String, dynamic>>;
  205. final List<FaceBox> faceBoxes =
  206. faceBoxesJson.map((json) => FaceBox.fromJson(json)).toList();
  207. final List<Uint8List> results =
  208. await generateFaceThumbnailsFromDataAndDetections(
  209. imageData,
  210. faceBoxes,
  211. );
  212. sendPort.send(List.from(results));
  213. case ImageOperation.cropAndPadFace:
  214. final imageData = args['imageData'] as Uint8List;
  215. final faceBox = args['faceBox'] as List<double>;
  216. final Uint8List result =
  217. await cropAndPadFaceData(imageData, faceBox);
  218. sendPort.send(<dynamic>[result]);
  219. }
  220. } catch (e, stackTrace) {
  221. sendPort
  222. .send({'error': e.toString(), 'stackTrace': stackTrace.toString()});
  223. }
  224. });
  225. }
  226. /// The common method to run any operation in the isolate. It sends the [message] to [_isolateMain] and waits for the result.
  227. Future<dynamic> _runInIsolate(
  228. (ImageOperation, Map<String, dynamic>) message,
  229. ) async {
  230. await ensureSpawned();
  231. return _functionLock.synchronized(() async {
  232. _resetInactivityTimer();
  233. final completer = Completer<dynamic>();
  234. final answerPort = ReceivePort();
  235. _activeTasks++;
  236. _mainSendPort.send([message.$1.index, message.$2, answerPort.sendPort]);
  237. answerPort.listen((receivedMessage) {
  238. if (receivedMessage is Map && receivedMessage.containsKey('error')) {
  239. // Handle the error
  240. final errorMessage = receivedMessage['error'];
  241. final errorStackTrace = receivedMessage['stackTrace'];
  242. final exception = Exception(errorMessage);
  243. final stackTrace = StackTrace.fromString(errorStackTrace);
  244. completer.completeError(exception, stackTrace);
  245. } else {
  246. completer.complete(receivedMessage);
  247. }
  248. });
  249. _activeTasks--;
  250. return completer.future;
  251. });
  252. }
  253. /// Resets a timer that kills the isolate after a certain amount of inactivity.
  254. ///
  255. /// Should be called after initialization (e.g. inside `init()`) and after every call to isolate (e.g. inside `_runInIsolate()`)
  256. void _resetInactivityTimer() {
  257. _inactivityTimer?.cancel();
  258. _inactivityTimer = Timer(_inactivityDuration, () {
  259. if (_activeTasks > 0) {
  260. _logger.info('Tasks are still running. Delaying isolate disposal.');
  261. // Optionally, reschedule the timer to check again later.
  262. _resetInactivityTimer();
  263. } else {
  264. _logger.info(
  265. 'Clustering Isolate has been inactive for ${_inactivityDuration.inSeconds} seconds with no tasks running. Killing isolate.',
  266. );
  267. dispose();
  268. }
  269. });
  270. }
  271. /// Disposes the isolate worker.
  272. void dispose() {
  273. if (!isSpawned) return;
  274. isSpawned = false;
  275. _isolate.kill();
  276. _receivePort.close();
  277. _inactivityTimer?.cancel();
  278. }
  279. /// Preprocesses [imageData] for standard ML models inside a separate isolate.
  280. ///
  281. /// Returns a [Num3DInputMatrix] image usable for ML inference with BlazeFace.
  282. ///
  283. /// Uses [preprocessImageToMatrix] inside the isolate.
  284. Future<(Num3DInputMatrix, Size, Size)> preprocessImageBlazeFace(
  285. Uint8List imageData, {
  286. required bool normalize,
  287. required int requiredWidth,
  288. required int requiredHeight,
  289. FilterQuality quality = FilterQuality.medium,
  290. bool maintainAspectRatio = true,
  291. }) async {
  292. final Map<String, dynamic> results = await _runInIsolate(
  293. (
  294. ImageOperation.preprocessBlazeFace,
  295. {
  296. 'imageData': imageData,
  297. 'normalize': normalize,
  298. 'requiredWidth': requiredWidth,
  299. 'requiredHeight': requiredHeight,
  300. 'quality': quality.index,
  301. 'maintainAspectRatio': maintainAspectRatio,
  302. },
  303. ),
  304. );
  305. final inputs = results['inputs'] as Num3DInputMatrix;
  306. final originalSize = Size(
  307. results['originalWidth'] as double,
  308. results['originalHeight'] as double,
  309. );
  310. final newSize = Size(
  311. results['newWidth'] as double,
  312. results['newHeight'] as double,
  313. );
  314. return (inputs, originalSize, newSize);
  315. }
  316. /// Uses [preprocessImageToFloat32ChannelsFirst] inside the isolate.
  317. Future<(Float32List, Size, Size)> preprocessImageYoloOnnx(
  318. Uint8List imageData, {
  319. required bool normalize,
  320. required int requiredWidth,
  321. required int requiredHeight,
  322. FilterQuality quality = FilterQuality.medium,
  323. bool maintainAspectRatio = true,
  324. }) async {
  325. final Map<String, dynamic> results = await _runInIsolate(
  326. (
  327. ImageOperation.preprocessYoloOnnx,
  328. {
  329. 'imageData': imageData,
  330. 'normalize': normalize,
  331. 'requiredWidth': requiredWidth,
  332. 'requiredHeight': requiredHeight,
  333. 'quality': quality.index,
  334. 'maintainAspectRatio': maintainAspectRatio,
  335. },
  336. ),
  337. );
  338. final inputs = results['inputs'] as Float32List;
  339. final originalSize = Size(
  340. results['originalWidth'] as double,
  341. results['originalHeight'] as double,
  342. );
  343. final newSize = Size(
  344. results['newWidth'] as double,
  345. results['newHeight'] as double,
  346. );
  347. return (inputs, originalSize, newSize);
  348. }
  349. /// Preprocesses [imageData] for face alignment inside a separate isolate, to display the aligned faces. Mostly used for debugging.
  350. ///
  351. /// Returns a list of [Uint8List] images, one for each face, in png format.
  352. ///
  353. /// Uses [preprocessFaceAlignToUint8List] inside the isolate.
  354. ///
  355. /// WARNING: For preprocessing for MobileFaceNet, use [preprocessMobileFaceNet] instead!
  356. Future<List<Uint8List>> preprocessFaceAlign(
  357. Uint8List imageData,
  358. List<FaceDetectionAbsolute> faces,
  359. ) async {
  360. final faceLandmarks = faces.map((face) => face.allKeypoints).toList();
  361. return await _runInIsolate(
  362. (
  363. ImageOperation.preprocessFaceAlign,
  364. {
  365. 'imageData': imageData,
  366. 'faceLandmarks': faceLandmarks,
  367. },
  368. ),
  369. ).then((value) => value.cast<Uint8List>());
  370. }
  371. /// Preprocesses [imageData] for MobileFaceNet input inside a separate isolate.
  372. ///
  373. /// Returns a list of [Num3DInputMatrix] images, one for each face.
  374. ///
  375. /// Uses [preprocessToMobileFaceNetInput] inside the isolate.
  376. @Deprecated("Old method used in TensorFlow Lite")
  377. Future<
  378. (
  379. List<Num3DInputMatrix>,
  380. List<AlignmentResult>,
  381. List<bool>,
  382. List<double>,
  383. Size,
  384. )> preprocessMobileFaceNet(
  385. Uint8List imageData,
  386. List<FaceDetectionRelative> faces,
  387. ) async {
  388. final List<Map<String, dynamic>> facesJson =
  389. faces.map((face) => face.toJson()).toList();
  390. final Map<String, dynamic> results = await _runInIsolate(
  391. (
  392. ImageOperation.preprocessMobileFaceNet,
  393. {
  394. 'imageData': imageData,
  395. 'facesJson': facesJson,
  396. },
  397. ),
  398. );
  399. final inputs = results['inputs'] as List<Num3DInputMatrix>;
  400. final alignmentResultsJson =
  401. results['alignmentResultsJson'] as List<Map<String, dynamic>>;
  402. final alignmentResults = alignmentResultsJson.map((json) {
  403. return AlignmentResult.fromJson(json);
  404. }).toList();
  405. final isBlurs = results['isBlurs'] as List<bool>;
  406. final blurValues = results['blurValues'] as List<double>;
  407. final originalSize = Size(
  408. results['originalWidth'] as double,
  409. results['originalHeight'] as double,
  410. );
  411. return (inputs, alignmentResults, isBlurs, blurValues, originalSize);
  412. }
  413. /// Uses [preprocessToMobileFaceNetFloat32List] inside the isolate.
  414. Future<(Float32List, List<AlignmentResult>, List<bool>, List<double>, Size)>
  415. preprocessMobileFaceNetOnnx(
  416. String imagePath,
  417. List<FaceDetectionRelative> faces,
  418. ) async {
  419. final List<Map<String, dynamic>> facesJson =
  420. faces.map((face) => face.toJson()).toList();
  421. final Map<String, dynamic> results = await _runInIsolate(
  422. (
  423. ImageOperation.preprocessMobileFaceNetOnnx,
  424. {
  425. 'imagePath': imagePath,
  426. 'facesJson': facesJson,
  427. },
  428. ),
  429. );
  430. final inputs = results['inputs'] as Float32List;
  431. final alignmentResultsJson =
  432. results['alignmentResultsJson'] as List<Map<String, dynamic>>;
  433. final alignmentResults = alignmentResultsJson.map((json) {
  434. return AlignmentResult.fromJson(json);
  435. }).toList();
  436. final isBlurs = results['isBlurs'] as List<bool>;
  437. final blurValues = results['blurValues'] as List<double>;
  438. final originalSize = Size(
  439. results['originalWidth'] as double,
  440. results['originalHeight'] as double,
  441. );
  442. return (inputs, alignmentResults, isBlurs, blurValues, originalSize);
  443. }
  444. /// Generates a face thumbnail from [imageData] and a [faceDetection].
  445. ///
  446. /// Uses [generateFaceThumbnailFromData] inside the isolate.
  447. Future<Uint8List> generateFaceThumbnail(
  448. Uint8List imageData,
  449. FaceDetectionRelative faceDetection,
  450. ) async {
  451. return await _runInIsolate(
  452. (
  453. ImageOperation.generateFaceThumbnail,
  454. {
  455. 'imageData': imageData,
  456. 'faceDetection': faceDetection.toJson(),
  457. },
  458. ),
  459. ).then((value) => value[0] as Uint8List);
  460. }
  461. /// Generates face thumbnails for all [faceBoxes] in [imageData].
  462. ///
  463. /// Uses [generateFaceThumbnailsFromDataAndDetections] inside the isolate.
  464. Future<List<Uint8List>> generateFaceThumbnailsForImage(
  465. Uint8List imageData,
  466. List<FaceBox> faceBoxes,
  467. ) async {
  468. final List<Map<String, dynamic>> faceBoxesJson =
  469. faceBoxes.map((box) => box.toJson()).toList();
  470. return await _runInIsolate(
  471. (
  472. ImageOperation.generateFaceThumbnailsForImage,
  473. {
  474. 'imageData': imageData,
  475. 'faceBoxesList': faceBoxesJson,
  476. },
  477. ),
  478. ).then((value) => value.cast<Uint8List>());
  479. }
  480. /// Generates cropped and padded image data from [imageData] and a [faceBox].
  481. ///
  482. /// The steps are:
  483. /// 1. Crop the image to the face bounding box
  484. /// 2. Resize this cropped image to a square that is half the BlazeFace input size
  485. /// 3. Pad the image to the BlazeFace input size
  486. ///
  487. /// Uses [cropAndPadFaceData] inside the isolate.
  488. Future<Uint8List> cropAndPadFace(
  489. Uint8List imageData,
  490. List<double> faceBox,
  491. ) async {
  492. return await _runInIsolate(
  493. (
  494. ImageOperation.cropAndPadFace,
  495. {
  496. 'imageData': imageData,
  497. 'faceBox': List<double>.from(faceBox),
  498. },
  499. ),
  500. ).then((value) => value[0] as Uint8List);
  501. }
  502. }