Module pvinspect.data.io

Read and write images

Expand source code
"""Read and write images"""

import json
import logging
import pickle
import urllib.parse
from datetime import date, datetime
from functools import reduce
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union

import numpy as np
from numpy.lib.npyio import save
from pvinspect.common import util
from pvinspect.common.types import ObjectAnnotations, PathOrStr
from pvinspect.data.image import Modality
from pyparsing import ParseException
from shapely.errors import WKTReadingError
from shapely.geometry import Point, Polygon
from shapely.wkt import dumps as shapely_dumps
from shapely.wkt import loads as shapely_loads
from skimage import color, img_as_float, img_as_uint, io
from tqdm.auto import tqdm

from .exceptions import UnsupportedModalityException
from .image import *


def _prepare_json_meta(meta):

    # numpy scalar -> builtin
    if type(meta).__module__ == np.__name__ and meta.ndim == 0:
        meta = np.asscalar(meta)

    if isinstance(meta, dict):
        return {k: _prepare_json_meta(v) for k, v in meta.items()}
    elif isinstance(meta, list):
        return [_prepare_json_meta(v) for v in meta]
    elif isinstance(meta, (Polygon, Point)):
        return shapely_dumps(meta)
    elif isinstance(meta, (date, datetime)):
        return meta.isoformat()
    elif isinstance(meta, Modality):
        return str(meta)
    elif isinstance(meta, (str, float, int, bool)):
        return meta
    else:
        return None


def _load_json_meta_hook(pairs):
    result = dict()
    for k, v in pairs.items():
        if isinstance(v, str):
            if v == "Modality.EL_IMAGE":
                result[k] = Modality.EL_IMAGE
                continue
            if v == "Modality.PL_IMAGE":
                result[k] = Modality.PL_IMAGE
                continue

            try:
                result[k] = datetime.fromisoformat(v)
                continue
            except ValueError:
                result[k] = v
            try:
                with util.suppress_stderr():
                    result[k] = shapely_loads(v)
                continue
            except WKTReadingError:
                result[k] = v
        else:
            result[k] = v
    return result


def __assurePath(p: PathOrStr) -> Path:
    if isinstance(p, str):
        return Path(p)
    else:
        return p


def _get_meta_path(img_path: Path) -> Path:
    return img_path.parent / "{}.json".format(img_path.name)


def _read_image(
    path: PathOrStr,
    is_module_image: bool,
    is_partial_module: bool,
    lazy: bool,
    modality: int = None,
    cols: Optional[int] = None,
    rows: Optional[int] = None,
    first_col: Optional[int] = None,
    first_row: Optional[int] = None,
    force_dtype: Union[DType, None] = None,
    meta=None,
):
    assert (
        is_module_image
        and not is_partial_module
        or is_partial_module
        and not is_module_image
        or not is_partial_module
        and not is_module_image
    )

    path = __assurePath(path)

    def _read(path):
        img = io.imread(path)

        if img.dtype == ">u2":
            # big endian -> little endian
            img = img.astype(np.uint16)

        if img.ndim == 3 and (img.dtype == np.float32 or img.dtype == np.float64):
            img = color.rgb2gray(img)
        elif img.ndim == 3:
            img = img_as_uint(color.rgb2gray(img))

        if force_dtype == DType.FLOAT:
            img = img.astype(DTYPE_FLOAT)
        elif force_dtype == DType.UNSIGNED_INT:
            img = img.astype(DTYPE_UNSIGNED_INT)
        elif force_dtype == DType.INT:
            img = img.astype(DTYPE_INT)

        if (img.dtype == np.float32 or img.dtype == np.float64) and (
            img.min() < 0.0 or img.max() > 1.0
        ):
            raise RuntimeWarning(
                'Image "{}" is of type float but not scaled between 0 and 1. This might cause trouble later. You might want to force conversion to another datatype using force_dtype=DType.UNSIGNED_INT (for example).'.format(
                    path
                )
            )
        return img

    # conditionally enable lazy loading
    img = _read(path) if not lazy else Image.LazyData(partial(_read, path))

    # try to read meta file
    meta_path = _get_meta_path(path)
    meta_series: pd.Series = None
    if meta is not None:
        meta_series = meta
    elif meta_path.is_file():
        with open(meta_path, "r") as f:
            meta_series = pd.Series(json.load(f, object_hook=_load_json_meta_hook))

    # merge modality, rows and cols
    if (
        meta_series is not None
        and "modality" in meta_series.keys()
        and modality is None
    ):
        modality = meta_series["modality"]
    if meta_series is not None and "rows" in meta_series.keys() and rows is None:
        rows = meta_series["rows"]
    if meta_series is not None and "cols" in meta_series.keys() and cols is None:
        cols = meta_series["cols"]

    if is_partial_module:
        return PartialModuleImage(
            img,
            modality,
            path,
            cols,
            rows,
            first_row=first_row,
            first_col=first_col,
            meta=meta,
        )
    elif is_module_image:
        return ModuleImage(img, modality, path, cols, rows, meta=meta_series)
    else:
        return Image(img, path, modality, meta=meta_series)


def _get_meta_cache_path(sequence_path: Path) -> Path:
    return sequence_path / ".cached_meta.pck"


def _read_images(
    path: PathOrStr,
    is_module_image: bool,
    is_partial_module: bool,
    lazy: bool,
    same_camera: bool = False,
    modality: int = None,
    cols: int = None,
    rows: int = None,
    N: int = 0,
    pattern: Union[str, Tuple[str]] = ("*.png", "*.tif", "*.tiff", "*.bmp"),
    allow_different_dtypes=False,
    force_dtype: DType = None,
) -> ImageSequence:
    path = __assurePath(path)

    if isinstance(pattern, str):
        pattern = [pattern]

    # find files and skip if more than N
    imgpaths: List[Path] = list(
        reduce(lambda x, y: x + y, [list(path.glob(pat)) for pat in pattern])
    )
    imgpaths.sort()
    if N > 0 and N < len(imgpaths):
        imgpaths = imgpaths[:N]

    # check if cached meta data is available
    cache_file = _get_meta_cache_path(path)
    cached_meta: pd.DataFrame = None
    if cache_file.is_file():
        with open(cache_file, "rb") as f:
            try:
                cached_meta = pd.read_pickle(f)
            except:
                cache_file.unlink()

    imgs = list()
    missing_meta: Dict[str, pd.Series] = dict()
    for fn in tqdm(imgpaths):
        if cached_meta is not None and fn.name in cached_meta.keys():
            img = _read_image(
                fn,
                is_module_image,
                is_partial_module,
                lazy,
                modality,
                cols,
                rows,
                force_dtype=force_dtype,
                meta=cached_meta.loc[fn.name],
            )
        else:
            img = _read_image(
                fn,
                is_module_image,
                is_partial_module,
                lazy,
                modality,
                cols,
                rows,
                force_dtype=force_dtype,
            )
            missing_meta[fn.name] = img.meta_to_pandas()
        imgs.append(img)

    # update/create meta cache file
    if len(missing_meta.keys()) > 0:
        missing_meta_df = pd.DataFrame(missing_meta).T
        new_meta = (
            pd.concat([cached_meta, missing_meta_df])
            if cached_meta is not None
            else missing_meta_df
        )
        new_meta.to_pickle(cache_file)

    # TODO: How to handle this with lazy loading?
    # if not same_camera:
    #    homogeneous_types = np.all(
    #        np.array([img.dtype == imgs[0].dtype for img in imgs])
    #    )
    #    shapes = [img.shape for img in imgs]
    #    homogeneous_shapes = np.all(np.array([s == shapes[0] for s in shapes]))
    #    target_shape = np.max(shapes, axis=0)

    #    if not homogeneous_shapes:
    #        logging.warning(
    #            "The original images are of different shape. They might not be suited for all applications (for example superresolution)."
    #        )

    #    if not homogeneous_types:
    #        # target type is determined by the first image
    #        logging.warning(
    #            "The original images are of different type. They are converted to the type of the first image ({})".format(
    #                imgs[0].dtype
    #            )
    #        )
    #        conv = (
    #            img_as_float
    #            if imgs[0].dtype == np.float32 or imgs[0].dtype == np.float64
    #            else img_as_uint
    #        )
    #        imgs = [conv(img) for img in imgs]

    if is_module_image or is_partial_module:
        res = ModuleImageSequence(
            imgs, same_camera=same_camera, allow_different_dtypes=allow_different_dtypes
        )
    else:
        res = ImageSequence(imgs, same_camera, allow_different_dtypes)

    return res


def read_image(
    path: PathOrStr, modality: int = None, force_dtype: DType = None, lazy: bool = True,
) -> Image:
    """Read a single image of a solar module and return it

    Args:
        path (PathOrStr): Path to the file to be read
        modality (int): The imaging modality
        force_dtype (DType): Force images to have this datatype
        lazy (bool): Enable lazy loading for this image

    Returns:
        image: The module image
    """

    return _read_image(
        path=path,
        is_module_image=False,
        is_partial_module=False,
        lazy=lazy,
        modality=modality,
        force_dtype=force_dtype,
    )


def read_images(
    path: PathOrStr,
    same_camera: bool = False,
    modality: int = None,
    N: int = 0,
    pattern: Union[str, Tuple[str]] = ("*.png", "*.tif", "*.tiff", "*.bmp"),
    allow_different_dtypes=False,
    force_dtype: DType = None,
    lazy: bool = True,
) -> ImageSequence:
    """Read a sequence of images and return it

    Args:
        path (PathOrStr): Path to the sequence
        same_camera (bool): Indicate, if all images are from the same camera and hence share the same intrinsic parameters
        modality (int): The imaging modality
        N (int): Only read first N images
        pattern (Union[str, Tuple[str]]): Files must match any of the given pattern
        allow_different_dtypes (bool): Allow images to have different datatypes?
        force_dtype (DType): Force images to have this datatype
        lazy (bool): Enable lazy loading for these images

    Returns:
        image: The image sequence
    """

    return _read_images(
        path,
        is_module_image=False,
        same_camera=same_camera,
        is_partial_module=False,
        lazy=lazy,
        modality=modality,
        pattern=pattern,
        allow_different_dtypes=allow_different_dtypes,
        N=N,
        force_dtype=force_dtype,
    )


def read_module_image(
    path: PathOrStr,
    modality: Modality,
    cols: int = None,
    rows: int = None,
    lazy: bool = True,
) -> ModuleImage:
    """Read a single image of a solar module and return it

    Args:
        path (PathOrStr): Path to the file to be read
        modality (int): The imaging modality
        cols (int): Number of columns of cells
        rows (int): Number of rows of cells
        lazy (bool): Enable lazy loading for this image

    Returns:
        image: The module image
    """

    return _read_image(
        path=path,
        is_module_image=True,
        is_partial_module=False,
        lazy=lazy,
        modality=modality,
        cols=cols,
        rows=rows,
    )


def read_module_images(
    path: PathOrStr,
    modality: int,
    same_camera: bool = False,
    cols: int = None,
    rows: int = None,
    N: int = 0,
    pattern: Union[str, Tuple[str]] = ("*.png", "*.tif", "*.tiff", "*.bmp"),
    allow_different_dtypes=False,
    force_dtype: DType = None,
    lazy: bool = True,
) -> ModuleImageSequence:
    """Read a sequence of module images and return it

    Args:
        path (PathOrStr): Path to the sequence
        modality (int): The imaging modality
        same_camera (bool): Indicate, if all images are from the same camera and hence share the same intrinsic parameters
        cols (int): Number of columns of cells
        rows (int): Number of rows of cells
        N (int): Only read first N images
        pattern (Union[str, Tuple[str]]): Files must match any of the given pattern
        allow_different_dtypes (bool): Allow images to have different datatypes?
        force_dtype (DType): Force images to have this datatype
        lazy (bool): Enable lazy loading for these images

    Returns:
        image: The module image sequence
    """

    return _read_images(
        path=path,
        modality=modality,
        same_camera=same_camera,
        is_partial_module=False,
        is_module_image=True,
        lazy=lazy,
        cols=cols,
        rows=rows,
        N=N,
        pattern=pattern,
        allow_different_dtypes=allow_different_dtypes,
        force_dtype=force_dtype,
    )


def read_partial_module_image(
    path: PathOrStr,
    modality: int,
    cols: Optional[int] = None,
    rows: Optional[int] = None,
    first_col: Optional[int] = None,
    first_row: Optional[int] = None,
    lazy: bool = True,
) -> ModuleImage:
    """Read a single partial view of a solar module and return it

    Args:
        path (PathOrStr): Path to the file to be read
        modality (int): The imaging modality
        cols (int): Number of completely visible columns of cells
        rows (int): Number of completely visible rows of cells
        lazy (bool): Enable lazy loading for this image

    Returns:
        image: The module image
    """

    return _read_image(
        path=path,
        is_module_image=False,
        is_partial_module=True,
        lazy=lazy,
        modality=modality,
        cols=cols,
        rows=rows,
        first_col=first_col,
        first_row=first_row,
    )


def read_partial_module_images(
    path: PathOrStr,
    modality: int,
    same_camera: bool = False,
    cols: int = None,
    rows: int = None,
    N: int = 0,
    pattern: Union[str, Tuple[str]] = ("*.png", "*.tif", "*.tiff", "*.bmp"),
    allow_different_dtypes=False,
    force_dtype: DType = None,
    lazy: bool = True,
) -> ModuleImageSequence:
    """Read a sequence of partial views of solar modules and return it

    Args:
        path (PathOrStr): Path to the sequence
        modality (int): The imaging modality
        same_camera (bool): Indicate, if all images are from the same camera and hence share the same intrinsic parameters
        cols (int): Number of completely visible columns of cells
        rows (int): Number of completely visible rows of cells
        N (int): Only read first N images
        pattern (Union[str, Tuple[str]]): Files must match any of the given pattern
        allow_different_dtypes (bool): Allow images to have different datatypes?
        force_dtype (DType): Force images to have this datatype
        lazy (bool): Enable lazy loading for these images

    Returns:
        image: The module image sequence
    """

    return _read_images(
        path=path,
        is_module_image=False,
        is_partial_module=True,
        same_camera=same_camera,
        lazy=lazy,
        modality=modality,
        cols=cols,
        rows=rows,
        N=N,
        pattern=pattern,
        allow_different_dtypes=allow_different_dtypes,
        force_dtype=force_dtype,
    )


def save_image(
    filename: PathOrStr,
    image: Image,
    with_visusalization: bool = False,
    save_meta: bool = False,
    **kwargs
):
    """Write an image to disk. Float64 is automatically converted to float32 in order to be compatible to ImageJ.

    Args:
        filename (PathOrStr): Filename of the resulting image
        image (Image): The image
        with_visualization (bool): Include the same visualizations as with image.show() or sequence.head()
        save_meta (bool): Save meta data to separate json file (cannot be used with visualization)
    """
    filename = __assurePath(filename)

    if with_visusalization and save_meta:
        logging.error("Cannot save meta data for image with visualization")
        return

    if with_visusalization:
        plt.clf()
        image.show(**kwargs)
        plt.savefig(filename, **kwargs)
    else:
        if image.dtype == DType.FLOAT:
            io.imsave(filename, image.data.astype(np.float32), check_contrast=False)
        else:
            io.imsave(filename, image.data, check_contrast=False)
        if save_meta and len(image.list_meta()) > 0:
            meta = {k: image.get_meta(k) for k in image.list_meta()}
            meta = _prepare_json_meta(meta)
            meta_path = _get_meta_path(filename)
            with open(meta_path, "w") as f:
                json.dump(meta, f)

    # delete cached entry
    cache_file = _get_meta_cache_path(filename.parent)
    if save_meta and cache_file.is_file():
        cached_meta: pd.DataFrame = pd.read_pickle(cache_file)
        cached_meta.drop(filename.name, inplace=True)
        cached_meta.to_pickle(cache_file)


def save_images(
    path: PathOrStr,
    sequence: ImageSequence,
    mkdir: bool = True,
    with_visualization: bool = False,
    hierarchical: List[str] = None,
    include_meta_keys: bool = True,
    save_meta: bool = False,
    filename_prefix: str = "",
    filename_suffix: str = "",
    filename_hook: Callable[[Image], Path] = lambda image: Path(image.path.name),
    **kwargs
):
    """Write a sequence of images to disk

    Args:
        path (PathOrStr): Target directory
        sequence (ImageSequence): The sequence of images
        mkdir (bool): Automatically create missing directories
        with_visualization (bool): Include the same visualizations as with image.show() or sequence.head()
        hierarchical (List[str]): Create a directory hierarchy using given meta keys
        include_meta_keys (bool): Indicate, if meta keys should be included in the folder names
        save_meta (bool): Save image meta in json file
        filename_prefix (str): Prepend every filename with this
        filename_suffix (str): Add this suffix to every filename
        filename_hook (Callable[[Image], Path]): Callable that is used to determine the filename
    """

    path = __assurePath(path)

    if mkdir:
        path.mkdir(parents=True, exist_ok=True)

    # check if meta cache exists and delete if necessary
    cache_file = _get_meta_cache_path(path)
    if save_meta and cache_file.is_file():
        cache_file.unlink()

    for image in tqdm(sequence.images):

        # determine filename
        fn = filename_hook(image)
        fn = Path(filename_prefix + fn.stem + filename_suffix + fn.suffix)

        fpath = path
        if hierarchical is not None:
            for mk in hierarchical:
                if not include_meta_keys:
                    fpath /= str(image.get_meta(mk))
                else:
                    fpath /= "{}_{}".format(mk, str(image.get_meta(mk)))

        if mkdir:
            fpath.mkdir(parents=True, exist_ok=True)

        fpath /= fn
        save_image(
            fpath,
            image,
            with_visusalization=with_visualization,
            save_meta=save_meta,
            **kwargs
        )


def load_json_object_masks(path: PathOrStr) -> ObjectAnnotations:
    """Load object annotations from file

    Args:
        path (PathOrStr): Path to the annotations file
    
    Returns:
        annotations: Dict with filenames a keys and a list of annotations, where every
            annotation is a tuple of "classname" and a Polygon
    """

    path = __assurePath(path)

    with open(path, "r") as f:
        js = json.load(f)

    if isinstance(js, list):
        result = dict()
        for item in js:
            anns = list()
            for k, v in item["Label"].items():
                for ann in v:
                    poly = Polygon([(x["x"], x["y"]) for x in ann["geometry"]])
                    anns.append((k, poly))
            result[item["External ID"]] = anns
        return result
    elif isinstance(js, dict):
        result = dict()
        prefix_len = 124

        # id -> category name
        catbyid = dict()
        for item in js["categories"]:
            catbyid[item["id"]] = item["name"]

        for img in js["images"]:
            fn = urllib.parse.unquote(img["file_name"][prefix_len:])
            result[fn] = list()
            for item in js["annotations"]:
                if item["image_id"] == img["id"]:
                    x = item["segmentation"][0]
                    poly = Polygon(
                        [(x[2 * i + 0], x[2 * i + 1]) for i in range(len(x) // 2)]
                    )
                    result[fn].append((catbyid[item["category_id"]], poly))

        return result

Functions

def load_json_object_masks(path: Union[pathlib.Path, str]) ‑> Dict[str, List[Tuple[str, shapely.geometry.polygon.Polygon]]]

Load object annotations from file

Args

path : PathOrStr
Path to the annotations file

Returns

annotations
Dict with filenames a keys and a list of annotations, where every annotation is a tuple of "classname" and a Polygon
Expand source code
def load_json_object_masks(path: PathOrStr) -> ObjectAnnotations:
    """Load object annotations from file

    Args:
        path (PathOrStr): Path to the annotations file
    
    Returns:
        annotations: Dict with filenames a keys and a list of annotations, where every
            annotation is a tuple of "classname" and a Polygon
    """

    path = __assurePath(path)

    with open(path, "r") as f:
        js = json.load(f)

    if isinstance(js, list):
        result = dict()
        for item in js:
            anns = list()
            for k, v in item["Label"].items():
                for ann in v:
                    poly = Polygon([(x["x"], x["y"]) for x in ann["geometry"]])
                    anns.append((k, poly))
            result[item["External ID"]] = anns
        return result
    elif isinstance(js, dict):
        result = dict()
        prefix_len = 124

        # id -> category name
        catbyid = dict()
        for item in js["categories"]:
            catbyid[item["id"]] = item["name"]

        for img in js["images"]:
            fn = urllib.parse.unquote(img["file_name"][prefix_len:])
            result[fn] = list()
            for item in js["annotations"]:
                if item["image_id"] == img["id"]:
                    x = item["segmentation"][0]
                    poly = Polygon(
                        [(x[2 * i + 0], x[2 * i + 1]) for i in range(len(x) // 2)]
                    )
                    result[fn].append((catbyid[item["category_id"]], poly))

        return result
def read_image(path: Union[pathlib.Path, str], modality: int = None, force_dtype: DType = None, lazy: bool = True) ‑> Image

Read a single image of a solar module and return it

Args

path : PathOrStr
Path to the file to be read
modality : int
The imaging modality
force_dtype : DType
Force images to have this datatype
lazy : bool
Enable lazy loading for this image

Returns

image
The module image
Expand source code
def read_image(
    path: PathOrStr, modality: int = None, force_dtype: DType = None, lazy: bool = True,
) -> Image:
    """Read a single image of a solar module and return it

    Args:
        path (PathOrStr): Path to the file to be read
        modality (int): The imaging modality
        force_dtype (DType): Force images to have this datatype
        lazy (bool): Enable lazy loading for this image

    Returns:
        image: The module image
    """

    return _read_image(
        path=path,
        is_module_image=False,
        is_partial_module=False,
        lazy=lazy,
        modality=modality,
        force_dtype=force_dtype,
    )
def read_images(path: Union[pathlib.Path, str], same_camera: bool = False, modality: int = None, N: int = 0, pattern: Union[str, Tuple[str]] = ('*.png', '*.tif', '*.tiff', '*.bmp'), allow_different_dtypes=False, force_dtype: DType = None, lazy: bool = True) ‑> ImageSequence

Read a sequence of images and return it

Args

path : PathOrStr
Path to the sequence
same_camera : bool
Indicate, if all images are from the same camera and hence share the same intrinsic parameters
modality : int
The imaging modality
N : int
Only read first N images
pattern : Union[str, Tuple[str]]
Files must match any of the given pattern
allow_different_dtypes : bool
Allow images to have different datatypes?
force_dtype : DType
Force images to have this datatype
lazy : bool
Enable lazy loading for these images

Returns

image
The image sequence
Expand source code
def read_images(
    path: PathOrStr,
    same_camera: bool = False,
    modality: int = None,
    N: int = 0,
    pattern: Union[str, Tuple[str]] = ("*.png", "*.tif", "*.tiff", "*.bmp"),
    allow_different_dtypes=False,
    force_dtype: DType = None,
    lazy: bool = True,
) -> ImageSequence:
    """Read a sequence of images and return it

    Args:
        path (PathOrStr): Path to the sequence
        same_camera (bool): Indicate, if all images are from the same camera and hence share the same intrinsic parameters
        modality (int): The imaging modality
        N (int): Only read first N images
        pattern (Union[str, Tuple[str]]): Files must match any of the given pattern
        allow_different_dtypes (bool): Allow images to have different datatypes?
        force_dtype (DType): Force images to have this datatype
        lazy (bool): Enable lazy loading for these images

    Returns:
        image: The image sequence
    """

    return _read_images(
        path,
        is_module_image=False,
        same_camera=same_camera,
        is_partial_module=False,
        lazy=lazy,
        modality=modality,
        pattern=pattern,
        allow_different_dtypes=allow_different_dtypes,
        N=N,
        force_dtype=force_dtype,
    )
def read_module_image(path: Union[pathlib.Path, str], modality: Modality, cols: int = None, rows: int = None, lazy: bool = True) ‑> ModuleImage

Read a single image of a solar module and return it

Args

path : PathOrStr
Path to the file to be read
modality : int
The imaging modality
cols : int
Number of columns of cells
rows : int
Number of rows of cells
lazy : bool
Enable lazy loading for this image

Returns

image
The module image
Expand source code
def read_module_image(
    path: PathOrStr,
    modality: Modality,
    cols: int = None,
    rows: int = None,
    lazy: bool = True,
) -> ModuleImage:
    """Read a single image of a solar module and return it

    Args:
        path (PathOrStr): Path to the file to be read
        modality (int): The imaging modality
        cols (int): Number of columns of cells
        rows (int): Number of rows of cells
        lazy (bool): Enable lazy loading for this image

    Returns:
        image: The module image
    """

    return _read_image(
        path=path,
        is_module_image=True,
        is_partial_module=False,
        lazy=lazy,
        modality=modality,
        cols=cols,
        rows=rows,
    )
def read_module_images(path: Union[pathlib.Path, str], modality: int, same_camera: bool = False, cols: int = None, rows: int = None, N: int = 0, pattern: Union[str, Tuple[str]] = ('*.png', '*.tif', '*.tiff', '*.bmp'), allow_different_dtypes=False, force_dtype: DType = None, lazy: bool = True) ‑> ModuleImageSequence

Read a sequence of module images and return it

Args

path : PathOrStr
Path to the sequence
modality : int
The imaging modality
same_camera : bool
Indicate, if all images are from the same camera and hence share the same intrinsic parameters
cols : int
Number of columns of cells
rows : int
Number of rows of cells
N : int
Only read first N images
pattern : Union[str, Tuple[str]]
Files must match any of the given pattern
allow_different_dtypes : bool
Allow images to have different datatypes?
force_dtype : DType
Force images to have this datatype
lazy : bool
Enable lazy loading for these images

Returns

image
The module image sequence
Expand source code
def read_module_images(
    path: PathOrStr,
    modality: int,
    same_camera: bool = False,
    cols: int = None,
    rows: int = None,
    N: int = 0,
    pattern: Union[str, Tuple[str]] = ("*.png", "*.tif", "*.tiff", "*.bmp"),
    allow_different_dtypes=False,
    force_dtype: DType = None,
    lazy: bool = True,
) -> ModuleImageSequence:
    """Read a sequence of module images and return it

    Args:
        path (PathOrStr): Path to the sequence
        modality (int): The imaging modality
        same_camera (bool): Indicate, if all images are from the same camera and hence share the same intrinsic parameters
        cols (int): Number of columns of cells
        rows (int): Number of rows of cells
        N (int): Only read first N images
        pattern (Union[str, Tuple[str]]): Files must match any of the given pattern
        allow_different_dtypes (bool): Allow images to have different datatypes?
        force_dtype (DType): Force images to have this datatype
        lazy (bool): Enable lazy loading for these images

    Returns:
        image: The module image sequence
    """

    return _read_images(
        path=path,
        modality=modality,
        same_camera=same_camera,
        is_partial_module=False,
        is_module_image=True,
        lazy=lazy,
        cols=cols,
        rows=rows,
        N=N,
        pattern=pattern,
        allow_different_dtypes=allow_different_dtypes,
        force_dtype=force_dtype,
    )
def read_partial_module_image(path: Union[pathlib.Path, str], modality: int, cols: Optional[int] = None, rows: Optional[int] = None, first_col: Optional[int] = None, first_row: Optional[int] = None, lazy: bool = True) ‑> ModuleImage

Read a single partial view of a solar module and return it

Args

path : PathOrStr
Path to the file to be read
modality : int
The imaging modality
cols : int
Number of completely visible columns of cells
rows : int
Number of completely visible rows of cells
lazy : bool
Enable lazy loading for this image

Returns

image
The module image
Expand source code
def read_partial_module_image(
    path: PathOrStr,
    modality: int,
    cols: Optional[int] = None,
    rows: Optional[int] = None,
    first_col: Optional[int] = None,
    first_row: Optional[int] = None,
    lazy: bool = True,
) -> ModuleImage:
    """Read a single partial view of a solar module and return it

    Args:
        path (PathOrStr): Path to the file to be read
        modality (int): The imaging modality
        cols (int): Number of completely visible columns of cells
        rows (int): Number of completely visible rows of cells
        lazy (bool): Enable lazy loading for this image

    Returns:
        image: The module image
    """

    return _read_image(
        path=path,
        is_module_image=False,
        is_partial_module=True,
        lazy=lazy,
        modality=modality,
        cols=cols,
        rows=rows,
        first_col=first_col,
        first_row=first_row,
    )
def read_partial_module_images(path: Union[pathlib.Path, str], modality: int, same_camera: bool = False, cols: int = None, rows: int = None, N: int = 0, pattern: Union[str, Tuple[str]] = ('*.png', '*.tif', '*.tiff', '*.bmp'), allow_different_dtypes=False, force_dtype: DType = None, lazy: bool = True) ‑> ModuleImageSequence

Read a sequence of partial views of solar modules and return it

Args

path : PathOrStr
Path to the sequence
modality : int
The imaging modality
same_camera : bool
Indicate, if all images are from the same camera and hence share the same intrinsic parameters
cols : int
Number of completely visible columns of cells
rows : int
Number of completely visible rows of cells
N : int
Only read first N images
pattern : Union[str, Tuple[str]]
Files must match any of the given pattern
allow_different_dtypes : bool
Allow images to have different datatypes?
force_dtype : DType
Force images to have this datatype
lazy : bool
Enable lazy loading for these images

Returns

image
The module image sequence
Expand source code
def read_partial_module_images(
    path: PathOrStr,
    modality: int,
    same_camera: bool = False,
    cols: int = None,
    rows: int = None,
    N: int = 0,
    pattern: Union[str, Tuple[str]] = ("*.png", "*.tif", "*.tiff", "*.bmp"),
    allow_different_dtypes=False,
    force_dtype: DType = None,
    lazy: bool = True,
) -> ModuleImageSequence:
    """Read a sequence of partial views of solar modules and return it

    Args:
        path (PathOrStr): Path to the sequence
        modality (int): The imaging modality
        same_camera (bool): Indicate, if all images are from the same camera and hence share the same intrinsic parameters
        cols (int): Number of completely visible columns of cells
        rows (int): Number of completely visible rows of cells
        N (int): Only read first N images
        pattern (Union[str, Tuple[str]]): Files must match any of the given pattern
        allow_different_dtypes (bool): Allow images to have different datatypes?
        force_dtype (DType): Force images to have this datatype
        lazy (bool): Enable lazy loading for these images

    Returns:
        image: The module image sequence
    """

    return _read_images(
        path=path,
        is_module_image=False,
        is_partial_module=True,
        same_camera=same_camera,
        lazy=lazy,
        modality=modality,
        cols=cols,
        rows=rows,
        N=N,
        pattern=pattern,
        allow_different_dtypes=allow_different_dtypes,
        force_dtype=force_dtype,
    )
def save_image(filename: Union[pathlib.Path, str], image: Image, with_visusalization: bool = False, save_meta: bool = False, **kwargs)

Write an image to disk. Float64 is automatically converted to float32 in order to be compatible to ImageJ.

Args

filename : PathOrStr
Filename of the resulting image
image : Image
The image
with_visualization : bool
Include the same visualizations as with image.show() or sequence.head()
save_meta : bool
Save meta data to separate json file (cannot be used with visualization)
Expand source code
def save_image(
    filename: PathOrStr,
    image: Image,
    with_visusalization: bool = False,
    save_meta: bool = False,
    **kwargs
):
    """Write an image to disk. Float64 is automatically converted to float32 in order to be compatible to ImageJ.

    Args:
        filename (PathOrStr): Filename of the resulting image
        image (Image): The image
        with_visualization (bool): Include the same visualizations as with image.show() or sequence.head()
        save_meta (bool): Save meta data to separate json file (cannot be used with visualization)
    """
    filename = __assurePath(filename)

    if with_visusalization and save_meta:
        logging.error("Cannot save meta data for image with visualization")
        return

    if with_visusalization:
        plt.clf()
        image.show(**kwargs)
        plt.savefig(filename, **kwargs)
    else:
        if image.dtype == DType.FLOAT:
            io.imsave(filename, image.data.astype(np.float32), check_contrast=False)
        else:
            io.imsave(filename, image.data, check_contrast=False)
        if save_meta and len(image.list_meta()) > 0:
            meta = {k: image.get_meta(k) for k in image.list_meta()}
            meta = _prepare_json_meta(meta)
            meta_path = _get_meta_path(filename)
            with open(meta_path, "w") as f:
                json.dump(meta, f)

    # delete cached entry
    cache_file = _get_meta_cache_path(filename.parent)
    if save_meta and cache_file.is_file():
        cached_meta: pd.DataFrame = pd.read_pickle(cache_file)
        cached_meta.drop(filename.name, inplace=True)
        cached_meta.to_pickle(cache_file)
def save_images(path: Union[pathlib.Path, str], sequence: ImageSequence, mkdir: bool = True, with_visualization: bool = False, hierarchical: List[str] = None, include_meta_keys: bool = True, save_meta: bool = False, filename_prefix: str = '', filename_suffix: str = '', filename_hook: Callable[[Image], pathlib.Path] = <function <lambda>>, **kwargs)

Write a sequence of images to disk

Args

path : PathOrStr
Target directory
sequence : ImageSequence
The sequence of images
mkdir : bool
Automatically create missing directories
with_visualization : bool
Include the same visualizations as with image.show() or sequence.head()
hierarchical : List[str]
Create a directory hierarchy using given meta keys
include_meta_keys : bool
Indicate, if meta keys should be included in the folder names
save_meta : bool
Save image meta in json file
filename_prefix : str
Prepend every filename with this
filename_suffix : str
Add this suffix to every filename
filename_hook : Callable[[Image], Path]
Callable that is used to determine the filename
Expand source code
def save_images(
    path: PathOrStr,
    sequence: ImageSequence,
    mkdir: bool = True,
    with_visualization: bool = False,
    hierarchical: List[str] = None,
    include_meta_keys: bool = True,
    save_meta: bool = False,
    filename_prefix: str = "",
    filename_suffix: str = "",
    filename_hook: Callable[[Image], Path] = lambda image: Path(image.path.name),
    **kwargs
):
    """Write a sequence of images to disk

    Args:
        path (PathOrStr): Target directory
        sequence (ImageSequence): The sequence of images
        mkdir (bool): Automatically create missing directories
        with_visualization (bool): Include the same visualizations as with image.show() or sequence.head()
        hierarchical (List[str]): Create a directory hierarchy using given meta keys
        include_meta_keys (bool): Indicate, if meta keys should be included in the folder names
        save_meta (bool): Save image meta in json file
        filename_prefix (str): Prepend every filename with this
        filename_suffix (str): Add this suffix to every filename
        filename_hook (Callable[[Image], Path]): Callable that is used to determine the filename
    """

    path = __assurePath(path)

    if mkdir:
        path.mkdir(parents=True, exist_ok=True)

    # check if meta cache exists and delete if necessary
    cache_file = _get_meta_cache_path(path)
    if save_meta and cache_file.is_file():
        cache_file.unlink()

    for image in tqdm(sequence.images):

        # determine filename
        fn = filename_hook(image)
        fn = Path(filename_prefix + fn.stem + filename_suffix + fn.suffix)

        fpath = path
        if hierarchical is not None:
            for mk in hierarchical:
                if not include_meta_keys:
                    fpath /= str(image.get_meta(mk))
                else:
                    fpath /= "{}_{}".format(mk, str(image.get_meta(mk)))

        if mkdir:
            fpath.mkdir(parents=True, exist_ok=True)

        fpath /= fn
        save_image(
            fpath,
            image,
            with_visusalization=with_visualization,
            save_meta=save_meta,
            **kwargs
        )