Tutorial about images

In the context of SMLM you also have to deal with pixelated images. For instance, raw data is recorded as movies from which localization data is generated. Or widefield microscopy images are recorded together with SMLM data.

To deal with image data, various image processing libraries exist. Locan makes use of a simple image class that serves as container and adapter class for any image class from a third-party library.

import tempfile
from dataclasses import dataclass
from pathlib import Path

%matplotlib inline

import numpy as np
import matplotlib.pyplot as plt
import httpx
import skimage as ski

import locan as lc
from locan.data.images import Image
Jupyter environment detected. Enabling Open3D WebVisualizer.
[Open3D INFO] WebRTC GUI backend enabled.
[Open3D INFO] WebRTCWindowSystem: HTTP handshake server disabled.
lc.show_versions(system=False, dependencies=False, verbose=False)
Locan:
   version: 0.21.0

Python:
   version: 3.11.12
directory = Path(tempfile.mkdtemp())

Some image

Let’s get some image data:

url = "https://raw.githubusercontent.com/super-resolution/Locan/main/docs/_static/logo_plus.png"
response = httpx.get(url)
print("Response is ok: ", response.is_success)
Response is ok:  True
file_path = directory / Path(url).name

with open(file_path, 'wb') as file:
    for chunk in response.iter_bytes(chunk_size=128):
        file.write(chunk)
        
file_path
PosixPath('/tmp/tmpfv92ytt4/logo_plus.png')
logo = ski.io.imread(file_path)
type(logo), logo.shape
(numpy.ndarray, (340, 1154, 3))
plt.imshow(logo);
../../_images/d150fb9eb1108f01c91a5607dd5e34e974b6b63d426b01382739ba6f581f6004.png

The Image class

In this example, the image is a plain numpy array. However, the image can be a more complex class with a specific interface, depending on the image library used.

Locan provides an adapter class to wrap any image class. The adapter class locan.Image provides a minimal standard interface.

Image?

Objects should be instantiated through an appropriate constructor method.

[method for method in dir(Image) if method.startswith("from")]
['from_array', 'from_bins', 'from_napari', 'from_numpy', 'from_pillow']

Image attributes

Upon instantiation locan.Image wraps the original image class and provides an array as image.data.

Any attribute request is forwarded first to image.data or - if not available - to the original images class referenced through image._image.

image = Image.from_numpy(array=logo, is_rgb=True)
image.is_rgb
True
image.shape
(340, 1154, 3)
image.size
1177080
image.dtype
dtype('uint8')
image.ndim
3

The Image data attribute

Typically, image.data should present the image as an array object that can be used with the array API standard.

If this is not the case a numpy.NDArray is provided.

image = Image.from_array(array=[[1, 2, 3], [1, 2, 3]])
type(image.data), image.dtype
ImportError: Install array_api_compat to check if is_array_api_obj(x)
The data object is not compliant with the array API standard.
(numpy.ndarray, dtype('int64'))
image = Image.from_array(array=logo, is_rgb=True)
type(image.data), image.dtype
(numpy.ndarray, dtype('uint8'))
plt.imshow(image.data);
../../_images/d150fb9eb1108f01c91a5607dd5e34e974b6b63d426b01382739ba6f581f6004.png

You might want to use methods according to the array API standard also with other libraries than np.

np.sum(image.data)
np.uint64(238545861)

Alternatively, use a different method implementation from array_api_compat:

try:
    from array_api_compat import array_namespace
    xp = array_namespace(image.data)
    logo_as_float = xp.astype(image.data, np.float32)
    print(type(logo_as_float), logo_as_float.dtype)
except ImportError as e:
    print("Exception:", e)
Exception: No module named 'array_api_compat'

The Image bins attribute

To keep pixel coordinates with the image use the bins attribute:

image = Image.from_array(array=logo[:, :, 0], is_rgb=False)
image.shape
(340, 1154)
image.bins = lc.Bins(n_bins=image.shape, bin_range=[[10, 20],[100, 200]])
[edges[0:3] for edges in image.bins.bin_edges]
[array([10.        , 10.02941176, 10.05882353]),
 array([100.        , 100.08665511, 100.17331023])]
image.bins
<locan.process.aggregate.Bins at 0x7fa262286a90>

The Image meta attribute

Metadata that relates to some localization data should be kept under the meta attribute:

image = Image.from_array(array=logo[:, :, 0], is_rgb=False, meta={"comment": "Free text can go here..."})
image.meta
identifier: "492819c8-640c-4b1c-a756-de1dc7cc86f8"
comment: "Free text can go here..."
creation_time {
  seconds: 1754766916
  nanos: 260926000
}

Additional metadata should be added to the meta.map attribute or in a separate attribute:

exif = {"Model": "Some Camera", "Information": "details"}
image.exif = exif
image.exif
{'Model': 'Some Camera', 'Information': 'details'}
image.meta.map.update(exif)
image.meta
identifier: "492819c8-640c-4b1c-a756-de1dc7cc86f8"
comment: "Free text can go here..."
map {
  key: "Model"
  value: "Some Camera"
}
map {
  key: "Information"
  value: "details"
}
creation_time {
  seconds: 1754766916
  nanos: 260926000
}

Modify the Image class

To create an Image object from any other library object modify the initialization of self.data and other attributes accordingly.

Here is a contrived example:

@dataclass
class ThirdPartyImage:
    img = [[1, 2, 3], [1, 2, 3],]
    is_rgb = False
    extra_metadata = {"Information": "details"}

third_party_image = ThirdPartyImage()
third_party_image.img, third_party_image.is_rgb, third_party_image.extra_metadata
([[1, 2, 3], [1, 2, 3]], False, {'Information': 'details'})
class MyImage(Image):

    @classmethod
    def from_third_party(cls, image, meta=None):
        new_image = cls(
            image=image,
            data=np.asarray(image.img),
            is_rgb=image.is_rgb,
            meta=meta
        )
        new_image.meta.map.update(image.extra_metadata)
        return new_image
image = MyImage.from_third_party(image=third_party_image)
image.meta
identifier: "8d96ada1-6188-4fa3-ae53-9389aabb4f5e"
map {
  key: "Information"
  value: "details"
}
creation_time {
  seconds: 1754766916
  nanos: 282466000
}
image.data
array([[1, 2, 3],
       [1, 2, 3]])