Skip to content

Commit

Permalink
Merge branch 'main' of github.com:qupath/qubalab into bioio
Browse files Browse the repository at this point in the history
  • Loading branch information
alanocallaghan committed Oct 29, 2024
2 parents 2c8b1a6 + 9b3295c commit 50da702
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 67 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
name: test

on:
push:
branches:
main
pull_request:
workflow_dispatch:

Expand All @@ -16,9 +19,12 @@ jobs:
- name: Set up python
id: setup-python
uses: actions/setup-python@v5

- name: Install Openslide
run: sudo apt install openslide-tools

- name: Install
run: pip install .[test]
run: pip install .[test,openslide]

- name: Run tests
run: pytest
65 changes: 44 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,61 @@
# QuBaLab

This is a Python package for exploring quantitative bioimage analysis... *especially* (but not exclusively) in
combination with QuPath (https://qupath.github.io/).
This is a Python package for exploring quantitative bioimage analysis... *especially* (but not exclusively) in combination with QuPath (https://qupath.github.io/).

The name comes from **Quantitative Bioimage Analysis Laboratory**.
This is chosen to be reminiscent of QuPath (*Quantitative Pathology*), but recognizes that neither is really restricted
to pathology.
The name comes from **Quantitative Bioimage Analysis Laboratory**. This is chosen to be reminiscent of QuPath (*Quantitative Pathology*), but recognizes that neither is really restricted to pathology.

## Goals
## Why use QuBaLab?

QuBaLab isn't QuPath - they're just good friends.

* **QuPath** is a user-friendly Java application for bioimage analysis, which has some especially nice features for
handling whole slide and highly-multiplexed images. But lots of bioimage analysis researcher is done in Python,
and is hard to integrate with QuPath.
* **QuBaLab**'s main aim is to help with this, by providing tools to help exchange data between QuPath and Python
*without any direct dependency on QuPath and Java*. It therefore doesn't require QuPath to be installed, and
can be used entirely from Python.
* **QuPath** is a user-friendly Java application for bioimage analysis, which has some especially nice features for handling whole slide and highly-multiplexed images. But lots of bioimage analysis research is done in Python, and is hard to integrate with QuPath.
* **QuBaLab**'s main aim is to help with this, by providing tools to help exchange data between QuPath and Python *without any direct dependency on QuPath and Java*. It therefore doesn't require QuPath to be installed, and can be used entirely from Python.

QuBaLab doesn't share code with QuPath, but is uses many of the same conventions for accessing images and
representing objects in a GeoJSON-compatible way.
By using the same custom fields for things like measurements and classifications, exchanging data is much easier.
QuBaLab doesn't share code with QuPath, but is uses many of the same conventions for accessing images and representing objects in a GeoJSON compatible way. By using the same custom fields for things like measurements and classifications, exchanging data is much easier.

### How does QuBaLab compare to paquo?

paquo is an existing library linking Python and QuPath that provides a pythonic interface to QuPath.

We think paquo is great, and don't want to replace it!

Here are the 3 main differences as we see them:

1. Target audience
- paquo is written mostly for Python programmers who need to work with QuPath data
- QuBaLab is written mostly for QuPath users who want to dip into Python
2. Convenience vs. Efficiency
- paquo is based on JPype to provide full & efficient access to Java from Python
- QuBaLab is based on Py4J to exchange data between Java & Python - preferring convenience over efficiency
3. Pixel access
- paquo is for working with QuPath projects and objects - accessing pixels is beyond its scope (at least for now)
- QuBaLab enables requesting pixels as numpy or dask arrays, and provides functions to convert between thresholded images & QuPath objects

So if you're a Python programmer who needs an intuitive and efficient way to work with QuPath data, use paquo.

But if you're a QuPath user who wants to switch to Python for some tasks, including image processing, you might want to give QuBaLab a try.

## Getting started

You can find the documentation on https://qupath.github.io/qubalab/.
You can find the documentation on https://qupath.github.io/qubalab-docs/.

This project contains the QuBaLab package in the `qubalab` folder. Take a look at the *Installation* section to install it.

Some notebooks present in the `notebooks` folder show how to use the QuBaLab package. If you want to run them, you can take a look at the *Development* section.
If you just want to go through them, look at the [documentation](https://qupath.github.io/qubalab/notebooks.html).
Some notebooks in the `notebooks` folder demonstrate how to use QuBaLab. If you want to run them, you can take a look at the *Development* section. If you just want to browse the content in them, look at the [documentation](https://qupath.github.io/qubalab/notebooks.html).

## Installation

TODO when available on PyPI
QuBaLab will live on PyPI soon, but for the time being you should install it from GitHub. You can do this directly using:

Installing the package through PyPI will only give access to the Python library. If you want to run the notebooks or
contribute to this project, take a look at the *Development* section.
```bash
pip install --upgrade https://github.com/qupath/qubalab/tarball/main
```

or if you have git installed:

```bash
pip install git+https://github.com/qupath/qubalab.git
```

## Development

Expand All @@ -53,3 +72,7 @@ pip install -e ".[dev,test,openslide]" # install qubalab (-e me
jupyter lab . # to start the Jupyter notebooks
pytest # to run unit tests
```

## OpenSlide support

OpenSlide support relies on having OpenSlide binaries installed at a system level. Therefore, not all aspects of OpenSlide will work consistently across platforms, as the versions available from package managers or from OpenSlide directly may vary between operating systems. We hope that this will be resolved shortly with the release of OpenSlide 4.0.0 binaries in the [openslide-bin](https://pypi.org/project/openslide-bin/) package, and the release of [openslide-python](https://pypi.org/project/openslide-python/) 1.4.0 which will use them.
7 changes: 4 additions & 3 deletions notebooks/opening_images.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
"source": [
"### AICSImageIoServer\n",
"\n",
"We will use the **LuCa-7color_[13860,52919]_1x1component_data.tif** image (CC-BY 4.0 license) because it's a small fluorescence image."
"We will use the **Patient_test_1.ome.tiff** image (CC0 license) because it's a relatively small fluorescence image."
]
},
{
Expand All @@ -130,7 +130,7 @@
"outputs": [],
"source": [
"# Download or get image\n",
"luca_path = get_image(\"LuCa-7color_[13860,52919]_1x1component_data.tif\", \"https://downloads.openmicroscopy.org/images/Vectra-QPTIFF/perkinelmer/PKI_fields/LuCa-7color_%5b13860,52919%5d_1x1component_data.tif\")"
"fluoro_path = get_image(\"Patient_test_1.ome.tiff\", \"https://ftp.ebi.ac.uk/biostudies/fire/S-BIAD/463/S-BIAD463/Files/my_submission/Validation_raw/DCIS/Patient_test_1.ome.tiff\")"
]
},
{
Expand All @@ -145,7 +145,7 @@
"\n",
"# Create the ImageServer from the image path. This will read the image metadata but not the pixel values yet.\n",
"# This function has optional parameters you can find in the documentation\n",
"aicsimageio_server = AICSImageIoServer(luca_path)"
"aicsimageio_server = AICSImageIoServer(fluoro_path)"
]
},
{
Expand Down Expand Up @@ -463,6 +463,7 @@
},
"outputs": [],
"source": [
"%%script false --no-raise-error\n",
"import napari\n",
"import dask.array as da\n",
"\n",
Expand Down
2 changes: 2 additions & 0 deletions qubalab/images/openslide_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class OpenSlideServer(ImageServer):
"""
An image server that relies on OpenSlide (https://openslide.org/) to read RGB images.
This server may only be able to detect the full resolution of a pyramidal image.
OpenSlide provides some properties to define a rectangle bounding the non-empty region of the slide
(see https://openslide.org/api/python/#standard-properties). If such properties are found, only this
rectangle will be read (but note that this behaviour was not properly tested).
Expand Down
8 changes: 5 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ packages =
qubalab.qupath
python_requires = >= 3.10
install_requires =
tiffslide ~= 2.1.2
bioio ~= 1.0.0
dask-image ~= 2024.5.3
tiffslide ~= 2.4.0
bioio ~= 1.1.0
bioio-ome-zarr ~= 1.0.1
bioio-ome-tiff ~= 1.0.1
dask-image >= 0.6.0
py4j ~= 0.10.9.7
geojson ~= 3.1.0
shapely ~= 2.0.4
Expand Down
41 changes: 4 additions & 37 deletions tests/images/test_openslide_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,12 @@ def test_image_shapes():

shapes = openslide_server.metadata.shapes

assert shapes == multi_resolution_uint8_3channels.get_shapes()
# only the full resolution can be detected
assert shapes[0] == multi_resolution_uint8_3channels.get_shapes()[0]

openslide_server.close()


def test_image_pixel_calibration():
openslide_server = OpenSlideServer(multi_resolution_uint8_3channels.get_path())

pixel_calibration = openslide_server.metadata.pixel_calibration

assert pixel_calibration == PixelCalibration(
PixelLength.create_microns(multi_resolution_uint8_3channels.get_pixel_size_x_y_in_micrometers()),
PixelLength.create_microns(multi_resolution_uint8_3channels.get_pixel_size_x_y_in_micrometers())
)

openslide_server.close()


def test_is_rgb():
openslide_server = OpenSlideServer(multi_resolution_uint8_3channels.get_path())
Expand Down Expand Up @@ -93,7 +82,8 @@ def test_downsamples():

downsamples = openslide_server.metadata.downsamples

assert downsamples == multi_resolution_uint8_3channels.get_downsamples()
# only the full resolution can be detected
assert downsamples[0] == multi_resolution_uint8_3channels.get_downsamples()[0]

openslide_server.close()

Expand All @@ -119,26 +109,3 @@ def test_read_full_resolution_image():
np.testing.assert_array_equal(image, expected_pixels)

openslide_server.close()


def test_read_lower_resolution_image():
level = len(multi_resolution_uint8_3channels.get_shapes()) - 1
lowest_resolution = multi_resolution_uint8_3channels.get_shapes()[level]
downsample = multi_resolution_uint8_3channels.get_downsamples()[level]
openslide_server = OpenSlideServer(multi_resolution_uint8_3channels.get_path())
expected_pixels = np.array(
[[[multi_resolution_uint8_3channels.get_pixel_value(downsample, x, y, c)
for x in range(lowest_resolution.x)]
for y in range(lowest_resolution.y)]
for c in range(lowest_resolution.c)],
multi_resolution_uint8_3channels.get_dtype()
)

image = openslide_server.read_region(
downsample,
Region2D(width=openslide_server.metadata.width, height=openslide_server.metadata.height)
)

np.testing.assert_array_equal(image, expected_pixels)

openslide_server.close()
8 changes: 6 additions & 2 deletions tests/res/multi_resolution_uint8_3channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ def _write_image(pixels: np.array):
metadata=metadata,
subifds=number_of_subresolutions,
resolution=(number_of_pixels_per_cm, number_of_pixels_per_cm),
resolutionunit=3 # indicate that the resolution above is in cm^-1
resolutionunit=3, # indicate that the resolution above is in cm^-1,
photometric='rgb',
tile=(128, 128)
)

# Write sub resolutions
Expand All @@ -89,7 +91,9 @@ def _write_image(pixels: np.array):
metadata=metadata,
subfiletype=1, # indicate that the image is part of a multi-page image
resolution=(number_of_pixels_per_cm / downsample, number_of_pixels_per_cm / downsample),
resolutionunit=3
resolutionunit=3,
photometric='rgb',
tile=(128, 128)
)


Expand Down

0 comments on commit 50da702

Please sign in to comment.