Source code for flametrack.analysis.ir_analysis

from __future__ import annotations

from collections.abc import Sequence
from typing import Any

import cv2
import numpy as np
from numpy.typing import NDArray


[docs] def compute_remap_from_homography( homography: NDArray[np.float32] | NDArray[np.float64], width: int, height: int, ) -> tuple[NDArray[np.float32], NDArray[np.float32]]: """ Compute pixelwise remap grids from a homography. Args: homography (numpy.ndarray): 3×3 homography mapping output → input coordinates. width (int): Target image width in pixels. height (int): Target image height in pixels. Returns: tuple[numpy.ndarray, numpy.ndarray]: Two arrays ``(src_x, src_y)`` with shape ``(height, width)``, dtype ``float32`` suitable for ``cv2.remap``. """ # Pixelzentren (x+0.5, y+0.5) map_x_f = np.arange(width, dtype=np.float32) + 0.5 map_y_f = np.arange(height, dtype=np.float32) + 0.5 map_x, map_y = np.meshgrid(map_x_f, map_y_f, indexing="xy") ones = np.ones_like(map_x, dtype=np.float32) target_coords = np.stack([map_x, map_y, ones], axis=-1).reshape(-1, 3).T # (3, N) homography_f: NDArray[np.float32] = np.asarray(homography, dtype=np.float32) source_coords = homography_f @ target_coords source_coords /= source_coords[2, :] # normalize src_x = source_coords[0, :].reshape((height, width)).astype(np.float32, copy=False) src_y = source_coords[1, :].reshape((height, width)).astype(np.float32, copy=False) return src_x, src_y
[docs] def read_ir_data(filename: str) -> NDArray[np.float64]: """ Read raw IR data from a CSV-like ASCII export. The file is scanned until a line ``[Data]`` is found; subsequent lines are parsed using ``;`` as delimiter and a comma-to-dot decimal replacement. Args: filename (str): Path to the IR data file. Returns: numpy.ndarray: 2D array of IR values (dtype ``float64``). Raises: ValueError: If no ``[Data]`` section is found in the file. """ with open(filename, encoding="latin-1") as f: line = f.readline() while line: if line.startswith("[Data]"): arr = np.genfromtxt( (line.replace(",", ".")[:-2] for line in f.readlines()), delimiter=";", ) return np.asarray(arr, dtype=np.float64) line = f.readline() raise ValueError("No data found in file, check file format!")
[docs] def get_dewarp_parameters( corners: NDArray[np.float32] | Sequence[tuple[float, float]], target_pixels_width: int | None = None, target_pixels_height: int | None = None, target_ratio: float | None = None, *, plate_width_m: float | None = None, plate_height_m: float | None = None, pixels_per_millimeter: int = 1, ) -> dict[str, Any]: """ Calculate homography and target geometry for dewarping. You can either pass physical plate dimensions (``plate_width_m``, ``plate_height_m``) plus a pixel density, or infer target geometry from the selected corners and a desired aspect ratio. Args: corners (numpy.ndarray | Sequence[tuple[float, float]]): Four corner points in pixel coordinates, ordered clockwise starting at top-left. target_pixels_width (int, optional): Target width in pixels. If omitted, it will be derived from ``target_ratio`` and the measured corner distances. target_pixels_height (int, optional): Target height in pixels. If omitted, it will be derived from ``target_ratio`` and the measured corner distances. target_ratio (float, optional): Desired aspect ratio ``height / width``. Required if target size is not specified and no physical plate size is provided. plate_width_m (float, optional): Physical plate width in meters. Used with ``pixels_per_millimeter`` to derive target size if provided with ``plate_height_m``. plate_height_m (float, optional): Physical plate height in meters. Used with ``pixels_per_millimeter`` to derive target size if provided with ``plate_width_m``. pixels_per_millimeter (int, optional): Pixel density (px/mm) used when physical dimensions are given. Default is 1. Returns: dict[str, Any]: Dictionary with: - ``transformation_matrix`` (numpy.ndarray): 3×3 homography (float32). - ``target_pixels_width`` (int): Target width in pixels. - ``target_pixels_height`` (int): Target height in pixels. - ``target_ratio`` (float): ``height / width`` of the target. Raises: ValueError: If neither physical dimensions nor a target ratio are provided. Notes: Current conversion multiplies meter values by ``pixels_per_millimeter``. For strict unit consistency, consider using millimeters or ``pixels_per_meter``. """ buffer = 1.1 source_corners: NDArray[np.float32] = np.asarray(corners, dtype=np.float32) # Falls echte Plattenmaße gegeben sind, direkt daraus Pixel ableiten if plate_width_m is not None and plate_height_m is not None: target_pixels_width = int(plate_width_m * pixels_per_millimeter) target_pixels_height = int(plate_height_m * pixels_per_millimeter) # Sonst versucht: aus Ecken + Ratio ab zuleiten if target_pixels_width is None or target_pixels_height is None: if target_ratio is None: raise ValueError("Either plate dimensions or target ratio must be provided") # grobe Abschätzung Breite/Höhe in Pixeln aus den Ecken max_width = max( float(source_corners[1][0] - source_corners[0][0]), float(source_corners[2][0] - source_corners[3][0]), ) max_height = max( float(source_corners[2][1] - source_corners[1][1]), float(source_corners[3][1] - source_corners[0][1]), ) target_pixels_height = int( max(max_height, max_width / float(target_ratio)) * buffer ) target_pixels_width = int(target_pixels_height * float(target_ratio)) tpw = int(target_pixels_width) # mypy-safe ints tph = int(target_pixels_height) target_corners = np.array( [ [0.0, 0.0], [float(tpw), 0.0], [float(tpw), float(tph)], [0.0, float(tph)], ], dtype=np.float32, ) transformation_matrix = cv2.getPerspectiveTransform(source_corners, target_corners) return { "transformation_matrix": np.asarray(transformation_matrix, dtype=np.float32), "target_pixels_width": tpw, "target_pixels_height": tph, # Beibehaltung deines bisherigen Verhältnisses (height/width): "target_ratio": float(tph) / float(tpw), }