Source code for miop.image_helper.image

# Copyright (c) 2025, Maxime Paschoud.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
#
# (http://opensource.org/licenses/BSD-3-Clause)
#
# __author__ = "Maxime Paschoud, ETHZ: CMBM"
#

import cv2
import numpy as np
import matplotlib.pyplot as plt
from typing import Self
from tifffile import TiffFile
from numpy import pi
import os

import copy

[docs] class Image: """ Represents an image with associated metadata, and provides tools for preprocessing, transformation, and visualization. The image is loaded from the given path and may include metadata in 'fei', 'custom', or 'none' formats. Basic image transformations such as cropping, rotation, and downsampling are supported. Attributes ---------- image_path : str Path to the image file. image_name : str Name of the image file. image : np.ndarray Image data as a NumPy array, loaded using OpenCV. metadata_type : str Type of metadata used ('fei', 'custom', or 'none'). tilt_axis : list of float Axis about which the tilt is defined. Default is [1., 0, 0]. tilt : float Tilt angle in radians. rotation_z : float In-plane rotation angle (around z-axis) in radians. Automatically snapped to closest multiple of 90°. tilt_rad : bool Whether the tilt is in radians (always True after init). metadata : dict Parsed metadata from the image (if available). """ def __init__(self, image_path, image_name, metadata_type: str, metadata: [] = None, tilt_axis=[1.,0,0]): """ Initializes an Image instance by loading the image and extracting relevant metadata. Parameters ---------- image_path : str Path to the image file. image_name : str Name to associate with the image (used for saving). metadata_type : str Metadata format ('fei', 'custom', or 'none'). metadata : list, optional Custom metadata dictionary (required if `metadata_type='custom'`). Default is None. tilt_axis : list of float, optional Axis about which tilt is defined. Default is [1., 0, 0]. Raises ------ ValueError If the image cannot be loaded. NotImplementedError If the metadata type is unsupported. """ self.image_path = image_path self.image_name = image_name self.image = cv2.imread(image_path, cv2.IMREAD_COLOR) if self.image is None: raise ValueError(f"openCV could not read image at {image_path}") self.metadata_type = metadata_type self.tilt_axis = tilt_axis if metadata_type == 'fei': with TiffFile(self.image_path) as tif_img: self.metadata = tif_img.fei_metadata['EBeam'] self.tilt = self.metadata['StageTa'] self.rotation_z = self.metadata['StageR'] self.tilt_rad = True elif metadata_type == 'custom': self.metadata = metadata self.tilt = metadata['stage_tilt_deg'] * np.pi/180 self.rotation_z = metadata['stage_rotation_deg'] * np.pi/180 self.tilt_rad = True self.tilt_axis = metadata['tilt_axis'] elif metadata_type == 'none': self.tilt = 0 self.rotation_z = 0 self.tilt_rad = True else: raise NotImplementedError(f"Currently only fei and custom metadata types are implemented. metadata_type={metadata_type}") rot_z_values = np.array([0., 90, 180, -90])*np.pi/180 differences = [abs(self.rotation_z - val) for val in rot_z_values] self.rotation_z = rot_z_values[differences.index(min(differences))] @property def shape(self): """ Returns the shape of the image. Returns ------- tuple The shape of the image (height, width, channels). """ return self.image.shape
[docs] def crop(self, dim: [], top_left_corner: []) -> Self: """ Crops the image based on given dimensions and top-left coordinates. Parameters ---------- dim : list of int Width and height of the cropped region [width, height]. top_left_corner : list of int X and Y coordinates of the top-left corner of the cropping box. Returns ------- Self The image object after cropping (for method chaining). """ self.image = self.image[top_left_corner[1]:top_left_corner[1] + dim[1], top_left_corner[0]:top_left_corner[0] + dim[0]] return self
[docs] def downsample(self, factor_x: float, factor_y: float) -> Self: """ Downsamples the image using nearest-neighbor interpolation. Parameters ---------- factor_x : float Downsampling factor in the x-direction (width). factor_y : float Downsampling factor in the y-direction (height). Returns ------- Self The image object after downsampling (for method chaining). """ self.image = cv2.resize(self.image, (0,0), fx=factor_x, fy=factor_y, interpolation=cv2.INTER_NEAREST) return self
[docs] def rotate(self, angle, rad=False): """ Rotates the image by a given angle. Parameters ---------- angle : float Angle to rotate the image. rad : bool, optional If True, the angle is interpreted as radians. Default is False (degrees). Returns ------- np.ndarray Rotated image. """ # angle needs to be in deg to use cv2 methods if rad: angle = angle*180/pi h, w = self.shape[:2] center = (w // 2, h // 2) matrix = cv2.getRotationMatrix2D(center, angle, 1.0) rotated = cv2.warpAffine(self.image, matrix, (w, h)) self.image = rotated return rotated
[docs] def rotate_by_transpose(self): """ Rotates the image using matrix transposition based on `rotation_z`, which is expected to be a multiple of 90 degrees (in radians). Uses NumPy's `rot90`. Returns ------- np.ndarray Rotated image. Raises ------ ValueError If `rotation_z` exceeds 2π radians. """ if self.rotation_z > 2*np.pi: raise ValueError('rotation of the image in the plane (rotation_z attribute) is bigger than 2*np.pi.') k = self.rotation_z / (np.pi / 2) k = int(np.round(k)) self.image = np.rot90(self.image, k=k).copy() return self.image
[docs] def save(self, path_to_dir): """ Saves the image to the specified directory as a PNG file. Parameters ---------- path_to_dir : str Directory where the image should be saved. Returns ------- bool True if the image is successfully saved, False otherwise. """ try: os.makedirs(path_to_dir, exist_ok=True) path = os.path.join(path_to_dir, self.image_name + ".png") return cv2.imwrite(path, self.image) except: return False
[docs] def show(self): """ Displays the image using matplotlib. If the image is not loaded, a warning message is printed. """ if self.image is not None: plt.imshow(self.image) else: print("No image data to plot")