[mob] Add basic debug UI for breaking up cluster
This commit is contained in:
parent
15f9176208
commit
0176b01fea
3 changed files with 227 additions and 8 deletions
|
@ -390,7 +390,7 @@ class ClusterFeedbackService {
|
|||
return true;
|
||||
}
|
||||
|
||||
// TODO: iterate over this method and actually use it
|
||||
// TODO: iterate over this method to find sweet spot
|
||||
Future<Map<int, List<String>>> breakUpCluster(
|
||||
int clusterID, {
|
||||
useDbscan = true,
|
||||
|
@ -398,6 +398,7 @@ class ClusterFeedbackService {
|
|||
final faceMlDb = FaceMLDataDB.instance;
|
||||
|
||||
final faceIDs = await faceMlDb.getFaceIDsForCluster(clusterID);
|
||||
final originalFaceIDsSet = faceIDs.toSet();
|
||||
final fileIDs = faceIDs.map((e) => getFileIdFromFaceId(e)).toList();
|
||||
|
||||
final embeddings = await faceMlDb.getFaceEmbeddingMapForFile(fileIDs);
|
||||
|
@ -411,8 +412,8 @@ class ClusterFeedbackService {
|
|||
final dbscanClusters = await FaceClustering.instance.predictDbscan(
|
||||
embeddings,
|
||||
fileIDToCreationTime: fileIDToCreationTime,
|
||||
eps: 0.25,
|
||||
minPts: 4,
|
||||
eps: 0.30,
|
||||
minPts: 5,
|
||||
);
|
||||
|
||||
if (dbscanClusters.isEmpty) {
|
||||
|
@ -460,6 +461,27 @@ class ClusterFeedbackService {
|
|||
'Broke up cluster $clusterID into $amountOfNewClusters clusters \n ${clusterIdToCount.toString()}',
|
||||
);
|
||||
|
||||
final clusterIdToDisplayNames = <int, List<String>>{};
|
||||
if (kDebugMode) {
|
||||
for (final entry in clusterIdToFaceIds.entries) {
|
||||
final faceIDs = entry.value;
|
||||
final fileIDs = faceIDs.map((e) => getFileIdFromFaceId(e)).toList();
|
||||
final files = await FilesDB.instance.getFilesFromIDs(fileIDs);
|
||||
final displayNames = files.values.map((e) => e.displayName).toList();
|
||||
clusterIdToDisplayNames[entry.key] = displayNames;
|
||||
}
|
||||
}
|
||||
|
||||
final Set allClusteredFaceIDsSet = {};
|
||||
for (final List<String> value in clusterIdToFaceIds.values) {
|
||||
allClusteredFaceIDsSet.addAll(value);
|
||||
}
|
||||
final clusterIDToNoiseFaceID =
|
||||
originalFaceIDsSet.difference(allClusteredFaceIDsSet);
|
||||
if (clusterIDToNoiseFaceID.isNotEmpty) {
|
||||
clusterIdToFaceIds[-1] = clusterIDToNoiseFaceID.toList();
|
||||
}
|
||||
|
||||
return clusterIdToFaceIds;
|
||||
}
|
||||
|
||||
|
|
196
mobile/lib/ui/viewer/people/cluster_app_bar.dart
Normal file
196
mobile/lib/ui/viewer/people/cluster_app_bar.dart
Normal file
|
@ -0,0 +1,196 @@
|
|||
import 'dart:async';
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import "package:photos/db/files_db.dart";
|
||||
// import "package:photos/events/people_changed_event.dart";
|
||||
import 'package:photos/events/subscription_purchased_event.dart';
|
||||
// import "package:photos/face/db.dart";
|
||||
import "package:photos/face/model/person.dart";
|
||||
import 'package:photos/models/gallery_type.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/machine_learning/face_ml/face_ml_result.dart";
|
||||
import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart";
|
||||
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
|
||||
import "package:photos/ui/viewer/people/cluster_page.dart";
|
||||
// import "package:photos/utils/dialog_util.dart";
|
||||
|
||||
class ClusterAppBar extends StatefulWidget {
|
||||
final GalleryType type;
|
||||
final String? title;
|
||||
final SelectedFiles selectedFiles;
|
||||
final int clusterID;
|
||||
final Person? person;
|
||||
|
||||
const ClusterAppBar(
|
||||
this.type,
|
||||
this.title,
|
||||
this.selectedFiles,
|
||||
this.clusterID, {
|
||||
this.person,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ClusterAppBar> createState() => _AppBarWidgetState();
|
||||
}
|
||||
|
||||
enum ClusterPopupAction {
|
||||
setCover,
|
||||
breakupCluster,
|
||||
hide,
|
||||
}
|
||||
|
||||
class _AppBarWidgetState extends State<ClusterAppBar> {
|
||||
final _logger = Logger("_AppBarWidgetState");
|
||||
late StreamSubscription _userAuthEventSubscription;
|
||||
late Function() _selectedFilesListener;
|
||||
String? _appBarTitle;
|
||||
late CollectionActions collectionActions;
|
||||
final GlobalKey shareButtonKey = GlobalKey();
|
||||
bool isQuickLink = false;
|
||||
late GalleryType galleryType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedFilesListener = () {
|
||||
setState(() {});
|
||||
};
|
||||
collectionActions = CollectionActions(CollectionsService.instance);
|
||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||
_userAuthEventSubscription =
|
||||
Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
|
||||
setState(() {});
|
||||
});
|
||||
_appBarTitle = widget.title;
|
||||
galleryType = widget.type;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_userAuthEventSubscription.cancel();
|
||||
widget.selectedFiles.removeListener(_selectedFilesListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
_appBarTitle!,
|
||||
style:
|
||||
Theme.of(context).textTheme.headlineSmall!.copyWith(fontSize: 16),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
actions: kDebugMode ? _getDefaultActions(context) : null,
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _getDefaultActions(BuildContext context) {
|
||||
final List<Widget> actions = <Widget>[];
|
||||
// If the user has selected files, don't show any actions
|
||||
if (widget.selectedFiles.files.isNotEmpty ||
|
||||
!Configuration.instance.hasConfiguredAccount()) {
|
||||
return actions;
|
||||
}
|
||||
|
||||
final List<PopupMenuItem<ClusterPopupAction>> items = [];
|
||||
|
||||
items.addAll(
|
||||
[
|
||||
// PopupMenuItem(
|
||||
// value: ClusterPopupAction.setCover,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.image_outlined),
|
||||
// const Padding(
|
||||
// padding: EdgeInsets.all(8),
|
||||
// ),
|
||||
// Text(S.of(context).setCover),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
const PopupMenuItem(
|
||||
value: ClusterPopupAction.breakupCluster,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.analytics_outlined),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text('Break up cluster'),
|
||||
],
|
||||
),
|
||||
),
|
||||
// PopupMenuItem(
|
||||
// value: ClusterPopupAction.hide,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.visibility_off_outlined),
|
||||
// const Padding(
|
||||
// padding: EdgeInsets.all(8),
|
||||
// ),
|
||||
// Text(S.of(context).hide),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
|
||||
if (items.isNotEmpty) {
|
||||
actions.add(
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return items;
|
||||
},
|
||||
onSelected: (ClusterPopupAction value) async {
|
||||
if (value == ClusterPopupAction.breakupCluster) {
|
||||
// ignore: unawaited_futures
|
||||
await _breakUpCluster(context);
|
||||
}
|
||||
// else if (value == ClusterPopupAction.setCover) {
|
||||
// await setCoverPhoto(context);
|
||||
// } else if (value == ClusterPopupAction.hide) {
|
||||
// // ignore: unawaited_futures
|
||||
// }
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
Future<void> _breakUpCluster(BuildContext context) async {
|
||||
final newClusterIDToFaceIDs =
|
||||
await ClusterFeedbackService.instance.breakUpCluster(widget.clusterID);
|
||||
|
||||
for (final cluster in newClusterIDToFaceIDs.entries) {
|
||||
// ignore: unawaited_futures
|
||||
final newClusterID = cluster.key;
|
||||
final faceIDs = cluster.value;
|
||||
final files = await FilesDB.instance
|
||||
.getFilesFromIDs(faceIDs.map((e) => getFileIdFromFaceId(e)).toList());
|
||||
unawaited(
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ClusterPage(
|
||||
files.values.toList(),
|
||||
appendTitle:
|
||||
(newClusterID == -1) ? "(Analysis noise)" : "(Analysis)",
|
||||
clusterID: newClusterID,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,8 +15,8 @@ import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedba
|
|||
import "package:photos/ui/components/notification_widget.dart";
|
||||
import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart';
|
||||
import 'package:photos/ui/viewer/gallery/gallery.dart';
|
||||
import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
|
||||
import "package:photos/ui/viewer/people/add_person_action_sheet.dart";
|
||||
import "package:photos/ui/viewer/people/cluster_app_bar.dart";
|
||||
import "package:photos/ui/viewer/people/people_page.dart";
|
||||
import "package:photos/ui/viewer/search/result/search_result_page.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
@ -28,6 +28,7 @@ class ClusterPage extends StatefulWidget {
|
|||
final String tagPrefix;
|
||||
final int clusterID;
|
||||
final Person? personID;
|
||||
final String appendTitle;
|
||||
|
||||
static const GalleryType appBarType = GalleryType.cluster;
|
||||
static const GalleryType overlayType = GalleryType.cluster;
|
||||
|
@ -38,6 +39,7 @@ class ClusterPage extends StatefulWidget {
|
|||
this.tagPrefix = "",
|
||||
required this.clusterID,
|
||||
this.personID,
|
||||
this.appendTitle = "",
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -107,12 +109,11 @@ class _ClusterPageState extends State<ClusterPage> {
|
|||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(50.0),
|
||||
child: GalleryAppBarWidget(
|
||||
child: ClusterAppBar(
|
||||
SearchResultPage.appBarType,
|
||||
widget.personID != null
|
||||
? widget.personID!.attr.name
|
||||
: "${widget.searchResult.length} memories",
|
||||
"${widget.searchResult.length} memories${widget.appendTitle}",
|
||||
_selectedFiles,
|
||||
widget.clusterID,
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
|
|
Loading…
Add table
Reference in a new issue