Face thumbnail generation without canvas
This commit is contained in:
parent
8ef673fe58
commit
52b787f71e
3 changed files with 141 additions and 64 deletions
|
@ -16,15 +16,19 @@ Future<Map<String, Uint8List>?> getFaceCrops(
|
|||
EnteFile file,
|
||||
Map<String, FaceBox> faceBoxeMap,
|
||||
) async {
|
||||
late Uint8List? ioFileBytes;
|
||||
late String? imagePath;
|
||||
if (file.fileType != FileType.video) {
|
||||
final File? ioFile = await getFile(file);
|
||||
if (ioFile == null) {
|
||||
return null;
|
||||
}
|
||||
ioFileBytes = await ioFile.readAsBytes();
|
||||
imagePath = ioFile.path;
|
||||
} else {
|
||||
ioFileBytes = await getThumbnail(file);
|
||||
final thumbnail = await getThumbnailForUploadedFile(file);
|
||||
if (thumbnail == null) {
|
||||
return null;
|
||||
}
|
||||
imagePath = thumbnail.path;
|
||||
}
|
||||
final List<String> faceIds = [];
|
||||
final List<FaceBox> faceBoxes = [];
|
||||
|
@ -34,7 +38,7 @@ Future<Map<String, Uint8List>?> getFaceCrops(
|
|||
}
|
||||
final List<Uint8List> faceCrop =
|
||||
await ImageMlIsolate.instance.generateFaceThumbnailsForImage(
|
||||
ioFileBytes!,
|
||||
imagePath,
|
||||
faceBoxes,
|
||||
);
|
||||
final Map<String, Uint8List> result = {};
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import 'dart:async';
|
||||
import "dart:io" show File;
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data' show Float32List, Uint8List;
|
||||
import 'dart:ui';
|
||||
|
||||
import "package:flutter/rendering.dart";
|
||||
import 'package:flutter_isolate/flutter_isolate.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/face/model/box.dart";
|
||||
|
@ -13,13 +15,13 @@ import "package:photos/utils/image_ml_util.dart";
|
|||
import "package:synchronized/synchronized.dart";
|
||||
|
||||
enum ImageOperation {
|
||||
@Deprecated("No longer using BlazeFace`")
|
||||
preprocessBlazeFace,
|
||||
preprocessYoloOnnx,
|
||||
preprocessFaceAlign,
|
||||
preprocessMobileFaceNet,
|
||||
preprocessMobileFaceNetOnnx,
|
||||
generateFaceThumbnail,
|
||||
generateFaceThumbnailsForImage,
|
||||
generateFaceThumbnails,
|
||||
cropAndPadFace,
|
||||
}
|
||||
|
||||
|
@ -205,23 +207,14 @@ class ImageMlIsolate {
|
|||
'originalWidth': originalSize.width,
|
||||
'originalHeight': originalSize.height,
|
||||
});
|
||||
case ImageOperation.generateFaceThumbnail:
|
||||
final imageData = args['imageData'] as Uint8List;
|
||||
final faceDetectionJson =
|
||||
args['faceDetection'] as Map<String, dynamic>;
|
||||
final faceDetection =
|
||||
FaceDetectionRelative.fromJson(faceDetectionJson);
|
||||
final Uint8List result =
|
||||
await generateFaceThumbnailFromData(imageData, faceDetection);
|
||||
sendPort.send(<dynamic>[result]);
|
||||
case ImageOperation.generateFaceThumbnailsForImage:
|
||||
final imageData = args['imageData'] as Uint8List;
|
||||
case ImageOperation.generateFaceThumbnails:
|
||||
final imagePath = args['imagePath'] as String;
|
||||
final Uint8List imageData = await File(imagePath).readAsBytes();
|
||||
final faceBoxesJson =
|
||||
args['faceBoxesList'] as List<Map<String, dynamic>>;
|
||||
final List<FaceBox> faceBoxes =
|
||||
faceBoxesJson.map((json) => FaceBox.fromJson(json)).toList();
|
||||
final List<Uint8List> results =
|
||||
await generateFaceThumbnailsFromDataAndDetections(
|
||||
final List<Uint8List> results = await generateFaceThumbnails(
|
||||
imageData,
|
||||
faceBoxes,
|
||||
);
|
||||
|
@ -471,44 +464,28 @@ class ImageMlIsolate {
|
|||
return (inputs, alignmentResults, isBlurs, blurValues, originalSize);
|
||||
}
|
||||
|
||||
/// Generates a face thumbnail from [imageData] and a [faceDetection].
|
||||
///
|
||||
/// Uses [generateFaceThumbnailFromData] inside the isolate.
|
||||
Future<Uint8List> generateFaceThumbnail(
|
||||
Uint8List imageData,
|
||||
FaceDetectionRelative faceDetection,
|
||||
) async {
|
||||
return await _runInIsolate(
|
||||
(
|
||||
ImageOperation.generateFaceThumbnail,
|
||||
{
|
||||
'imageData': imageData,
|
||||
'faceDetection': faceDetection.toJson(),
|
||||
},
|
||||
),
|
||||
).then((value) => value[0] as Uint8List);
|
||||
}
|
||||
|
||||
/// Generates face thumbnails for all [faceBoxes] in [imageData].
|
||||
///
|
||||
/// Uses [generateFaceThumbnailsFromDataAndDetections] inside the isolate.
|
||||
/// Uses [generateFaceThumbnails] inside the isolate.
|
||||
Future<List<Uint8List>> generateFaceThumbnailsForImage(
|
||||
Uint8List imageData,
|
||||
String imagePath,
|
||||
List<FaceBox> faceBoxes,
|
||||
) async {
|
||||
final List<Map<String, dynamic>> faceBoxesJson =
|
||||
faceBoxes.map((box) => box.toJson()).toList();
|
||||
return await _runInIsolate(
|
||||
(
|
||||
ImageOperation.generateFaceThumbnailsForImage,
|
||||
ImageOperation.generateFaceThumbnails,
|
||||
{
|
||||
'imageData': imageData,
|
||||
'imagePath': imagePath,
|
||||
'faceBoxesList': faceBoxesJson,
|
||||
},
|
||||
),
|
||||
).then((value) => value.cast<Uint8List>());
|
||||
}
|
||||
|
||||
@Deprecated('For second pass of BlazeFace, no longer used')
|
||||
|
||||
/// Generates cropped and padded image data from [imageData] and a [faceBox].
|
||||
///
|
||||
/// The steps are:
|
||||
|
|
|
@ -15,6 +15,7 @@ import "dart:ui";
|
|||
// ImageConfiguration;
|
||||
// import 'package:flutter/material.dart' as material show Image;
|
||||
import 'package:flutter/painting.dart' as paint show decodeImageFromList;
|
||||
import 'package:image/image.dart' as img_lib;
|
||||
import 'package:ml_linalg/linalg.dart';
|
||||
import "package:photos/face/model/box.dart";
|
||||
import 'package:photos/models/ml/ml_typedefs.dart';
|
||||
|
@ -36,7 +37,7 @@ Color readPixelColor(
|
|||
if (x < 0 || x >= image.width || y < 0 || y >= image.height) {
|
||||
// throw ArgumentError('Invalid pixel coordinates.');
|
||||
log('[WARNING] `readPixelColor`: Invalid pixel coordinates, out of bounds');
|
||||
return const Color(0x00000000);
|
||||
return const Color.fromARGB(255, 208, 16, 208);
|
||||
}
|
||||
assert(byteData.lengthInBytes == 4 * image.width * image.height);
|
||||
|
||||
|
@ -44,12 +45,39 @@ Color readPixelColor(
|
|||
return Color(_rgbaToArgb(byteData.getUint32(byteOffset)));
|
||||
}
|
||||
|
||||
void setPixelColor(
|
||||
Size imageSize,
|
||||
ByteData byteData,
|
||||
int x,
|
||||
int y,
|
||||
Color color,
|
||||
) {
|
||||
if (x < 0 || x >= imageSize.width || y < 0 || y >= imageSize.height) {
|
||||
log('[WARNING] `setPixelColor`: Invalid pixel coordinates, out of bounds');
|
||||
return;
|
||||
}
|
||||
assert(byteData.lengthInBytes == 4 * imageSize.width * imageSize.height);
|
||||
|
||||
final int byteOffset = 4 * (imageSize.width.toInt() * y + x);
|
||||
byteData.setUint32(byteOffset, _argbToRgba(color.value));
|
||||
}
|
||||
|
||||
int _rgbaToArgb(int rgbaColor) {
|
||||
final int a = rgbaColor & 0xFF;
|
||||
final int rgb = rgbaColor >> 8;
|
||||
return rgb + (a << 24);
|
||||
}
|
||||
|
||||
int _argbToRgba(int argbColor) {
|
||||
final int r = (argbColor >> 16) & 0xFF;
|
||||
final int g = (argbColor >> 8) & 0xFF;
|
||||
final int b = argbColor & 0xFF;
|
||||
final int a = (argbColor >> 24) & 0xFF;
|
||||
return (r << 24) + (g << 16) + (b << 8) + a;
|
||||
}
|
||||
|
||||
@Deprecated('Used in TensorFlow Lite only, no longer needed')
|
||||
|
||||
/// Creates an empty matrix with the specified shape.
|
||||
///
|
||||
/// The `shape` argument must be a list of length 2 or 3, where the first
|
||||
|
@ -464,11 +492,53 @@ Future<Image> resizeAndCenterCropImage(
|
|||
return resizedImage;
|
||||
}
|
||||
|
||||
/// Crops an [image] based on the specified [x], [y], [width] and [height].
|
||||
Future<Uint8List> cropImage(
|
||||
Image image,
|
||||
ByteData imgByteData, {
|
||||
required int x,
|
||||
required int y,
|
||||
required int width,
|
||||
required int height,
|
||||
}) async {
|
||||
// final newByteData = ByteData(width * height * 4);
|
||||
// for (var h = y; h < y + height; h++) {
|
||||
// for (var w = x; w < x + width; w++) {
|
||||
// final pixel = readPixelColor(image, imgByteData, w, y);
|
||||
// setPixelColor(
|
||||
// Size(width.toDouble(), height.toDouble()),
|
||||
// newByteData,
|
||||
// w,
|
||||
// h,
|
||||
// pixel,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// final newImage =
|
||||
// decodeImageFromRgbaBytes(newByteData.buffer.asUint8List(), width, height);
|
||||
|
||||
final newImage = img_lib.Image(width: width, height: height);
|
||||
|
||||
for (var h = y; h < y + height; h++) {
|
||||
for (var w = x; w < x + width; w++) {
|
||||
final pixel = readPixelColor(image, imgByteData, w, h);
|
||||
newImage.setPixel(
|
||||
w - x,
|
||||
h - y,
|
||||
img_lib.ColorRgb8(pixel.red, pixel.green, pixel.blue),
|
||||
);
|
||||
}
|
||||
}
|
||||
final newImageDataPng = img_lib.encodePng(newImage);
|
||||
|
||||
return newImageDataPng;
|
||||
}
|
||||
|
||||
/// Crops an [image] based on the specified [x], [y], [width] and [height].
|
||||
/// Optionally, the cropped image can be resized to comply with a [maxSize] and/or [minSize].
|
||||
/// Optionally, the cropped image can be rotated from the center by [rotation] radians.
|
||||
/// Optionally, the [quality] of the resizing interpolation can be specified.
|
||||
Future<Image> cropImage(
|
||||
Future<Image> cropImageWithCanvas(
|
||||
Image image, {
|
||||
required double x,
|
||||
required double y,
|
||||
|
@ -798,7 +868,7 @@ Future<List<Uint8List>> preprocessFaceAlignToUint8List(
|
|||
continue;
|
||||
}
|
||||
final alignmentBox = getAlignedFaceBox(alignmentResult);
|
||||
final Image alignedFace = await cropImage(
|
||||
final Image alignedFace = await cropImageWithCanvas(
|
||||
image,
|
||||
x: alignmentBox[0],
|
||||
y: alignmentBox[1],
|
||||
|
@ -886,7 +956,7 @@ Future<
|
|||
continue;
|
||||
}
|
||||
final alignmentBox = getAlignedFaceBox(alignmentResult);
|
||||
final Image alignedFace = await cropImage(
|
||||
final Image alignedFace = await cropImageWithCanvas(
|
||||
image,
|
||||
x: alignmentBox[0],
|
||||
y: alignmentBox[1],
|
||||
|
@ -970,7 +1040,7 @@ Future<(Float32List, List<AlignmentResult>, List<bool>, List<double>, Size)>
|
|||
continue;
|
||||
}
|
||||
final alignmentBox = getAlignedFaceBox(alignmentResult);
|
||||
final Image alignedFace = await cropImage(
|
||||
final Image alignedFace = await cropImageWithCanvas(
|
||||
image,
|
||||
x: alignmentBox[0],
|
||||
y: alignmentBox[1],
|
||||
|
@ -1168,29 +1238,52 @@ void warpAffineFloat32List(
|
|||
}
|
||||
}
|
||||
|
||||
/// Generates a face thumbnail from [imageData] and a [faceDetection].
|
||||
///
|
||||
/// Returns a [Uint8List] image, in png format.
|
||||
Future<Uint8List> generateFaceThumbnailFromData(
|
||||
Future<List<Uint8List>> generateFaceThumbnails(
|
||||
Uint8List imageData,
|
||||
FaceDetectionRelative faceDetection,
|
||||
List<FaceBox> faceBoxes,
|
||||
) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
final Image image = await decodeImageFromData(imageData);
|
||||
final ByteData imgByteData = await getByteDataFromImage(image);
|
||||
|
||||
final Image faceThumbnail = await cropImage(
|
||||
image,
|
||||
x: (faceDetection.xMinBox * image.width).round() - 20,
|
||||
y: (faceDetection.yMinBox * image.height).round() - 30,
|
||||
width: (faceDetection.width * image.width).round() + 40,
|
||||
height: (faceDetection.height * image.height).round() + 60,
|
||||
);
|
||||
// int i = 0;
|
||||
try {
|
||||
final List<Uint8List> faceThumbnails = [];
|
||||
|
||||
return await encodeImageToUint8List(
|
||||
faceThumbnail,
|
||||
format: ImageByteFormat.png,
|
||||
);
|
||||
for (final faceBox in faceBoxes) {
|
||||
final xCrop = (faceBox.x - faceBox.width / 2).round();
|
||||
final yCrop = (faceBox.y - faceBox.height / 2).round();
|
||||
final widthCrop = (faceBox.width * 2).round();
|
||||
final heightCrop = (faceBox.height * 2).round();
|
||||
final Uint8List faceThumbnail = await cropImage(
|
||||
image,
|
||||
imgByteData,
|
||||
x: xCrop,
|
||||
y: yCrop,
|
||||
width: widthCrop,
|
||||
height: heightCrop,
|
||||
);
|
||||
// final Uint8List faceThumbnailPng = await encodeImageToUint8List(
|
||||
// faceThumbnail,
|
||||
// format: ImageByteFormat.png,
|
||||
// );
|
||||
faceThumbnails.add(faceThumbnail);
|
||||
// i++;
|
||||
}
|
||||
stopwatch.stop();
|
||||
log('Face thumbnail generation took: ${stopwatch.elapsedMilliseconds} ms');
|
||||
|
||||
return faceThumbnails;
|
||||
} catch (e, s) {
|
||||
log('[ImageMlUtils] Error generating face thumbnails: $e, \n stackTrace: $s');
|
||||
// log('[ImageMlUtils] cropImage problematic input argument: ${faceBoxes[i]}');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Old method using canvas, replaced by `generateFaceThumbnails`")
|
||||
|
||||
/// Generates a face thumbnail from [imageData] and a [faceDetection].
|
||||
///
|
||||
/// Returns a [Uint8List] image, in png format.
|
||||
|
@ -1205,7 +1298,7 @@ Future<List<Uint8List>> generateFaceThumbnailsFromDataAndDetections(
|
|||
final List<Uint8List> faceThumbnails = [];
|
||||
|
||||
for (final faceBox in faceBoxes) {
|
||||
final Image faceThumbnail = await cropImage(
|
||||
final Image faceThumbnail = await cropImageWithCanvas(
|
||||
image,
|
||||
x: faceBox.x - faceBox.width / 2,
|
||||
y: faceBox.y - faceBox.height / 2,
|
||||
|
@ -1227,6 +1320,8 @@ Future<List<Uint8List>> generateFaceThumbnailsFromDataAndDetections(
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated('For second pass of BlazeFace, no longer used')
|
||||
|
||||
/// Generates cropped and padded image data from [imageData] and a [faceBox].
|
||||
///
|
||||
/// The steps are:
|
||||
|
@ -1241,7 +1336,7 @@ Future<Uint8List> cropAndPadFaceData(
|
|||
) async {
|
||||
final Image image = await decodeImageFromData(imageData);
|
||||
|
||||
final Image faceCrop = await cropImage(
|
||||
final Image faceCrop = await cropImageWithCanvas(
|
||||
image,
|
||||
x: (faceBox[0] * image.width),
|
||||
y: (faceBox[1] * image.height),
|
||||
|
@ -1390,6 +1485,7 @@ Color getPixelBicubic(num fx, num fy, Image image, ByteData byteDataRgba) {
|
|||
return Color.fromRGBO(c0, c1, c2, 1.0);
|
||||
}
|
||||
|
||||
@Deprecated('Old method only used in other deprecated methods')
|
||||
List<double> getAlignedFaceBox(AlignmentResult alignment) {
|
||||
final List<double> box = [
|
||||
// [xMinBox, yMinBox, xMaxBox, yMaxBox]
|
||||
|
|
Loading…
Add table
Reference in a new issue