| """orientationpy-app — 2D structure-tensor orientation analysis.
|
|
|
| Uses [orientationpy](https://gitlab.com/epfl-center-for-imaging/orientationpy)
|
| to compute per-pixel orientation of a 2D grayscale image, then renders it as
|
| an HSV map where:
|
|
|
| Hue = orientation angle (theta), mapped from [-90°, 90°] to [0, 1]
|
| Saturation = coherency (how directed the structure is)
|
| Value = intensity (trace of the structure tensor), normalized
|
|
|
| Single `process(image, sigma)` — sigma is the Gaussian window for the
|
| structure tensor.
|
| """
|
| from __future__ import annotations
|
|
|
| import colorsys
|
|
|
| import numpy as np
|
| import orientationpy as op
|
| from skimage import color
|
|
|
|
|
| def _to_gray_float(image: np.ndarray) -> np.ndarray:
|
| arr = np.asarray(image)
|
| if arr.ndim == 3 and arr.shape[-1] in (3, 4):
|
| arr = color.rgb2gray(arr[..., :3])
|
| elif arr.ndim == 3:
|
| arr = arr.mean(axis=-1)
|
| return arr.astype(np.float32)
|
|
|
|
|
| def _normalize(a: np.ndarray) -> np.ndarray:
|
| mn, mx = float(a.min()), float(a.max())
|
| rng = max(mx - mn, 1e-8)
|
| return np.clip((a - mn) / rng, 0.0, 1.0)
|
|
|
|
|
| def _hsv_to_rgb(hsv: np.ndarray) -> np.ndarray:
|
| flat = hsv.reshape(-1, 3)
|
| out = np.array([colorsys.hsv_to_rgb(*row) for row in flat])
|
| return out.reshape(hsv.shape)
|
|
|
|
|
| def process(image: np.ndarray, sigma: float = 2.0) -> np.ndarray:
|
| gray = _to_gray_float(image)
|
|
|
| gradients = op.computeGradient(gray, mode="gaussian")
|
| structure_tensor = op.computeStructureTensor(gradients, sigma=float(sigma))
|
|
|
| orientation = op.computeOrientation(structure_tensor)
|
| theta = orientation["theta"]
|
|
|
| intensity = op.computeIntensity(structure_tensor)
|
| directionality = op.computeStructureDirectionality(structure_tensor)
|
|
|
| h = ((theta + 90.0) / 180.0) % 1.0
|
| s = _normalize(directionality)
|
| v = _normalize(intensity)
|
|
|
| hsv = np.stack([h, s, v], axis=-1)
|
| rgb = _hsv_to_rgb(hsv)
|
| return (rgb * 255).astype(np.uint8)
|
|
|
|
|
| def stats(image: np.ndarray, sigma: float = 2.0) -> dict:
|
| """Summary orientation statistics (no image render) — handy over the API."""
|
| gray = _to_gray_float(image)
|
| gradients = op.computeGradient(gray, mode="gaussian")
|
| structure_tensor = op.computeStructureTensor(gradients, sigma=float(sigma))
|
| theta = op.computeOrientation(structure_tensor)["theta"]
|
| directionality = op.computeStructureDirectionality(structure_tensor)
|
|
|
|
|
| w = np.clip(directionality, 0, None)
|
| ang = np.deg2rad(theta * 2.0)
|
| dom = float(np.rad2deg(np.arctan2((w * np.sin(ang)).sum(), (w * np.cos(ang)).sum())) / 2.0)
|
| return {
|
| "dims": [int(d) for d in gray.shape],
|
| "sigma": float(sigma),
|
| "dominant_angle_deg": round(dom, 2),
|
| "mean_coherency": round(float(_normalize(directionality).mean()), 4),
|
| }
|
|
|