Ver Fonte

[mob][photos] When cropping a face from an image, make the image a square and add some buffer around it

ashilkn há 1 ano atrás
pai
commit
8b236cde09
2 ficheiros alterados com 87 adições e 4 exclusões
  1. 20 0
      mobile/lib/face/model/box.dart
  2. 67 4
      mobile/lib/utils/face/face_util.dart

+ 20 - 0
mobile/lib/face/model/box.dart

@@ -41,3 +41,23 @@ class FaceBox {
         'height': height,
       };
 }
+
+/// Bounding box of a face.
+///
+/// [xMin] and [yMin] are the coordinates of the top left corner of the box, and
+/// [width] and [height] are the width and height of the box.
+///
+/// One unit is equal to one pixel in the original image.
+class FaceBoxImage {
+  final int xMin;
+  final int yMin;
+  final int width;
+  final int height;
+
+  FaceBoxImage({
+    required this.xMin,
+    required this.yMin,
+    required this.width,
+    required this.height,
+  });
+}

+ 67 - 4
mobile/lib/utils/face/face_util.dart

@@ -1,3 +1,4 @@
+import "dart:math";
 import "dart:typed_data";
 
 import "package:computer/computer.dart";
@@ -8,6 +9,7 @@ import "package:photos/face/model/box.dart";
 
 final _logger = Logger("FaceUtil");
 final _computer = Computer.shared();
+const _faceImageBufferFactor = 0.2;
 
 ///Convert img.Image to ui.Image and use RawImage to display.
 Future<List<img.Image>> generateImgFaceThumbnails(
@@ -74,16 +76,77 @@ Future<img.Image> decodeToImgImage(String imagePath) async {
 
 /// Returns an Image from 'package:image/image.dart'
 img.Image cropFaceBoxFromImage(img.Image image, FaceBox faceBox) {
+  final squareFaceBox = _getSquareFaceBoxImage(image, faceBox);
+  final squareFaceBoxWithBuffer =
+      _addBufferAroundFaceBox(squareFaceBox, _faceImageBufferFactor);
   return img.copyCrop(
     image,
-    x: (image.width * faceBox.xMin).round(),
-    y: (image.height * faceBox.yMin).round(),
-    width: (image.width * faceBox.width).round(),
-    height: (image.height * faceBox.height).round(),
+    x: squareFaceBoxWithBuffer.xMin,
+    y: squareFaceBoxWithBuffer.yMin,
+    width: squareFaceBoxWithBuffer.width,
+    height: squareFaceBoxWithBuffer.height,
     antialias: false,
   );
 }
 
+/// Returns a square face box image from the original image with
+/// side length equal to the maximum of the width and height of the face box in
+/// the OG image.
+FaceBoxImage _getSquareFaceBoxImage(img.Image image, FaceBox faceBox) {
+  final width = (image.width * faceBox.width).round();
+  final height = (image.height * faceBox.height).round();
+  final side = max(width, height);
+  final xImage = (image.width * faceBox.xMin).round();
+  final yImage = (image.height * faceBox.yMin).round();
+
+  if (height >= width) {
+    final xImageAdj = (xImage - (height - width) / 2).round();
+    return FaceBoxImage(
+      xMin: xImageAdj,
+      yMin: yImage,
+      width: side,
+      height: side,
+    );
+  } else {
+    final yImageAdj = (yImage - (width - height) / 2).round();
+    return FaceBoxImage(
+      xMin: xImage,
+      yMin: yImageAdj,
+      width: side,
+      height: side,
+    );
+  }
+}
+
+///To add some buffer around the face box so that the face isn't cropped
+///too close to the face.
+FaceBoxImage _addBufferAroundFaceBox(
+  FaceBoxImage faceBoxImage,
+  double bufferFactor,
+) {
+  final heightBuffer = faceBoxImage.height * bufferFactor;
+  final widthBuffer = faceBoxImage.width * bufferFactor;
+  final xMinWithBuffer = faceBoxImage.xMin - widthBuffer;
+  final yMinWithBuffer = faceBoxImage.yMin - heightBuffer;
+  final widthWithBuffer = faceBoxImage.width + 2 * widthBuffer;
+  final heightWithBuffer = faceBoxImage.height + 2 * heightBuffer;
+  //Do not add buffer if the top left edge of the image is out of bounds
+  //after adding the buffer.
+  if (xMinWithBuffer < 0 || yMinWithBuffer < 0) {
+    return faceBoxImage;
+  }
+  //Another similar case that can be handled is when the bottom right edge
+  //of the image is out of bounds after adding the buffer. But the
+  //the visual difference is not as significant as when the top left edge
+  //is out of bounds, so we are not handling that case.
+  return FaceBoxImage(
+    xMin: xMinWithBuffer.round(),
+    yMin: yMinWithBuffer.round(),
+    width: widthWithBuffer.round(),
+    height: heightWithBuffer.round(),
+  );
+}
+
 List<Uint8List> _encodeImagesToJpg(Map args) {
   final images = args["images"] as List<img.Image>;
   return images.map((img.Image image) => img.encodeJpg(image)).toList();