Browse Source

[mob] Faster face cropping method

laurenspriem 1 year ago
parent
commit
a09b71cc15

+ 106 - 33
mobile/lib/ui/viewer/file_details/face_widget.dart

@@ -1,4 +1,5 @@
 import "dart:developer" show log;
+import "dart:io" show Platform;
 import "dart:typed_data";
 
 import "package:flutter/material.dart";
@@ -9,6 +10,7 @@ import 'package:photos/models/file/file.dart';
 import "package:photos/services/search_service.dart";
 import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
 import "package:photos/ui/viewer/people/cluster_page.dart";
+import "package:photos/ui/viewer/people/cropped_face_image_view.dart";
 import "package:photos/ui/viewer/people/people_page.dart";
 import "package:photos/utils/face/face_box_crop.dart";
 import "package:photos/utils/thumbnail_util.dart";
@@ -29,11 +31,104 @@ class FaceWidget extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return FutureBuilder<Uint8List?>(
-      future: getFaceCrop(),
-      builder: (context, snapshot) {
-        if (snapshot.hasData) {
-          final ImageProvider imageProvider = MemoryImage(snapshot.data!);
+    if (Platform.isIOS) {
+      return FutureBuilder<Uint8List?>(
+        future: getFaceCrop(),
+        builder: (context, snapshot) {
+          if (snapshot.hasData) {
+            final ImageProvider imageProvider = MemoryImage(snapshot.data!);
+            return GestureDetector(
+              onTap: () async {
+                log(
+                  "FaceWidget is tapped, with person $person and clusterID $clusterID",
+                  name: "FaceWidget",
+                );
+                if (person == null && clusterID == null) {
+                  return;
+                }
+                if (person != null) {
+                  await Navigator.of(context).push(
+                    MaterialPageRoute(
+                      builder: (context) => PeoplePage(
+                        person: person!,
+                      ),
+                    ),
+                  );
+                } else if (clusterID != null) {
+                  final fileIdsToClusterIds =
+                      await FaceMLDataDB.instance.getFileIdToClusterIds();
+                  final files = await SearchService.instance.getAllFiles();
+                  final clusterFiles = files
+                      .where(
+                        (file) =>
+                            fileIdsToClusterIds[file.uploadedFileID]
+                                ?.contains(clusterID) ??
+                            false,
+                      )
+                      .toList();
+                  await Navigator.of(context).push(
+                    MaterialPageRoute(
+                      builder: (context) => ClusterPage(
+                        clusterFiles,
+                        cluserID: clusterID!,
+                      ),
+                    ),
+                  );
+                }
+              },
+              child: Column(
+                children: [
+                  ClipRRect(
+                    borderRadius:
+                        const BorderRadius.all(Radius.elliptical(16, 12)),
+                    child: SizedBox(
+                      width: 60,
+                      height: 60,
+                      child: Image(
+                        image: imageProvider,
+                        fit: BoxFit.cover,
+                      ),
+                    ),
+                  ),
+                  const SizedBox(height: 8),
+                  if (person != null)
+                    Text(
+                      person!.attr.name.trim(),
+                      style: Theme.of(context).textTheme.bodySmall,
+                      overflow: TextOverflow.ellipsis,
+                      maxLines: 1,
+                    ),
+                ],
+              ),
+            );
+          } else {
+            if (snapshot.connectionState == ConnectionState.waiting) {
+              return const ClipRRect(
+                borderRadius: BorderRadius.all(Radius.elliptical(16, 12)),
+                child: SizedBox(
+                  width: 60, // Ensure consistent sizing
+                  height: 60,
+                  child: CircularProgressIndicator(),
+                ),
+              );
+            }
+            if (snapshot.hasError) {
+              log('Error getting face: ${snapshot.error}');
+            }
+            return const ClipRRect(
+              borderRadius: BorderRadius.all(Radius.elliptical(16, 12)),
+              child: SizedBox(
+                width: 60, // Ensure consistent sizing
+                height: 60,
+                child: NoThumbnailWidget(),
+              ),
+            );
+          }
+        },
+      );
+    } else {
+      return Builder(
+        builder: (context) {
           return GestureDetector(
             onTap: () async {
               log(
@@ -81,9 +176,9 @@ class FaceWidget extends StatelessWidget {
                   child: SizedBox(
                     width: 60,
                     height: 60,
-                    child: Image(
-                      image: imageProvider,
-                      fit: BoxFit.cover,
+                    child: CroppedFaceImageView(
+                      enteFile: file,
+                      face: face,
                     ),
                   ),
                 ),
@@ -98,31 +193,9 @@ class FaceWidget extends StatelessWidget {
               ],
             ),
           );
-        } else {
-          if (snapshot.connectionState == ConnectionState.waiting) {
-            return const ClipRRect(
-              borderRadius: BorderRadius.all(Radius.elliptical(16, 12)),
-              child: SizedBox(
-                width: 60, // Ensure consistent sizing
-                height: 60,
-                child: CircularProgressIndicator(),
-              ),
-            );
-          }
-          if (snapshot.hasError) {
-            log('Error getting face: ${snapshot.error}');
-          }
-          return const ClipRRect(
-            borderRadius: BorderRadius.all(Radius.elliptical(16, 12)),
-            child: SizedBox(
-              width: 60, // Ensure consistent sizing
-              height: 60,
-              child: NoThumbnailWidget(),
-            ),
-          );
-        }
-      },
-    );
+        },
+      );
+    }
   }
 
   Future<Uint8List?> getFaceCrop() async {

+ 117 - 0
mobile/lib/ui/viewer/people/cropped_face_image_view.dart

@@ -0,0 +1,117 @@
+import 'dart:developer' show log;
+import "dart:io" show File;
+
+import 'package:flutter/material.dart';
+import "package:photos/face/model/face.dart";
+import "package:photos/models/file/file.dart";
+import "package:photos/ui/viewer/file/thumbnail_widget.dart";
+import "package:photos/utils/file_util.dart";
+
+class CroppedFaceInfo {
+  final Image image;
+  final double scale;
+  final double offsetX;
+  final double offsetY;
+
+  const CroppedFaceInfo({
+    required this.image,
+    required this.scale,
+    required this.offsetX,
+    required this.offsetY,
+  });
+}
+
+class CroppedFaceImageView extends StatelessWidget {
+  final EnteFile enteFile;
+  final Face face;
+
+  const CroppedFaceImageView({
+    Key? key,
+    required this.enteFile,
+    required this.face,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return FutureBuilder(
+      future: getImage(),
+      builder: (context, snapshot) {
+        if (snapshot.hasData) {
+          return LayoutBuilder(
+            builder: (BuildContext context, BoxConstraints constraints) {
+              final Image image = snapshot.data!;
+
+              final double viewWidth = constraints.maxWidth;
+              final double viewHeight = constraints.maxHeight;
+
+              final faceBox = face.detection.box;
+
+              final double relativeFaceCenterX =
+                  faceBox.xMin + faceBox.width / 2;
+              final double relativeFaceCenterY =
+                  faceBox.yMin + faceBox.height / 2;
+
+              const double desiredFaceHeightRelativeToWidget = 1 / 2;
+              final double scale =
+                  (1 / faceBox.height) * desiredFaceHeightRelativeToWidget;
+
+              final double widgetCenterX = viewWidth / 2;
+              final double widgetCenterY = viewHeight / 2;
+
+              final double imageAspectRatio = enteFile.width / enteFile.height;
+              final double widgetAspectRatio = viewWidth / viewHeight;
+              final double imageToWidgetRatio =
+                  imageAspectRatio / widgetAspectRatio;
+
+              double offsetX =
+                  (widgetCenterX - relativeFaceCenterX * viewWidth) * scale;
+              double offsetY =
+                  (widgetCenterY - relativeFaceCenterY * viewHeight) * scale;
+
+              if (imageAspectRatio > widgetAspectRatio) {
+                // Landscape Image: Adjust offsetX more conservatively
+                offsetX = offsetX * imageToWidgetRatio;
+              } else {
+                // Portrait Image: Adjust offsetY more conservatively
+                offsetY = offsetY / imageToWidgetRatio;
+              }
+
+              return ClipRect(
+                clipBehavior: Clip.antiAlias,
+                child: Transform.translate(
+                  offset: Offset(
+                    offsetX,
+                    offsetY,
+                  ),
+                  child: Transform.scale(
+                    scale: scale,
+                    child: image,
+                  ),
+                ),
+              );
+            },
+          );
+        } else {
+          if (snapshot.hasError) {
+            log('Error getting cover face for person: ${snapshot.error}');
+          }
+          return ThumbnailWidget(
+            enteFile,
+          );
+        }
+      },
+    );
+  }
+
+  Future<Image?> getImage() async {
+    final File? ioFile = await getFile(enteFile);
+    if (ioFile == null) {
+      return null;
+    }
+
+    final imageData = await ioFile.readAsBytes();
+    final image = Image.memory(imageData, fit: BoxFit.cover);
+
+    return image;
+  }
+}