Skip to content

Commit ea8586c

Browse files
add mask text label
Co-Authored-By: Jenna Tomkinson <107513215+jenna-tomkinson@users.noreply.github.com>
1 parent 461eb7b commit ea8586c

2 files changed

Lines changed: 160 additions & 5 deletions

File tree

src/cytodataframe/frame.py

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,11 +1332,41 @@ def _find_matching_segmentation_path(
13321332
return matching_files[0] if matching_files else None
13331333

13341334
for file_pattern, original_pattern in pattern_map.items():
1335-
if not re.search(original_pattern, data_value):
1335+
matched = re.search(original_pattern, data_value)
1336+
if not matched:
13361337
continue
1337-
for file in sorted(root.rglob("*")):
1338-
if re.search(file_pattern, file.name):
1339-
return file
1338+
identifiers: list[str] = []
1339+
identifiers.extend(
1340+
str(group)
1341+
for group in matched.groups()
1342+
if isinstance(group, str) and group.strip()
1343+
)
1344+
identifiers.extend(
1345+
[
1346+
pathlib.Path(data_value).stem,
1347+
pathlib.Path(candidate_path).stem,
1348+
]
1349+
)
1350+
identifiers = list(dict.fromkeys(idf for idf in identifiers if idf))
1351+
1352+
candidate_roots: list[pathlib.Path] = []
1353+
parent_name = pathlib.Path(candidate_path).parent.name
1354+
if parent_name:
1355+
parent_scoped_root = root / parent_name
1356+
if parent_scoped_root.exists():
1357+
candidate_roots.append(parent_scoped_root)
1358+
candidate_roots.append(root)
1359+
1360+
for search_root in candidate_roots:
1361+
matching_files = [
1362+
file
1363+
for file in sorted(search_root.rglob("*"))
1364+
if file.is_file()
1365+
and re.search(file_pattern, file.name)
1366+
and any(idf in file.stem for idf in identifiers)
1367+
]
1368+
if matching_files:
1369+
return matching_files[0]
13401370
return None
13411371

13421372
def _prepare_3d_label_overlay(
@@ -2619,7 +2649,7 @@ def _resolve_plotter_window_height(
26192649
return int(height_digits.group(0))
26202650
return 300
26212651

2622-
def _add_label_overlay_toggle_control(
2652+
def _add_label_overlay_toggle_control( # noqa: C901
26232653
self: CytoDataFrame_type,
26242654
plotter: Any,
26252655
overlay_actors: List[Any],
@@ -2654,6 +2684,80 @@ def _toggle_overlay(state: Any) -> None:
26542684
size=size,
26552685
position=position,
26562686
)
2687+
label_text = str(display_options.get("label_overlay_toggle_label", "Mask"))
2688+
label_font_size = int(
2689+
display_options.get("label_overlay_toggle_font_size", 9)
2690+
)
2691+
label_gap = int(display_options.get("label_overlay_toggle_label_gap", 24))
2692+
label_shift_left = int(
2693+
display_options.get("label_overlay_toggle_label_shift_left", 212)
2694+
)
2695+
estimated_text_px = int(max(32, len(label_text) * label_font_size * 0.95))
2696+
label_pos = (
2697+
max(
2698+
0,
2699+
int(position[0]) - estimated_text_px - label_gap - label_shift_left,
2700+
),
2701+
max(0, int(position[1]) + 10),
2702+
)
2703+
window_size = getattr(plotter, "window_size", None)
2704+
window_width = 300
2705+
if (
2706+
isinstance(window_size, (tuple, list))
2707+
and len(window_size) >= MIN_POSITION_COMPONENTS
2708+
and isinstance(window_size[0], (int, float))
2709+
):
2710+
window_width = int(window_size[0])
2711+
window_width = max(1, window_width)
2712+
window_height = max(
2713+
1,
2714+
self._resolve_plotter_window_height(
2715+
plotter=plotter,
2716+
display_options=display_options,
2717+
),
2718+
)
2719+
label_pos_norm = (
2720+
max(0.01, min(0.95, float(label_pos[0]) / float(window_width))),
2721+
max(0.01, min(0.95, float(label_pos[1]) / float(window_height))),
2722+
)
2723+
label_name = f"cdf-label-toggle-{uuid.uuid4().hex}"
2724+
text_added = False
2725+
try:
2726+
plotter.add_text(
2727+
label_text,
2728+
position=label_pos_norm,
2729+
font_size=label_font_size,
2730+
color="white",
2731+
name=label_name,
2732+
viewport=True,
2733+
shadow=True,
2734+
)
2735+
text_added = True
2736+
except Exception:
2737+
pass
2738+
if not text_added:
2739+
try:
2740+
plotter.add_text(
2741+
label_text,
2742+
position=label_pos,
2743+
font_size=label_font_size,
2744+
color="white",
2745+
name=label_name,
2746+
shadow=True,
2747+
)
2748+
text_added = True
2749+
except Exception:
2750+
pass
2751+
if not text_added:
2752+
with contextlib.suppress(Exception):
2753+
plotter.add_text(
2754+
label_text,
2755+
position="lower_right",
2756+
font_size=label_font_size,
2757+
color="white",
2758+
name=label_name,
2759+
shadow=True,
2760+
)
26572761
logger.debug("Added 3D label overlay toggle checkbox to plotter view.")
26582762

26592763
def _toggle_overlay_actors_visibility(

tests/test_frame.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,45 @@ def test_get_3d_label_overlay_from_cell_applies_bbox_crop(
360360
assert overlay.max() == 255
361361

362362

363+
def test_find_matching_segmentation_path_filters_by_image_identifier(
364+
tmp_path: pathlib.Path,
365+
) -> None:
366+
mask_dir = tmp_path / "masks"
367+
mask_dir.mkdir()
368+
(mask_dir / "img_a_mask.tiff").write_bytes(b"")
369+
(mask_dir / "img_b_mask.tiff").write_bytes(b"")
370+
371+
matched = CytoDataFrame._find_matching_segmentation_path(
372+
data_value="img_a.tiff",
373+
pattern_map={r".*_mask\.tiff$": r".*"},
374+
file_dir=str(mask_dir),
375+
candidate_path=pathlib.Path("img_a.tiff"),
376+
)
377+
378+
assert matched is not None
379+
assert matched.name == "img_a_mask.tiff"
380+
381+
382+
def test_find_matching_segmentation_path_prefers_candidate_parent_tree(
383+
tmp_path: pathlib.Path,
384+
) -> None:
385+
mask_dir = tmp_path / "masks"
386+
(mask_dir / "plate_a").mkdir(parents=True)
387+
(mask_dir / "plate_b").mkdir(parents=True)
388+
(mask_dir / "plate_a" / "nuclei1_mask.tiff").write_bytes(b"")
389+
(mask_dir / "plate_b" / "nuclei1_mask.tiff").write_bytes(b"")
390+
391+
matched = CytoDataFrame._find_matching_segmentation_path(
392+
data_value="plate_a/nuclei1.tiff",
393+
pattern_map={r".*_mask\.tiff$": r".*"},
394+
file_dir=str(mask_dir),
395+
candidate_path=pathlib.Path("/tmp/plate_a/nuclei1.tiff"),
396+
)
397+
398+
assert matched is not None
399+
assert matched.parent.name == "plate_a"
400+
401+
363402
def test_cytodataframe_input(
364403
tmp_path: pathlib.Path,
365404
basic_outlier_dataframe: pd.DataFrame,
@@ -1164,6 +1203,8 @@ def fake_build_pyvista_viewer(**kwargs: object): # noqa: ANN202
11641203
assert captured["backend"] == "trame"
11651204
assert captured["widget_height"] == "140px"
11661205
assert isinstance(captured.get("label_volume"), np.ndarray)
1206+
assert isinstance(grid[1, 1], widgets.Box)
1207+
assert len(grid[1, 1].children) == 1
11671208

11681209

11691210
def test_get_displayed_rows_when_under_limit(monkeypatch: pytest.MonkeyPatch):
@@ -1397,6 +1438,7 @@ def test_add_label_overlay_toggle_control_toggles_overlay_actor_visibility() ->
13971438
toggles: list[int] = []
13981439
renders: list[bool] = []
13991440
checkbox_kwargs: dict[str, object] = {}
1441+
label_kwargs: dict[str, object] = {}
14001442

14011443
class FakeActor:
14021444
def SetVisibility(self, value: int) -> None:
@@ -1420,6 +1462,9 @@ def add_checkbox_button_widget(
14201462
checkbox_kwargs["size"] = size
14211463
checkbox_kwargs["position"] = position
14221464

1465+
def add_text(self, *_args: object, **kwargs: object) -> None:
1466+
label_kwargs.update(kwargs)
1467+
14231468
actor = FakeActor()
14241469
plotter = FakePlotter()
14251470
cdf._add_label_overlay_toggle_control(
@@ -1431,6 +1476,12 @@ def add_checkbox_button_widget(
14311476
assert checkbox_kwargs["value"] is True
14321477
assert checkbox_kwargs["size"] == 24
14331478
assert checkbox_kwargs["position"] == (266, 10)
1479+
label_position = label_kwargs["position"]
1480+
assert isinstance(label_position, tuple)
1481+
assert label_position == pytest.approx((0.01, 20 / 300))
1482+
assert label_kwargs["viewport"] is True
1483+
assert label_kwargs["color"] == "white"
1484+
assert label_kwargs["font_size"] == 9
14341485
callback = checkbox_kwargs["callback"]
14351486
assert callable(callback)
14361487

0 commit comments

Comments
 (0)