|
@@ -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();
|