image_ml_isolate.dart 19 KB

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