File size: 2,990 Bytes
664feed | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | """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"] # in degrees, range ~[-90, 90]
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)
# theta is axial (period 180°): take the directionality-weighted double-angle mean.
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),
}
|