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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
"""example showing how to read annotations from an existing project"""
from pathlib import Path
from paquo.projects import QuPathProject

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

# read the project and raise Exception if it's not there
with QuPathProject(EXAMPLE_PROJECT, mode='r') as qp:
    print("opened", qp.name)
    # iterate over the images
    for image in qp.images:
        # annotations are accessible via the hierarchy
        annotations = image.hierarchy.annotations

        print("Image", image.image_name, "has", len(annotations), "annotations.")
        for annotation in annotations:
            # annotations are paquo.pathobjects.QuPathPathAnnotationObject instances
            # their ROIs are accessible as shapely geometries via the .roi property
            print("> class:", annotation.path_class.name, "roi:", annotation.roi)

    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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"""example showing how to create a project with annotations"""
from pathlib import Path
from paquo.projects import QuPathProject
from paquo.images import QuPathImageType
from shapely.geometry import Point, Polygon, LineString

EXAMPLE_PROJECT = Path(__file__).parent.absolute() / "projects" / "example_02_project"
EXAMPLE_IMAGE = Path(__file__).parent.absolute() / "images" / "image_1.svs"

ANNOTATIONS = {
    'Annotation 1': Point(500, 500),
    'Annotation 2': Polygon.from_bounds(510, 400, 610, 600),
    'Some Other Name': LineString([[400, 400], [450, 450], [400, 425]])
}

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

    # add a new image:
    entry = qp.add_image(EXAMPLE_IMAGE, image_type=QuPathImageType.BRIGHTFIELD_H_E)

    for name, roi in ANNOTATIONS.items():
        # add the annotations without a class set
        annotation = entry.hierarchy.add_annotation(roi=roi)
        annotation.name = name

    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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"""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.")

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
"""example showing how to create a project with image metadata"""
from pathlib import Path
from paquo.projects import QuPathProject
from paquo.images import QuPathImageType

EXAMPLE_PROJECT = Path(__file__).parent.absolute() / "projects" / "example_04_project"
EXAMPLE_IMAGE_DIR = Path(__file__).parent.absolute() / "images"

METADATA = {
    "image_0.svs": {
        "annotator": "Alice",
        "status": "finished",
        "diagnosis": "healthy",
    },
    "image_1.svs": {
        "annotator": "Bob",
        "status": "started",
        "diagnosis": "sick",
    },
    "image_2.svs": {
        "annotator": "Chuck",
        "status": "waiting",
        "diagnosis": "unknown",
    },
}

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

    for image_fn, metadata in METADATA.items():
        entry = qp.add_image(
            EXAMPLE_IMAGE_DIR / image_fn,
            image_type=QuPathImageType.BRIGHTFIELD_H_E
        )
        # entry.metadata is a dict-like proxy:
        # > entry.metadata[key] = value
        # > entry.metadata.update(new_dict)
        # > etc...
        entry.metadata = metadata

    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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
"""example showing how to draw a tile detection overlay over an image"""
import itertools
import math
import random
from pathlib import Path
from typing import Tuple, Iterator

from shapely.geometry import Polygon

from paquo.images import QuPathImageType
from paquo.projects import QuPathProject

EXAMPLE_PROJECT = Path(__file__).parent.absolute() / "projects" / "example_05_project"
EXAMPLE_IMAGE = Path(__file__).parent.absolute() / "images" / "image_1.svs"


def measurement(x, y, w, h, a=8) -> float:
    """some measurement that you want to display"""
    v = math.exp(-a * ((2 * x / w - 1) ** 2 + (2 * y / h - 1) ** 2))
    v += random.uniform(-0.1, 0.1)
    return min(max(0., v), 1.)


def iterate_grid(width, height, grid_size) -> Iterator[Tuple[int, int]]:
    """return corner x,y coordinates for a grid"""
    yield from itertools.product(
        range(0, width, grid_size),
        range(0, height, grid_size)
    )


with QuPathProject(EXAMPLE_PROJECT, mode='x') as qp:
    print("created", qp.name)
    # add an image
    entry = qp.add_image(
        EXAMPLE_IMAGE,
        image_type=QuPathImageType.BRIGHTFIELD_H_E
    )

    tile_size = 50
    img_width = entry.width
    img_height = entry.height

    # iterate over the image in a grid pattern
    for x0, y0 in iterate_grid(img_width, img_height, grid_size=tile_size):
        tile = Polygon.from_bounds(x0, y0, x0 + tile_size, y0 + tile_size)
        # add tiles (tiles are specialized detection objects drawn without border)
        detection = entry.hierarchy.add_tile(
            roi=tile,
            measurements={
                'measurement': measurement(x0, y0, img_width, img_height)
            }
        )

    print("added", len(entry.hierarchy.detections), "tiles")
    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

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 👍