[mob] Faster face cropping method

This commit is contained in:
laurenspriem 2024-03-23 17:02:22 +05:30
parent b1b3bcc534
commit a09b71cc15
2 changed files with 223 additions and 33 deletions

View file

@ -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 {

View file

@ -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;
}
}