Example #1
0
def test_project_creation_mode(mode, tmp_path2, new_project):
    # test creating in empty dir
    cm = pytest.raises(FileNotFoundError) if "r" in mode else nullcontext()
    with cm:
        QuPathProject(tmp_path2, mode=mode).save()

    # prepare an existing proj
    p = new_project.path
    new_project.save()
    del new_project
    assert not p.with_suffix('.qpproj.backup').is_file()
    assert not any(p.parent.parent.glob('*.backup'))

    # test creating in existing dir
    if "x" in mode:
        cm = pytest.raises(FileExistsError)
    elif "r" == mode:
        cm = pytest.raises(IOError)  # can't save!
    else:
        cm = nullcontext()
    with cm:
        QuPathProject(p, mode=mode).save()

    assert p.is_file()
    backups = list(p.parent.parent.glob('*.backup'))

    if 'w' in mode:
        assert len(backups) == 1
        assert backups[0].is_file()
    else:
        assert len(backups) == 0
Example #2
0
def project_with_removed_image_without_image_data(removable_svs_small):
    with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir:
        qp = QuPathProject(tmpdir, mode='x')
        _ = qp.add_image(removable_svs_small)
        qp.save()
        removable_svs_small.unlink()
        yield qp.path
Example #3
0
def project_with_removed_image(removable_svs_small):
    with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir:
        qp = QuPathProject(tmpdir, mode='x')
        _ = qp.add_image(removable_svs_small,
                         image_type=QuPathImageType.BRIGHTFIELD_H_E)
        qp.save()
        removable_svs_small.unlink()
        yield qp.path
Example #4
0
def list_project(path):
    """print information about the project"""
    from paquo.projects import QuPathProject

    with QuPathProject(path, mode='r') as qp:
        print("Project:", qp.name)
        print("Classes: #", len(qp.path_classes), sep='')
        for path_class in qp.path_classes:
            color = path_class.color
            color_str = f" [{color.to_hex()}]" if color else ""
            print(f"- {path_class.id}{color_str}")

        _md_keys = set()
        _md_value_len = defaultdict(int)
        print("Images: #", len(qp.images), sep='')
        for idx, image in enumerate(qp.images):
            print(f"- [{idx}] {image.image_name}")
            for k, v in image.metadata.items():
                _md_keys.add(k)
                _md_value_len[k] = max(_md_value_len[k], len(v), len(k))

        print("Project Metadata: #", len(_md_keys), sep='')
        if len(_md_keys):
            _md_keys = list(sorted(_md_keys))
            hdr = ["#    "]
            for _key in _md_keys:
                hdr.append(f"{_key:<{_md_value_len[_key]}}")
            print(" ".join(hdr))
            for idx, image in enumerate(qp.images):
                line = [f"- [{idx}]"]
                for _key in _md_keys:
                    val = image.metadata.get(_key, '')
                    line.append(f"{val:<{_md_value_len[_key]}}")
                print(" ".join(line))
Example #5
0
def test_imagedata_saving_for_removed_images_without_type(
        project_with_removed_image_without_image_data):
    with QuPathProject(project_with_removed_image_without_image_data,
                       mode='r+') as qp:
        entry = qp.images[0]
        # todo: check if we actually want this behavior.
        #   at least it's documented now
        assert not (entry.entry_path / "data.qpdata").is_file()
Example #6
0
def test_project_version(new_project):
    from paquo.java import GeneralTools
    new_project.save()
    qp_version = str(GeneralTools.getVersion())
    assert new_project.version == qp_version

    with QuPathProject(new_project.path, mode='r+') as qp:
        assert qp.version == qp_version
def project_and_changes(svs_small):
    with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir:
        qp = QuPathProject(tmpdir, mode='x')
        entry = qp.add_image(svs_small)
        entry.hierarchy.add_annotation(
            roi=shapely.geometry.Point(1, 2)
        )
        qp.save()
        project_path = qp.path.parent
        del qp

        last_changes = {}
        for file in project_path.glob("**/*.*"):
            p = str(file.absolute())
            last_changes[p] = file.stat().st_mtime

        yield project_path, last_changes
Example #8
0
def test_project_creation_input_error(tmp_path):

    p = tmp_path / Path('somewhere')
    p.mkdir()

    with pytest.raises(TypeError):
        # noinspection PyTypeChecker
        QuPathProject(p, image_provider=object())

    with pytest.raises(ValueError):
        QuPathProject(p / 'myproject.proj')  # needs .qpproj

    with pytest.raises(FileNotFoundError):
        QuPathProject(p / 'myproject.qpproj', mode='r+')

    with open(p / 'should-not-be-here', 'w') as f:
        f.write('project directories need to be empty')
    with pytest.raises(ValueError):
        QuPathProject(p, mode='x')
Example #9
0
def test_project_image_uri_update(tmp_path, svs_small):

    project_path = tmp_path / "paquo-project"
    new_svs_small = tmp_path / "images" / f"image_be_gone{svs_small.suffix}"
    new_svs_small.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy(svs_small, new_svs_small)
    assert new_svs_small.is_file()

    # create a project
    with QuPathProject(project_path, mode='x') as qp:
        entry = qp.add_image(new_svs_small)
        assert entry.is_readable()
        assert all(qp.is_readable().values())

    # cleanup
    del entry
    del qp

    # remove image
    os.unlink(new_svs_small)

    # reload the project
    with QuPathProject(project_path, mode='r+') as qp:
        entry, = qp.images
        assert not entry.is_readable()
        assert not all(qp.is_readable().values())

        # test if there's no remapping
        qp.update_image_paths(uri2uri={})
        # test if we remap to same
        qp.update_image_paths(uri2uri={entry.uri: entry.uri})

        # create mapping for uris
        uri2uri = {
            entry.uri: ImageProvider.uri_from_path(svs_small),
        }
        # update the uris
        qp.update_image_paths(uri2uri=uri2uri)

        # test that entry can be read
        assert entry.is_readable()
        assert all(qp.is_readable().values())
Example #10
0
def test_project_image_uri_update_try_relative(tmp_path, svs_small):

    # prepare initial location
    location_0 = tmp_path / "location_0"
    location_0.mkdir()

    # prepare a project and images at location_0
    image_path = location_0 / "images" / "image0.svs"
    image_path.parent.mkdir(parents=True)
    shutil.copy(svs_small, image_path)
    assert image_path.is_file()

    # create a project
    with QuPathProject(location_0 / "project", mode='x') as qp:
        entry = qp.add_image(image_path)
        assert entry.is_readable()
        assert all(qp.is_readable().values())

    # cleanup
    del entry
    del qp

    # NOW move the location
    location_1 = tmp_path / "location_1"
    shutil.move(location_0, location_1)

    with QuPathProject(location_1 / "project", mode='r') as qp:
        entry, = qp.images
        assert not entry.is_readable()
        assert not all(qp.is_readable().values())
        assert 'location_1' in str(qp.path)
        assert 'location_0' in entry.uri  # still points to the old location

        # fixme: testing private api
        assert 'location_0' in str(qp.java_object.getPreviousURI().toString())

        qp.update_image_paths(try_relative=True)

        # test that entry can be read
        assert entry.is_readable()
        assert all(qp.is_readable().values())
Example #11
0
def export_annotations(path, image_idx, pretty=False):
    """print annotations as geojson"""
    import pprint

    from paquo.projects import QuPathProject

    with QuPathProject(path, mode='r') as qp:
        image = qp.images[image_idx]
        data = image.hierarchy.to_geojson()
        if pretty:
            pprint.pprint(data)
        else:
            print(data)
Example #12
0
def test_readonly_recovery_image_server(project_with_removed_image):
    from paquo.java import compatibility
    if not compatibility.supports_image_server_recovery():
        pytest.skip(f"unsupported in {compatibility.version}")

    with QuPathProject(project_with_removed_image, mode='r') as qp:
        image_entry = qp.images[0]

        assert image_entry.height
        assert image_entry.width
        assert image_entry.num_channels
        assert image_entry.num_timepoints
        assert image_entry.num_z_slices
        assert image_entry.downsample_levels

        assert repr(image_entry.hierarchy)
Example #13
0
def project_with_data(svs_small):
    from shapely.geometry import Point
    from paquo.classes import QuPathPathClass
    from paquo.images import QuPathImageType
    from paquo.projects import QuPathProject

    with tempfile.TemporaryDirectory(prefix='paquo-') as tmp_path:
        with QuPathProject(tmp_path, mode='x') as qp:
            entry = qp.add_image(svs_small,
                                 image_type=QuPathImageType.BRIGHTFIELD_OTHER)
            entry.hierarchy.add_annotation(roi=Point(500, 500))
            pcs = list(qp.path_classes)
            pcs.append(QuPathPathClass("myclass"))
            qp.path_classes = pcs
            entry.metadata["mykey"] = "myval"
            qp.save()
            yield qp.path
Example #14
0
def test_add_duplicate_to_hierarchy(project_with_annotations):
    """adding duplicate annotations works"""
    # create a project with an image and annotations
    from paquo.projects import QuPathProject

    p = project_with_annotations.path
    num_annotations = len(project_with_annotations.images[0].hierarchy)
    annotation = next(iter(project_with_annotations.images[0].hierarchy.annotations))
    del project_with_annotations

    # read project
    with QuPathProject(p, mode="a") as qp:
        entry1 = qp.images[0]
        entry1.hierarchy.add_annotation(
            roi=annotation.roi
        )
        assert len(entry1.hierarchy) == num_annotations + 1
Example #15
0
def test_add_to_existing_hierarchy(project_with_annotations):
    # create a project with an image and annotations
    from shapely.geometry import Point
    from paquo.projects import QuPathProject

    p = project_with_annotations.path
    num_annotations = len(project_with_annotations.images[0].hierarchy)
    del project_with_annotations

    # read project
    with QuPathProject(p, mode="a") as qp:
        entry1 = qp.images[0]

        assert len(entry1.hierarchy) == num_annotations
        entry1.hierarchy.add_annotation(
            roi=Point(2, 2)
        )
        assert len(entry1.hierarchy) == num_annotations + 1
Example #16
0
def create_project(project_path,
                   class_names_colors,
                   images,
                   annotations_json_func=None,
                   remove_default_classes=False,
                   force_write=False):
    """create a qupath project"""
    from paquo._logging import get_logger
    from paquo._utils import load_json_from_path
    from paquo.classes import QuPathPathClass
    from paquo.images import QuPathImageType
    from paquo.projects import QuPathProject

    _logger = get_logger(__name__)

    mode = 'x' if not force_write else 'w'

    if annotations_json_func is not None and not callable(
            annotations_json_func):
        raise ValueError("annotations_json_func should be callable")

    # noinspection PyTypeChecker
    with QuPathProject(project_path, mode=mode) as qp:
        if remove_default_classes:
            qp.path_classes = ()
        path_classes = list(qp.path_classes)
        for name, color in class_names_colors:
            path_classes.append(QuPathPathClass(name, color=color))
        qp.path_classes = path_classes

        for image in images:
            qp_image = qp.add_image(image,
                                    image_type=QuPathImageType.BRIGHTFIELD_H_E)

            if annotations_json_func:
                annotations_jsons = annotations_json_func(Path(image).name)
                for annotations_json in annotations_jsons:
                    _logger.info(f"loading '{annotations_json}'")
                    geojson = load_json_from_path(annotations_json)
                    qp_image.hierarchy.load_geojson(geojson["annotations"])

        name = qp.name
    return name
Example #17
0
def test_hierarchy_update_on_annotation_update(project_with_annotations):
    """updating annotation or detection objects triggers is_changed on hierarchy"""
    # create a project with an image and annotations
    from paquo.projects import QuPathProject
    from paquo.classes import QuPathPathClass

    p = project_with_annotations.path
    num_annotations = len(project_with_annotations.images[0].hierarchy)
    del project_with_annotations

    # test update
    with QuPathProject(p, mode="a") as qp:
        entry1 = qp.images[0]

        assert len(entry1.hierarchy) == num_annotations

        for annotation in entry1.hierarchy.annotations:
            annotation.update_path_class(QuPathPathClass("new"))
            assert entry1.is_changed()  # every update changes
            assert len(entry1.hierarchy) == num_annotations

        assert all(a.path_class.name == "new" for a in entry1.hierarchy.annotations)
Example #18
0
def prepare_example_resources():
    """build an example project"""
    from paquo.projects import QuPathProject
    from paquo.images import QuPathImageType
    from paquo.classes import QuPathPathClass
    from shapely.geometry import Polygon

    # download all images
    print(">>> downloading images...")
    images = list(download_image(copies=3))
    # remove the example_project
    example_project_dir = PROJECTS_DIR / "example_01_project"
    shutil.rmtree(example_project_dir, ignore_errors=True)

    print(">>> creating project...")
    with QuPathProject(example_project_dir, mode="x") as qp:
        for img_fn in images:
            qp.add_image(img_fn, image_type=QuPathImageType.BRIGHTFIELD_H_E)
        qp.path_classes = map(QuPathPathClass,
                              ["myclass_0", "myclass_1", "myclass_2"])
        for idx, image in enumerate(qp.images):
            image.metadata['image_number'] = str(1000 + idx)

        def _get_bounds(idx_x, idx_y):
            cx = 200 * idx_x + 100
            cy = 200 * idx_y + 100
            return cx - 50, cy - 50, cx + 50, cy + 50

        image_0 = qp.images[0]
        for x, y in itertools.product(range(4), repeat=2):
            roi = Polygon.from_bounds(*_get_bounds(x, y))
            image_0.hierarchy.add_annotation(
                roi, path_class=QuPathPathClass("myclass_1"))

        with open(DATA_DIR / "annotations.geojson", "w") as f:
            json.dump(image_0.hierarchy.to_geojson(), f)

    print(">>> done.")
def image_entry(svs_small):
    with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir:
        qp = QuPathProject(tmpdir, mode='x')
        entry = qp.add_image(svs_small)
        yield entry
def new_project(tmp_path):
    yield QuPathProject(tmp_path / "paquo-project", mode='x')
def readonly_project(project_and_changes):
    project_path, changes = project_and_changes
    qp = QuPathProject(project_path, mode="r")
    qp.__changes = changes
    yield qp
"""example showing how to create a empty project with classes"""
from pathlib import Path
from paquo.projects import QuPathProject
from paquo.classes import QuPathPathClass

EXAMPLE_PROJECT = Path(
    __file__).parent.absolute() / "projects" / "example_03_project"

MY_CLASSES_AND_COLORS = [
    ("My Class 1", "#ff0000"),
    ("Some Other Class", "#0000ff"),
    ("Nothing*", "#00ff00"),
]

# create a the new project
with QuPathProject(EXAMPLE_PROJECT, mode='x') as qp:
    print("created", qp.name)

    new_classes = []
    for class_name, class_color in MY_CLASSES_AND_COLORS:
        new_classes.append(QuPathPathClass(name=class_name, color=class_color))

    # setting QuPathProject.path_class always replaces all classes
    qp.path_classes = new_classes
    print("project classes:")
    for path_class in qp.path_classes:
        print(">", f"'{path_class.name}'", path_class.color.to_hex())

    print(f"done. Please look at {qp.name} in QuPath.")
Example #23
0
def test_unsupported_mode(tmp_path):
    with pytest.raises(ValueError):
        QuPathProject(tmp_path, mode="???")
Example #24
0
def test_project_name(tmp_path):
    p = Path(tmp_path) / "my_project_123"
    p.mkdir()
    qp = QuPathProject(p, mode='x')
    assert qp.name == "my_project_123"
Example #25
0
def test_project_open_with_filename(new_project):
    new_project.save()
    # this points to path/project.qpproj
    proj_fn = new_project.path
    QuPathProject(proj_fn, mode='r+')
Example #26
0
def test_readonly_recovery_hierarchy(
        project_with_removed_image_without_image_data):
    with QuPathProject(project_with_removed_image_without_image_data,
                       mode='r+') as qp:
        entry = qp.images[0]
        assert repr(entry.hierarchy)
Example #27
0
def test_project_add_image_writes_project(tmp_path, svs_small):
    qp = QuPathProject(tmp_path, mode='x')
    qp.add_image(svs_small)

    assert qp.path.is_file()
Example #28
0
def test_imagedata_saving_for_removed_images(project_with_removed_image):
    with QuPathProject(project_with_removed_image, mode='r+') as qp:
        entry = qp.images[0]
        assert (entry.entry_path / "data.qpdata").is_file()
Example #29
0
def test_project_instance():
    with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir:
        q = QuPathProject(tmpdir, mode='x')
        repr(q)
        q.save()
Example #30
0
def test_project_create_no_dir():
    with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir:
        project_path = Path(tmpdir) / "new_project"
        q = QuPathProject(project_path, mode='x')
        q.save()