File size: 2,515 Bytes
3f820d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Computes the deterministic difficulty score from post-render geometry metrics.

Pure function of :class:`~src.analysis.geometry_analyzer.GeometryResult` and
the configured weights -- no randomness is involved, so the score is
trivially reproducible given identical geometry inputs.
"""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np

from src.analysis.geometry_analyzer import GeometryResult
from src.config.schema import DifficultyWeights
from src.utils.geometry_utils import clamp01


@dataclass
class DifficultyResult:
    """The five difficulty components, the weighted score, and its tier."""

    misplacement: float
    whitespace: float
    faintness: float
    smallness: float
    low_texture: float
    score: float
    tier: str


class DifficultyAnalyzer:
    """Computes the exact spec-defined difficulty score and tier.

    ``score = 0.35*misplacement + 0.20*whitespace + 0.20*faintness + 0.15*smallness + 0.10*low_texture``

    Example:
        >>> analyzer = DifficultyAnalyzer(cfg.difficulty)
        >>> result = analyzer.analyze(geometry_result)
    """

    def __init__(self, weights: DifficultyWeights) -> None:
        self.weights = weights

    def analyze(self, geometry: GeometryResult) -> DifficultyResult:
        """Compute the difficulty components, weighted score, and tier for one sample."""
        w = self.weights

        misplacement = clamp01(1.0 - geometry.polygon_overlap_proxy)
        whitespace = clamp01(geometry.whitespace_ratio)
        faintness = clamp01((geometry.mean_dark_intensity - 90.0) / 120.0)
        smallness = clamp01(1.0 - geometry.signature_area_ratio / 0.04)
        low_texture = clamp01(1.0 - geometry.gray_stddev / 60.0)

        score = (
            w.misplacement * misplacement
            + w.whitespace * whitespace
            + w.faintness * faintness
            + w.smallness * smallness
            + w.low_texture * low_texture
        )
        score = float(np.clip(score, 0.0, 1.0))

        if score < w.easy_threshold:
            tier = "easy"
        elif score < w.hard_threshold:
            tier = "medium"
        else:
            tier = "hard"

        return DifficultyResult(
            misplacement=round(misplacement, 4),
            whitespace=round(whitespace, 4),
            faintness=round(faintness, 4),
            smallness=round(smallness, 4),
            low_texture=round(low_texture, 4),
            score=round(score, 4),
            tier=tier,
        )