Quickstart

Before you start diving deep into paquo we strongly recommend to read the excellent QuPath Documentation. Since paquo is just providing a pythonic interface to QuPath functionality very many concepts map directly between the python and java world.

Danger

Paquo is undergoing heavy development. Expect things to change before we reach version 1.0.0

Working with projects

QuPath projects are accessed in paquo via paquo.projects.QuPathProject. It’s best to use them via their contextmanager interface, because then paquo.projects.QuPathProject.save() get’s automatically called after you made changes:

from paquo.projects import QuPathProject

qp = QuPathProject('./my_qupath_project/project.qpproj')
...  # science

By default paquo opens projects in readonly mode. Images on a project are provided via a sequence-like proxy interface (basically a tuple) and they can be added via the projects paquo.projects.QuPathProject.add_image() method.

>>> from paquo.projects import QuPathProject
>>> from paquo.images import QuPathImageType
>>> qp = QuPathProject('./my_qupath_project', mode='a')  # open project for appending
>>> qp.images  # <- access images via this
ImageEntries(['image_0.svs', 'image_1.svs', 'image_2.svs'])
>>> qp.add_image('/path/to/my/image.svs', image_type=QuPathImageType.OTHER)

When you open an existing project, it might be possible that some of the images in the project have been moved around. (Maybe you send the project to a friend, and they have the same images on a network share but the path is of course different.) To check this, projects provide a method paquo.projects.QuPathProject.is_readable(). It returns image ids (URIs in the current default) and a boolean indicating if the file can be reached:

>>> from paquo.projects import QuPathProject
>>> qp = QuPathProject('./my_qupath_project', mode='r')
>>> qp.images  # <- access images via this
ImageEntries(['image_0.svs', 'image_1.svs', 'image_2.svs'])
>>> qp.is_readable()
{'file:/share/image_0.svs': True,
 'file:/somewhere_else/image_1.svs': False,
 'file:/share/image_2.svs': True}

With default settings you can reassign missing images via paquo.projects.QuPathProject.update_image_paths() which takes a uri to uri mapping as an input:

with QuPathProject('./my_qupath_project', mode='r+') as qp:
    qp.update_image_paths(uri2uri={"file:/somewhere_else/image_1.svs": "file:/share/image_1.svs"})
    assert all(qp.is_readable().values())

Danger

There’s a few things to know about this. The way images are passed in paquo uses an abstraction layer named paquo.images.ImageProvider. It’s default implementation doesn’t do anything smart and uses image URIs to identify images. (It also only supports ‘file:/’ uris for now.) The idea behind this is that we can provide a caching layer for gathering images from many different sources. Follow Paquo Issue #13 for updates. We will add additional documentation once the implementation details are sorted out.

Projects also serve as a container for classes. They are exposed via another sequence-like proxy:

>>> from paquo.projects import QuPathProject
>>> qp = QuPathProject('./my_qupath_project', mode='r')
>>> qp.path_classes  # <- access classes via this attribute
(QuPathPathClass('myclass_0'), QuPathPathClass('myclass_1'), QuPathPathClass('myclass_2'))

Refer to the class example Predefine classes in a project for more details.

Working with annotations

paquo uses shapely to provide a pythonic interface to Qupath’s annotations. It’s recommended to make yourself familiar with shapely. Annotations are accessed on a hierarchy of a QuPathProjectEntry. You access them through a set-like readonly proxy object. If you want to add additional annotations use the paquo.hierarchy.QuPathPathObjectHierarchy.add_annotations() method.

>>> qp = QuPathProject('./my_new_project/project.qpproj', mode='r')  # open an existing project
>>> image = qp.images[0]  # get the first image
>>> image.hierarchy.annotations  # annotations are stored in a set like proxy object
QuPathPathAnnotationObjectSet(n=3)
>>> for annotation in image.hierarchy.annotations:
...     print(annotation.name, annotation.path_class, annotation.roi)
...
None QuPathPathClass('myclass_1') POLYGON ((50 50, 50 150, 150 150, 150 50, 50 50))
MyAnnotation QuPathPathClass('myclass_2') POLYGON ((50 650, 50 750, 150 750, 150 650, 50 650))
Another None POLYGON ((650 650, 650 750, 750 750, 750 650, 650 650))

Examples

You can find the code for many use case examples To get started setup a python environment with paquo. Git clone the repository and cd to the examples directory.

user@computer:~$ git clone git@github.com:bayer-science-for-a-better-life/paquo.git
user@computer:~$ cd paquo/examples
user@computer:examples$ python prepare_resources.py

This will create a folder images and a folder projects with example data. These are required for all of the examples to run. Refer to the examples to quickly learn how to solve a certain problem with paquo. In case your specific problem does not have an example yet, feel free to open a new issue in paquo’s issue tracker.

Tip

If you already have a solution for a problem and think it might have value for others (NOTE: it always does!) feel free to fork the paquo repository and create a Pull Request adding the new example.

Reading annotations

To read annotations from an existing project follow the code as shown here:

 1"""example showing how to read annotations from an existing project"""
 2from pathlib import Path
 3from paquo.projects import QuPathProject
 4
 5EXAMPLE_PROJECT = Path(__file__).parent.absolute() / "projects" / "example_01_project"
 6
 7# read the project and raise Exception if it's not there
 8with QuPathProject(EXAMPLE_PROJECT, mode='r') as qp:
 9    print("opened", qp.name)
10    # iterate over the images
11    for image in qp.images:
12        # annotations are accessible via the hierarchy
13        annotations = image.hierarchy.annotations
14
15        print("Image", image.image_name, "has", len(annotations), "annotations.")
16        for annotation in annotations:
17            # annotations are paquo.pathobjects.QuPathPathAnnotationObject instances
18            # their ROIs are accessible as shapely geometries via the .roi property
19            print("> class:", annotation.path_class.name, "roi:", annotation.roi)
20
21    print("done")

Add annotations to a project

To add annotations to a project you simply need to define them as shapely Geometries and then add them to your QuPath project as demonstrated here:

 1"""example showing how to create a project with annotations"""
 2from pathlib import Path
 3from paquo.projects import QuPathProject
 4from paquo.images import QuPathImageType
 5from shapely.geometry import Point, Polygon, LineString
 6
 7EXAMPLE_PROJECT = Path(__file__).parent.absolute() / "projects" / "example_02_project"
 8EXAMPLE_IMAGE = Path(__file__).parent.absolute() / "images" / "image_1.svs"
 9
10ANNOTATIONS = {
11    'Annotation 1': Point(500, 500),
12    'Annotation 2': Polygon.from_bounds(510, 400, 610, 600),
13    'Some Other Name': LineString([[400, 400], [450, 450], [400, 425]])
14}
15
16# create a the new project
17with QuPathProject(EXAMPLE_PROJECT, mode='x') as qp:
18    print("created", qp.name)
19
20    # add a new image:
21    entry = qp.add_image(EXAMPLE_IMAGE, image_type=QuPathImageType.BRIGHTFIELD_H_E)
22
23    for name, roi in ANNOTATIONS.items():
24        # add the annotations without a class set
25        annotation = entry.hierarchy.add_annotation(roi=roi)
26        annotation.name = name
27
28    print(f"done. Please look at {qp.name} in QuPath.")

Predefine classes in a project

Sometimes you might want to create projects with many predefined classes so that different users wont mistype the class names, or that you can enforce a unique coloring scheme accross your projects. Adding classes is as simple as:

 1"""example showing how to create a empty project with classes"""
 2from pathlib import Path
 3from paquo.projects import QuPathProject
 4from paquo.classes import QuPathPathClass
 5
 6EXAMPLE_PROJECT = Path(__file__).parent.absolute() / "projects" / "example_03_project"
 7
 8MY_CLASSES_AND_COLORS = [
 9    ("My Class 1", "#ff0000"),
10    ("Some Other Class", "#0000ff"),
11    ("Nothing*", "#00ff00"),
12]
13
14# create a the new project
15with QuPathProject(EXAMPLE_PROJECT, mode='x') as qp:
16    print("created", qp.name)
17
18    new_classes = []
19    for class_name, class_color in MY_CLASSES_AND_COLORS:
20        new_classes.append(
21            QuPathPathClass(name=class_name, color=class_color)
22        )
23
24    # setting QuPathProject.path_class always replaces all classes
25    qp.path_classes = new_classes
26    print("project classes:")
27    for path_class in qp.path_classes:
28        print(">", f"'{path_class.name}'", path_class.color.to_hex())
29
30    print(f"done. Please look at {qp.name} in QuPath.")

Add image metadata

Especially in bigger projects it can be useful if you know, you annotated a certain image, or what’s the current state of the annotations. For those things it’s best to use metadata like this:

 1"""example showing how to create a project with image metadata"""
 2from pathlib import Path
 3from paquo.projects import QuPathProject
 4from paquo.images import QuPathImageType
 5
 6EXAMPLE_PROJECT = Path(__file__).parent.absolute() / "projects" / "example_04_project"
 7EXAMPLE_IMAGE_DIR = Path(__file__).parent.absolute() / "images"
 8
 9METADATA = {
10    "image_0.svs": {
11        "annotator": "Alice",
12        "status": "finished",
13        "diagnosis": "healthy",
14    },
15    "image_1.svs": {
16        "annotator": "Bob",
17        "status": "started",
18        "diagnosis": "sick",
19    },
20    "image_2.svs": {
21        "annotator": "Chuck",
22        "status": "waiting",
23        "diagnosis": "unknown",
24    },
25}
26
27# create a the new project
28with QuPathProject(EXAMPLE_PROJECT, mode='x') as qp:
29    print("created", qp.name)
30
31    for image_fn, metadata in METADATA.items():
32        entry = qp.add_image(
33            EXAMPLE_IMAGE_DIR / image_fn,
34            image_type=QuPathImageType.BRIGHTFIELD_H_E
35        )
36        # entry.metadata is a dict-like proxy:
37        # > entry.metadata[key] = value
38        # > entry.metadata.update(new_dict)
39        # > etc...
40        entry.metadata = metadata
41
42    print(f"done. Please look at {qp.name} in QuPath. And look at he Project Metadata.")

Drawing tiled overlays

If you want to display additional information on top of your image, that can be easily hidden by a user, You can use TileDetectionObjects to build a grid containing measurement values:

 1"""example showing how to draw a tile detection overlay over an image"""
 2import itertools
 3import math
 4import random
 5from pathlib import Path
 6from typing import Tuple, Iterator
 7
 8from shapely.geometry import Polygon
 9
10from paquo.images import QuPathImageType
11from paquo.projects import QuPathProject
12
13EXAMPLE_PROJECT = Path(__file__).parent.absolute() / "projects" / "example_05_project"
14EXAMPLE_IMAGE = Path(__file__).parent.absolute() / "images" / "image_1.svs"
15
16
17def measurement(x, y, w, h, a=8) -> float:
18    """some measurement that you want to display"""
19    v = math.exp(-a * ((2 * x / w - 1) ** 2 + (2 * y / h - 1) ** 2))
20    v += random.uniform(-0.1, 0.1)
21    return min(max(0., v), 1.)
22
23
24def iterate_grid(width, height, grid_size) -> Iterator[Tuple[int, int]]:
25    """return corner x,y coordinates for a grid"""
26    yield from itertools.product(
27        range(0, width, grid_size),
28        range(0, height, grid_size)
29    )
30
31
32with QuPathProject(EXAMPLE_PROJECT, mode='x') as qp:
33    print("created", qp.name)
34    # add an image
35    entry = qp.add_image(
36        EXAMPLE_IMAGE,
37        image_type=QuPathImageType.BRIGHTFIELD_H_E
38    )
39
40    tile_size = 50
41    img_width = entry.width
42    img_height = entry.height
43
44    # iterate over the image in a grid pattern
45    for x0, y0 in iterate_grid(img_width, img_height, grid_size=tile_size):
46        tile = Polygon.from_bounds(x0, y0, x0 + tile_size, y0 + tile_size)
47        # add tiles (tiles are specialized detection objects drawn without border)
48        detection = entry.hierarchy.add_tile(
49            roi=tile,
50            measurements={
51                'measurement': measurement(x0, y0, img_width, img_height)
52            }
53        )
54
55    print("added", len(entry.hierarchy.detections), "tiles")
56    print(f"done. Please look at {qp.name} in QuPath and look at 'Measure > Show Measurement Maps'")

This will allow you to display extra data like this:

Tile Overlay Example 05

Putting Detection Measurements into a Pandas DataFrame

Extracting data from QuPath into Pandas is similarly straightforward:

import pandas as pd
qp = QuPathProject('./my_new_project/project.qpproj', mode='r')  # open an existing project
image = qp.images[0]  # get the first image
detections = image.hierarchy.detections  # detections are stored in a set like proxy object
df = pd.DataFrame(detection.measurements for detection in detections)  # put the measurements dictionary for each detection into a pandas DataFrame

More examples

We need your input! 🙇

Tip

In case you need another example for the specific thing you’d like to do, please feel free to open a new issue on paquo’s issue tracker. We’ll try our best to help you 👍