faces_item_widget.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import "dart:developer" as dev show log;
  2. import "package:flutter/foundation.dart" show Uint8List, kDebugMode;
  3. import "package:flutter/material.dart";
  4. import "package:logging/logging.dart";
  5. import "package:photos/face/db.dart";
  6. import "package:photos/face/model/box.dart";
  7. import "package:photos/face/model/face.dart";
  8. import "package:photos/face/model/person.dart";
  9. import "package:photos/models/file/file.dart";
  10. import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart";
  11. import "package:photos/services/machine_learning/face_ml/person/person_service.dart";
  12. import "package:photos/ui/components/buttons/chip_button_widget.dart";
  13. import "package:photos/ui/components/info_item_widget.dart";
  14. import "package:photos/ui/viewer/file_details/face_widget.dart";
  15. import "package:photos/utils/face/face_box_crop.dart";
  16. import "package:photos/utils/thumbnail_util.dart";
  17. class FacesItemWidget extends StatefulWidget {
  18. final EnteFile file;
  19. const FacesItemWidget(this.file, {super.key});
  20. @override
  21. State<FacesItemWidget> createState() => _FacesItemWidgetState();
  22. }
  23. class _FacesItemWidgetState extends State<FacesItemWidget> {
  24. bool editMode = false;
  25. @override
  26. void initState() {
  27. super.initState();
  28. setState(() {});
  29. }
  30. @override
  31. Widget build(BuildContext context) {
  32. return InfoItemWidget(
  33. key: const ValueKey("Faces"),
  34. leadingIcon: Icons.face_retouching_natural_outlined,
  35. subtitleSection: _faceWidgets(context, widget.file, editMode),
  36. hasChipButtons: true,
  37. editOnTap: _toggleEditMode,
  38. );
  39. }
  40. void _toggleEditMode() {
  41. setState(() {
  42. editMode = !editMode;
  43. });
  44. }
  45. Future<List<Widget>> _faceWidgets(
  46. BuildContext context,
  47. EnteFile file,
  48. bool editMode,
  49. ) async {
  50. try {
  51. if (file.uploadedFileID == null) {
  52. return [
  53. const ChipButtonWidget(
  54. "File not uploaded yet",
  55. noChips: true,
  56. ),
  57. ];
  58. }
  59. final List<Face>? faces = await FaceMLDataDB.instance
  60. .getFacesForGivenFileID(file.uploadedFileID!);
  61. if (faces == null) {
  62. return [
  63. const ChipButtonWidget(
  64. "Image not analyzed",
  65. noChips: true,
  66. ),
  67. ];
  68. }
  69. // Remove faces with low scores
  70. if (!kDebugMode) {
  71. faces.removeWhere((face) => (face.score < 0.75));
  72. } else {
  73. faces.removeWhere((face) => (face.score < 0.5));
  74. }
  75. if (faces.isEmpty) {
  76. return [
  77. const ChipButtonWidget(
  78. "No faces found",
  79. noChips: true,
  80. ),
  81. ];
  82. }
  83. // TODO: add deduplication of faces of same person
  84. final faceIdsToClusterIds = await FaceMLDataDB.instance
  85. .getFaceIdsToClusterIds(faces.map((face) => face.faceID));
  86. final Map<String, PersonEntity> persons =
  87. await PersonService.instance.getPersonsMap();
  88. final clusterIDToPerson =
  89. await FaceMLDataDB.instance.getClusterIDToPersonID();
  90. // Sort faces by name and score
  91. final faceIdToPersonID = <String, String>{};
  92. for (final face in faces) {
  93. final clusterID = faceIdsToClusterIds[face.faceID];
  94. if (clusterID != null) {
  95. final personID = clusterIDToPerson[clusterID];
  96. if (personID != null) {
  97. faceIdToPersonID[face.faceID] = personID;
  98. }
  99. }
  100. }
  101. faces.sort((Face a, Face b) {
  102. final aPersonID = faceIdToPersonID[a.faceID];
  103. final bPersonID = faceIdToPersonID[b.faceID];
  104. if (aPersonID != null && bPersonID == null) {
  105. return -1;
  106. } else if (aPersonID == null && bPersonID != null) {
  107. return 1;
  108. } else {
  109. return b.score.compareTo(a.score);
  110. }
  111. });
  112. // Make sure hidden faces are last
  113. faces.sort((Face a, Face b) {
  114. final aIsHidden =
  115. persons[faceIdToPersonID[a.faceID]]?.data.isIgnored ?? false;
  116. final bIsHidden =
  117. persons[faceIdToPersonID[b.faceID]]?.data.isIgnored ?? false;
  118. if (aIsHidden && !bIsHidden) {
  119. return 1;
  120. } else if (!aIsHidden && bIsHidden) {
  121. return -1;
  122. } else {
  123. return 0;
  124. }
  125. });
  126. final lastViewedClusterID = ClusterFeedbackService.lastViewedClusterID;
  127. final faceWidgets = <FaceWidget>[];
  128. final faceCrops = getRelevantFaceCrops(faces);
  129. for (final Face face in faces) {
  130. final int? clusterID = faceIdsToClusterIds[face.faceID];
  131. final PersonEntity? person = clusterIDToPerson[clusterID] != null
  132. ? persons[clusterIDToPerson[clusterID]!]
  133. : null;
  134. final highlight =
  135. (clusterID == lastViewedClusterID) && (person == null);
  136. faceWidgets.add(
  137. FaceWidget(
  138. file,
  139. face,
  140. faceCrops: faceCrops,
  141. clusterID: clusterID,
  142. person: person,
  143. highlight: highlight,
  144. editMode: highlight ? editMode : false,
  145. ),
  146. );
  147. }
  148. return faceWidgets;
  149. } catch (e, s) {
  150. Logger("FacesItemWidget").info(e, s);
  151. return <FaceWidget>[];
  152. }
  153. }
  154. Future<Map<String, Uint8List>?> getRelevantFaceCrops(
  155. Iterable<Face> faces,
  156. ) async {
  157. try {
  158. final faceIdToCrop = <String, Uint8List>{};
  159. final facesWithoutCrops = <String, FaceBox>{};
  160. for (final face in faces) {
  161. final Uint8List? cachedFace = faceCropCache.get(face.faceID);
  162. if (cachedFace != null) {
  163. faceIdToCrop[face.faceID] = cachedFace;
  164. } else {
  165. final faceCropCacheFile = cachedFaceCropPath(face.faceID);
  166. if ((await faceCropCacheFile.exists())) {
  167. final data = await faceCropCacheFile.readAsBytes();
  168. faceCropCache.put(face.faceID, data);
  169. faceIdToCrop[face.faceID] = data;
  170. } else {
  171. facesWithoutCrops[face.faceID] = face.detection.box;
  172. }
  173. }
  174. }
  175. if (facesWithoutCrops.isEmpty) {
  176. return faceIdToCrop;
  177. }
  178. final result = await pool.withResource(
  179. () async => await getFaceCrops(
  180. widget.file,
  181. facesWithoutCrops,
  182. ),
  183. );
  184. if (result == null) {
  185. return (faceIdToCrop.isEmpty) ? null : faceIdToCrop;
  186. }
  187. for (final entry in result.entries) {
  188. final Uint8List? computedCrop = result[entry.key];
  189. if (computedCrop != null) {
  190. faceCropCache.put(entry.key, computedCrop);
  191. final faceCropCacheFile = cachedFaceCropPath(entry.key);
  192. faceCropCacheFile.writeAsBytes(computedCrop).ignore();
  193. faceIdToCrop[entry.key] = computedCrop;
  194. }
  195. }
  196. return (faceIdToCrop.isEmpty) ? null : faceIdToCrop;
  197. } catch (e, s) {
  198. dev.log(
  199. "Error getting face crops for faceIDs: ${faces.map((face) => face.faceID).toList()}",
  200. error: e,
  201. stackTrace: s,
  202. );
  203. return null;
  204. }
  205. }
  206. }