Source code for sdt.roi.imagej

# SPDX-FileCopyrightText: 2020 Lukas Schrangl <lukas.schrangl@tuwien.ac.at>
#
# SPDX-License-Identifier: BSD-3-Clause

"""Load ROIs from ImageJ .roi and .zip files"""
import struct
import enum
import os
from io import BytesIO
import pathlib
import zipfile
import mmap

import numpy as np

from . import roi


__all__ = ["load_imagej", "load_imagej_zip"]


header1_spec = [
    ("magic", "4s"),
    ("version", "h"),
    ("type", "b"),
    ("reserved 1", "b"),
    ("top", "h"),
    ("left", "h"),
    ("bottom", "h"),
    ("right", "h"),
    ("n coordinates", "h"),
    ("x1", "f"),
    ("y1", "f"),
    ("x2", "f"),
    ("y2", "f"),
    ("stroke width", "h"),
    ("shape roi size", "i"),
    ("stroke color", "i"),
    ("fill color", "i"),
    ("subtype", "h"),
    ("options", "h"),
    ("float param", "f"),  # This can also be two bytes for certain
                           # (unsupported) ROI types
    ("rounded rect arc size", "h"),
    ("position", "i"),
    ("header2 offset", "i")]
header1_unpack = ">" + "".join([s[1] for s in header1_spec])

header2_spec = [
    ("c position", "i"),
    ("z position", "i"),
    ("t position", "i"),
    ("name offset", "i"),
    ("name length", "i"),
    ("overlay label color", "i"),
    ("overlay font size", "h"),
    ("avalaible byte 1", "b"),
    ("image opacity", "b"),
    ("image size", "i"),
    ("float stroke width", "f"),
    ("roi props offset", "i"),
    ("roi props length", "i")]
header2_unpack = ">" + "".join([s[1] for s in header2_spec])


class Type(enum.IntEnum):
    polygon = 0
    rect = 1
    oval = 2
    line = 3
    freeline = 4
    polyline = 5
    no_roi = 6
    freehand = 7
    traced = 8
    angle = 9
    point = 10


class SubType(enum.IntEnum):
    none = 0
    text = 1
    arrow = 2
    ellipse = 3
    image = 4
    rotated_rect = 5


class Options(enum.IntEnum):  # Python 3.5 has no IntFlag yet…
    spline_fit = 1
    double_headed = 2
    outline = 4
    overlay_labels = 8
    overlay_names = 16
    overlay_backgrounds = 32
    overlay_bold = 64
    sub_pixel_resolution = 128
    draw_offset = 256
    zero_transparent = 512


coord_offset = 64


def _load(data):
    """Load ROI from file (implementation)

    Parameters
    ----------
    data : bytes or mmap
        ROI file data

    Returns
    -------
    roi.ROI or roi.PathROI or roi.RectangleROI or roi.EllipseROI
        ROI object representing the ROI described in the file
    """
    h1 = struct.unpack(header1_unpack, data[:struct.calcsize(header1_unpack)])
    h1 = {k[0]: v for k, v in zip(header1_spec, h1)}

    if h1["magic"] != b"Iout":
        raise ValueError("Not an ImageJ ROI (wrong magic).")

    if h1["shape roi size"] > 0:
        raise NotImplementedError("Composite ROI not supported.")

    if h1["options"] & Options.spline_fit:
        raise NotImplementedError("ROI with spline fit not supported.")

    width = h1["right"] - h1["left"]
    height = h1["bottom"] - h1["top"]

    if h1["type"] == Type.rect:
        if h1["rounded rect arc size"] > 0:
            raise NotImplementedError(
                "Rounded rectangle corners not supported.")
        if (h1["options"] & Options.sub_pixel_resolution and
                h1["version"] >= 223):
            return roi.RectROI((h1["x1"], h1["x2"]), size=(h1["x2"], h1["y2"]))
        else:
            return roi.ROI((h1["left"], h1["top"]), size=(width, height))
    if h1["type"] == Type.oval:
        if (h1["options"] & Options.sub_pixel_resolution and
                h1["version"] >= 223):
            axes = (h1["x2"] / 2, h1["y2"] / 2)
            center = (h1["x1"] + axes[0], h1["x2"] + axes[1])
        else:
            axes = (width / 2, height / 2)
            center = (h1["left"] + axes[0], h1["top"] + axes[1])
        return roi.EllipseROI(center, axes)
    if h1["type"] == Type.freehand and h1["subtype"] == SubType.ellipse:
        x = h1["x1"], h1["x2"]
        y = h1["y1"], h1["y2"]
        ax0 = np.sqrt((x[1] - x[0])**2 + (y[1] - y[0])**2) / 2
        axes = (ax0, ax0 * h1["float param"])
        center = ((x[0] + x[1]) / 2, (y[0] + y[1]) / 2)
        angle = np.arctan2(y[1] - y[0], x[1] - x[0])
        return roi.EllipseROI(center, axes, angle)
    if h1["type"] in (Type.polygon, Type.freehand, Type.traced):
        if h1["subtype"] in (SubType.arrow, SubType.image, SubType.text):
            raise NotImplementedError(
                "{} ROIs are not supported".format(
                    str(SubType(h1["subtype"]))))

        n = h1["n coordinates"]
        int_coord_unpack = ">{}h".format(n)
        float_coord_unpack = ">{}f".format(n)
        if (h1["options"] & Options.sub_pixel_resolution and
                h1["version"] >= 223):
            start_read = coord_offset + 2 * struct.calcsize(int_coord_unpack)
            coords = np.frombuffer(data, float_coord_unpack, offset=start_read,
                                   count=2).T
        else:
            coords = np.frombuffer(data, int_coord_unpack, offset=coord_offset,
                                   count=2).T + (h1["left"], h1["top"])
        return roi.PathROI(coords)

    raise NotImplementedError(
        "{} ROIs are not supported".format(str(Type(h1["type"]))))


[docs]def load_imagej(file_or_data): """Load ROI from ImageJ ROI file Parameters ---------- file_or_data : str or pathlib.Path or bytes or file Source data. A `str` or `pathlib.Path` has to point to a file that can be opened for binary reading and is seekable. If `bytes`, this has to be the contents of a ROI file. A file has to be opened to allow mem-mapping ("r+b"). Returns ------- roi.ROI or roi.PathROI or roi.RectangleROI or roi.EllipseROI ROI object representing the ROI described in the file """ if isinstance(file_or_data, bytes): return _load(file_or_data) if isinstance(file_or_data, (str, pathlib.Path)): with open(str(file_or_data), "r+b") as f: with mmap.mmap(f.fileno(), 0) as m: return _load(m) # Let's hope it is an opened file with mmap.mmap(file_or_data.fileno(), 0) as m: return _load(m)
def _load_zip(z): """Load ROIs from ImageJ zip file (implementation) Parameters ---------- z : zipfile.ZipFile Zip file opened for reading Returns ------- dict of ROI objects Use the ROI names inside the zip as keys and the return values :py:func:`load_imagej` calls as values. """ ret = {} for n in z.namelist(): with z.open(n) as f: ret[os.path.splitext(n)[0]] = load_imagej(f.read()) return ret
[docs]def load_imagej_zip(file): """Load ROIs from ImageJ zip file Parameters ---------- file: str or pathlib.Path or zipfile.ZipFile Name/path of the zip file or ZipFile opened for reading Returns ------- dict of ROI objects Use the ROI names inside the zip as keys and the return values :py:func:`load_imagej` calls as values. """ if isinstance(file, (str, pathlib.Path)): with zipfile.ZipFile(str(file)) as z: return _load_zip(z) # Let's hope it is an opened ZipFile return _load_zip(file)