# 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")