Переглянути джерело

[mob] Add basic debug UI for breaking up cluster

laurenspriem 1 рік тому
батько
коміт
0176b01fea

+ 25 - 3
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<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 - 0
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<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,
+            ),
+          ),
+        ),
+      );
+    }
+  }
+}

+ 6 - 5
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<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(