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),
    }