diff --git a/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart b/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart index 3b9ca7383e..7a83c8d826 100644 --- a/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart +++ b/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart @@ -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>> 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 = >{}; + 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 value in clusterIdToFaceIds.values) { + allClusteredFaceIDsSet.addAll(value); + } + final clusterIDToNoiseFaceID = + originalFaceIDsSet.difference(allClusteredFaceIDsSet); + if (clusterIDToNoiseFaceID.isNotEmpty) { + clusterIdToFaceIds[-1] = clusterIDToNoiseFaceID.toList(); + } + return clusterIdToFaceIds; } diff --git a/mobile/lib/ui/viewer/people/cluster_app_bar.dart b/mobile/lib/ui/viewer/people/cluster_app_bar.dart new file mode 100644 index 0000000000..bc32c90883 --- /dev/null +++ b/mobile/lib/ui/viewer/people/cluster_app_bar.dart @@ -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 createState() => _AppBarWidgetState(); +} + +enum ClusterPopupAction { + setCover, + breakupCluster, + hide, +} + +class _AppBarWidgetState extends State { + 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().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 _getDefaultActions(BuildContext context) { + final List actions = []; + // If the user has selected files, don't show any actions + if (widget.selectedFiles.files.isNotEmpty || + !Configuration.instance.hasConfiguredAccount()) { + return actions; + } + + final List> 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 _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, + ), + ), + ), + ); + } + } +} diff --git a/mobile/lib/ui/viewer/people/cluster_page.dart b/mobile/lib/ui/viewer/people/cluster_page.dart index ca114db665..0717410f49 100644 --- a/mobile/lib/ui/viewer/people/cluster_page.dart +++ b/mobile/lib/ui/viewer/people/cluster_page.dart @@ -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 { 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(