[mob] Faster face cropping method
This commit is contained in:
parent
b1b3bcc534
commit
a09b71cc15
2 changed files with 223 additions and 33 deletions
|
@ -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
mobile/lib/ui/viewer/people/cropped_face_image_view.dart
Normal file
117
mobile/lib/ui/viewer/people/cropped_face_image_view.dart
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue