|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import base64 |
| 4 | +import json |
3 | 5 | from pathlib import Path |
4 | 6 | from typing import Final |
5 | 7 |
|
| 8 | +import numpy as np |
| 9 | +import PIL.Image |
6 | 10 | import pytest |
7 | 11 | from PyQt5.QtCore import QPointF |
8 | 12 | from PyQt5.QtCore import Qt |
9 | 13 | from pytestqt.qtbot import QtBot |
10 | 14 |
|
11 | 15 | from labelme import _shape |
| 16 | +from labelme import utils |
12 | 17 | from labelme._shape import Shape |
13 | 18 | from labelme.app import MainWindow |
14 | 19 | from labelme.widgets.canvas import Canvas |
|
17 | 22 |
|
18 | 23 | from ..conftest import assert_labelfile_sanity |
19 | 24 | from ..conftest import close_or_pause |
| 25 | +from .conftest import MainWinFactory |
20 | 26 | from .conftest import click_canvas_fraction |
21 | 27 | from .conftest import drag_canvas |
| 28 | +from .conftest import hover_widget_pos |
22 | 29 | from .conftest import image_to_widget_pos |
23 | 30 | from .conftest import schedule_on_dialog |
24 | 31 | from .conftest import select_shape |
| 32 | +from .conftest import show_window_and_wait_for_imagedata |
25 | 33 | from .conftest import submit_label_dialog |
26 | 34 |
|
27 | 35 | _TEST_FILE_NAME: Final[str] = "annotated/2011_000003.json" |
@@ -602,3 +610,134 @@ def test_remove_point_blocked_at_minimum( |
602 | 610 |
|
603 | 611 | _save_and_check(win=raw_win, tmp_path=tmp_path) |
604 | 612 | close_or_pause(qtbot=qtbot, widget=raw_win, pause=pause) |
| 613 | + |
| 614 | + |
| 615 | +def _click_to_select(qtbot: QtBot, canvas: Canvas, image_pos: QPointF) -> None: |
| 616 | + pos = image_to_widget_pos(canvas=canvas, image_pos=image_pos) |
| 617 | + hover_widget_pos(qtbot=qtbot, canvas=canvas, pos=pos) |
| 618 | + qtbot.mouseClick(canvas, Qt.LeftButton, pos=pos) |
| 619 | + qtbot.wait(50) |
| 620 | + |
| 621 | + |
| 622 | +@pytest.mark.gui |
| 623 | +def test_select_point_shape_by_click( |
| 624 | + qtbot: QtBot, |
| 625 | + raw_win: MainWindow, |
| 626 | + pause: bool, |
| 627 | +) -> None: |
| 628 | + canvas = raw_win._canvas_widgets.canvas |
| 629 | + raw_win._switch_canvas_mode(edit=False, create_mode="point") |
| 630 | + qtbot.wait(50) |
| 631 | + |
| 632 | + submit_label_dialog(qtbot=qtbot, label_dialog=raw_win._label_dialog, label="pt") |
| 633 | + click_canvas_fraction(qtbot=qtbot, canvas=canvas, xy=(0.5, 0.5)) |
| 634 | + |
| 635 | + shape = _wait_for_shape(qtbot=qtbot, canvas=canvas, label="pt") |
| 636 | + assert shape.shape_type == "point" |
| 637 | + |
| 638 | + raw_win._switch_canvas_mode(edit=True) |
| 639 | + qtbot.wait(50) |
| 640 | + |
| 641 | + _click_to_select(qtbot=qtbot, canvas=canvas, image_pos=QPointF(shape.points[0])) |
| 642 | + |
| 643 | + assert shape in canvas.selected_shapes |
| 644 | + |
| 645 | + close_or_pause(qtbot=qtbot, widget=raw_win, pause=pause) |
| 646 | + |
| 647 | + |
| 648 | +@pytest.mark.gui |
| 649 | +def test_right_click_on_shape_opens_context_menu( |
| 650 | + qtbot: QtBot, |
| 651 | + annotated_win: MainWindow, |
| 652 | + monkeypatch: pytest.MonkeyPatch, |
| 653 | + pause: bool, |
| 654 | +) -> None: |
| 655 | + canvas = annotated_win._canvas_widgets.canvas |
| 656 | + # No prior right-drag has populated `_selected_shapes_copy`, so the bare |
| 657 | + # right-click should open menus[0] (the no-clipboard variant). Stubbing |
| 658 | + # both exec_ methods catches a regression that would route to menus[1]. |
| 659 | + menu_opened: list[int] = [] |
| 660 | + monkeypatch.setattr( |
| 661 | + canvas.menus[0], |
| 662 | + "exec_", |
| 663 | + lambda *args, **kwargs: menu_opened.append(0) or None, |
| 664 | + ) |
| 665 | + monkeypatch.setattr( |
| 666 | + canvas.menus[1], |
| 667 | + "exec_", |
| 668 | + lambda *args, **kwargs: menu_opened.append(1) or None, |
| 669 | + ) |
| 670 | + |
| 671 | + bounds_center = _shape.bounds(shape=canvas.shapes[_SHAPE_INDEX]).center() |
| 672 | + pos = image_to_widget_pos(canvas=canvas, image_pos=bounds_center) |
| 673 | + qtbot.mouseMove(canvas, pos=pos) |
| 674 | + qtbot.wait(50) |
| 675 | + qtbot.mouseClick(canvas, Qt.RightButton, pos=pos) |
| 676 | + qtbot.wait(50) |
| 677 | + |
| 678 | + assert menu_opened == [0] |
| 679 | + |
| 680 | + close_or_pause(qtbot=qtbot, widget=annotated_win, pause=pause) |
| 681 | + |
| 682 | + |
| 683 | +@pytest.mark.gui |
| 684 | +def test_select_mask_shape_by_click( |
| 685 | + main_win: MainWinFactory, |
| 686 | + qtbot: QtBot, |
| 687 | + data_path: Path, |
| 688 | + tmp_path: Path, |
| 689 | + pause: bool, |
| 690 | +) -> None: |
| 691 | + # Mask cells are indexed pixel-for-pixel from points[0]; clicking inside |
| 692 | + # the True region must land on a True cell to exercise the mask branch |
| 693 | + # of `_shape.contains_point`. |
| 694 | + mask_arr = np.zeros((40, 40), dtype=np.uint8) |
| 695 | + mask_arr[10:30, 10:30] = 1 |
| 696 | + mask_b64 = utils.img_arr_to_b64(mask_arr) |
| 697 | + |
| 698 | + raw_image_path = data_path / "raw/2011_000003.jpg" |
| 699 | + img_b64 = base64.b64encode(raw_image_path.read_bytes()).decode("utf-8") |
| 700 | + image_width, image_height = PIL.Image.open(raw_image_path).size |
| 701 | + |
| 702 | + fixture_json = tmp_path / "mask_fixture.json" |
| 703 | + fixture_json.write_text( |
| 704 | + json.dumps( |
| 705 | + { |
| 706 | + "version": "6.0.0", |
| 707 | + "flags": {}, |
| 708 | + "shapes": [ |
| 709 | + { |
| 710 | + "label": "mask_shape", |
| 711 | + "points": [[100.0, 100.0], [139.0, 139.0]], |
| 712 | + "group_id": None, |
| 713 | + "description": "", |
| 714 | + "shape_type": "mask", |
| 715 | + "flags": {}, |
| 716 | + "mask": mask_b64, |
| 717 | + } |
| 718 | + ], |
| 719 | + "imagePath": raw_image_path.name, |
| 720 | + "imageData": img_b64, |
| 721 | + "imageHeight": image_height, |
| 722 | + "imageWidth": image_width, |
| 723 | + } |
| 724 | + ) |
| 725 | + ) |
| 726 | + |
| 727 | + win = main_win( |
| 728 | + file_or_dir=str(fixture_json), |
| 729 | + config_overrides={"with_image_data": True}, |
| 730 | + ) |
| 731 | + show_window_and_wait_for_imagedata(qtbot=qtbot, win=win) |
| 732 | + canvas = win._canvas_widgets.canvas |
| 733 | + |
| 734 | + qtbot.waitUntil(lambda: any(s.label == "mask_shape" for s in canvas.shapes)) |
| 735 | + shape = next(s for s in canvas.shapes if s.label == "mask_shape") |
| 736 | + |
| 737 | + # True region in image coords: rows/cols 110..129 (mask[10:30,10:30] + |
| 738 | + # origin (100,100)). Click well inside that block. |
| 739 | + _click_to_select(qtbot=qtbot, canvas=canvas, image_pos=QPointF(120.0, 120.0)) |
| 740 | + |
| 741 | + assert shape in canvas.selected_shapes |
| 742 | + |
| 743 | + close_or_pause(qtbot=qtbot, widget=win, pause=pause) |
0 commit comments