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
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
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
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))
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()
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
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')
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())
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())
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)
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)
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
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
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
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
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)
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.")
def test_unsupported_mode(tmp_path): with pytest.raises(ValueError): QuPathProject(tmp_path, mode="???")
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"
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+')
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)
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()
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()
def test_project_instance(): with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir: q = QuPathProject(tmpdir, mode='x') repr(q) q.save()
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()