Skip to content

modosaic.segmentation.validators.impl.normals_field_quality

modosaic.segmentation.validators.impl.normals_field_quality

NormalsFieldQualityValidator

NormalsFieldQualityValidator(*, eps=1e-06, nz_min=0.1)

Bases: NormalsValidator[NormalFieldStats]

Measure smoothness and integrability of a normal field.

Initialize the validator.

Parameters:

Name Type Description Default
eps float

Numerical stability constant for vector normalization.

1e-06
nz_min float

Minimum absolute z component used for integrability.

0.1
Source code in modosaic/segmentation/validators/impl/normals_field_quality.py
19
20
21
22
23
24
25
26
27
def __init__(self, *, eps: float = 1e-6, nz_min: float = 0.1) -> None:
    """Initialize the validator.

    Args:
        eps: Numerical stability constant for vector normalization.
        nz_min: Minimum absolute z component used for integrability.
    """
    self.eps = eps
    self.nz_min = nz_min

validate

validate(record, generated)

Compute normal-field quality statistics.

Source code in modosaic/segmentation/validators/impl/normals_field_quality.py
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
84
85
86
87
88
@override
def validate(self, record: ImageRecord, generated: np.ndarray) -> NormalFieldStats:
    """Compute normal-field quality statistics."""
    logger.debug(f"Computing normals field quality for record sample {record.sample_id}")
    n = np.asarray(generated).astype(np.float32, copy=False)
    if n.ndim != 3 or n.shape[-1] != 3:
        logger.debug(f"Invalid normals shape for record sample {record.sample_id}: {n.shape}")
        return NormalFieldStats()

    n = np.nan_to_num(n, nan=0.0, posinf=0.0, neginf=0.0)
    n = NormalsFieldQualityValidator._normalize_vec(n, eps=self.eps)

    # --- Smoothness: neighbor angular variation ---
    a = n[:, :-1, :]
    b = n[:, 1:, :]
    ang_x = NormalsFieldQualityValidator._neighbor_angle_deg(a, b)

    a2 = n[:-1, :, :]
    b2 = n[1:, :, :]
    ang_y = NormalsFieldQualityValidator._neighbor_angle_deg(a2, b2)

    ang = np.concatenate([ang_x.reshape(-1), ang_y.reshape(-1)], axis=0)
    ang = ang[np.isfinite(ang)]
    if ang.size == 0:
        logger.debug(f"No finite normals neighbor angles for record sample {record.sample_id}")
        return NormalFieldStats()

    smooth_mean = float(np.mean(ang))
    smooth_median = float(np.median(ang))

    # --- Integrability (orthographic): p=-nx/nz, q=-ny/nz, curl = |dp/dy - dq/dx| ---
    nx, ny, nz = n[..., 0], n[..., 1], n[..., 2]
    valid = np.isfinite(nx) & np.isfinite(ny) & np.isfinite(nz) & (np.abs(nz) >= self.nz_min)

    denom = nz + np.sign(nz) * self.eps
    p = -nx / denom
    q = -ny / denom

    # dp/dy
    dpdy = NormalsFieldQualityValidator._finite_difference_gradient(p, axis=0)
    dqdx = NormalsFieldQualityValidator._finite_difference_gradient(q, axis=1)

    curl = np.abs(dpdy - dqdx)
    curl = curl[valid & np.isfinite(curl)]
    if curl.size == 0:
        logger.debug(f"Computed normals smoothness without integrability for record sample {record.sample_id}")
        return NormalFieldStats(
            valid=True,
            smoothness_mean_deg=smooth_mean,
            smoothness_median_deg=smooth_median,
        )

    logger.debug(f"Computed normals field quality for record sample {record.sample_id}")
    return NormalFieldStats(
        valid=True,
        smoothness_mean_deg=smooth_mean,
        smoothness_median_deg=smooth_median,
        integrability_mean=float(np.mean(curl)),
        integrability_median=float(np.median(curl)),
    )