From 599fa858f6c8478c1c64f580485b8510e6b940e6 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Fri, 27 Feb 2026 23:18:15 +0700 Subject: [PATCH] fix: improve TUI with View buttons and smooth out blur interpolation --- src/faceblur/app.py | 57 +++++++++++++++++++++++++++++++++++++------- src/faceblur/blur.py | 19 +++++++++++---- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/faceblur/app.py b/src/faceblur/app.py index 5e44c2a..748b171 100644 --- a/src/faceblur/app.py +++ b/src/faceblur/app.py @@ -337,6 +337,16 @@ class FaceSelectionScreen(Screen): .face-row { height: 3; margin-bottom: 0; + align: left middle; + } + + .face-checkbox { + width: 80%; + } + + .view-btn { + min-width: 8; + margin-left: 2; } #blur-method-label { @@ -391,14 +401,22 @@ class FaceSelectionScreen(Screen): sample_info = ( f" [dim]{self.face_samples[cluster.id]}[/dim]" ) - yield Checkbox( - f"Person {cluster.id + 1} " - f"({len(cluster.faces)} detections)" - f"{sample_info}", - value=True, - id=f"cluster-{cluster.id}", - classes="face-row", - ) + with Horizontal(classes="face-row"): + yield Checkbox( + f"Person {cluster.id + 1} " + f"({len(cluster.faces)} detections)" + f"{sample_info}", + value=True, + id=f"cluster-{cluster.id}", + classes="face-checkbox", + ) + if cluster.id in self.face_samples: + yield Button( + "View", + id=f"view-{cluster.id}", + variant="default", + classes="view-btn", + ) yield Label("Blur method:", id="blur-method-label") yield Select( [ @@ -424,6 +442,25 @@ class FaceSelectionScreen(Screen): self.app.pop_screen() elif event.button.id == "blur-btn": self._start_encoding() + elif event.button.id and event.button.id.startswith("view-"): + cluster_id = int(event.button.id.split("-")[1]) + if cluster_id in self.face_samples: + self._open_image(self.face_samples[cluster_id]) + + def _open_image(self, path: Path) -> None: + """Open image in default viewer.""" + import subprocess + import sys + + try: + if sys.platform == "darwin": + subprocess.run(["open", str(path)]) + elif sys.platform == "win32": + os.startfile(str(path)) + else: + subprocess.run(["xdg-open", str(path)]) + except Exception: + pass # Ignore if viewer fails to launch def _start_encoding(self) -> None: # Collect selected cluster IDs @@ -438,7 +475,9 @@ class FaceSelectionScreen(Screen): if cluster.id == -1: selected_ids.add(-1) - blur_method = self.query_one("#blur-method", Select).value + blur_method = str(self.query_one("#blur-method", Select).value) + if blur_method == "Select.NoSelection": + blur_method = "gaussian" # Generate output path stem = self.video_path.stem diff --git a/src/faceblur/blur.py b/src/faceblur/blur.py index 6a6a857..58e9dbe 100644 --- a/src/faceblur/blur.py +++ b/src/faceblur/blur.py @@ -145,13 +145,24 @@ def get_bboxes_for_frame( all_clusters = set(prev_by_cluster.keys()) | set(next_by_cluster.keys()) for cid in all_clusters: if cid in prev_by_cluster and cid in next_by_cluster: + # Face present in both: standard linear interpolation bbox = interpolate_bboxes(prev_by_cluster[cid], next_by_cluster[cid], t) result.append((cid, bbox)) elif cid in prev_by_cluster: - # Face leaving frame — use prev bbox (fade out handled by caller if desired) - result.append((cid, prev_by_cluster[cid])) + # Face leaving: shrink to center of previous bbox + x1, y1, x2, y2 = prev_by_cluster[cid] + cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 + center_bbox = (cx, cy, cx, cy) + # Interpolate from full bbox (t=0) to center point (t=1) + bbox = interpolate_bboxes(prev_by_cluster[cid], center_bbox, t) + result.append((cid, bbox)) else: - # Face entering frame — use next bbox - result.append((cid, next_by_cluster[cid])) + # Face entering: grow from center of next bbox + x1, y1, x2, y2 = next_by_cluster[cid] + cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 + center_bbox = (cx, cy, cx, cy) + # Interpolate from center point (t=0) to full bbox (t=1) + bbox = interpolate_bboxes(center_bbox, next_by_cluster[cid], t) + result.append((cid, bbox)) return result