image_ml_isolate.dart 19 KB

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