File size: 8,260 Bytes
79574cc
dbab656
79574cc
 
 
dbab656
79574cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dbab656
 
79574cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
"""Rendu HTML « Profil d'image du corpus » — Sprint 93 (A.II.7).

Suite directe ``picarones/core/image_predictive.py``.  Pattern
identique aux autres rendus : server-side, pas de JS, anti-
injection systématique.

Vue
---
Deux blocs dans une section unique :

1. **Complexité paléographique** : moyenne, médiane, min, max,
   écart-type sur l'ensemble du corpus.
2. **Homogénéité du corpus** : score combiné + détail par
   feature (mean, stdev, contribution normalisée).

Adaptive : ``""`` si pas de données.

Note d'intégration
------------------
Module pur — l'utilisateur compose :

.. code-block:: python

    from picarones.measurements.image_predictive import aggregate_corpus_predictive
    from picarones.report.image_predictive_render import (
        build_image_predictive_html,
    )

    qualities = [doc.image_quality.as_dict() for doc in benchmark.docs]
    agg = aggregate_corpus_predictive(qualities)
    html = build_image_predictive_html(agg, labels)
"""

from __future__ import annotations

from html import escape as _e
from typing import Optional


def _color_for_score(score: float) -> str:
    """Vert (faible) → orange → rouge (élevé)."""
    f = max(0.0, min(1.0, score))
    if f < 0.5:
        t = f / 0.5
        r = int(167 + (235 - 167) * t)
        g = int(240 + (180 - 240) * t)
        b = int(167 + (60 - 167) * t)
    else:
        t = (f - 0.5) / 0.5
        r = int(235 + (220 - 235) * t)
        g = int(180 + (50 - 180) * t)
        b = int(60 + (50 - 60) * t)
    return f"#{r:02x}{g:02x}{b:02x}"


_FEATURE_LABEL_KEYS = {
    "noise_level": "imgpred_feat_noise",
    "sharpness_score": "imgpred_feat_sharpness",
    "contrast_score": "imgpred_feat_contrast",
    "rotation_degrees": "imgpred_feat_rotation",
}


def _render_complexity_block(
    aggregated: dict, labels: dict[str, str],
) -> str:
    h_complex = labels.get(
        "imgpred_complexity", "Complexité paléographique",
    )
    h_mean = labels.get("imgpred_mean", "Moyenne")
    h_median = labels.get("imgpred_median", "Médiane")
    h_min = labels.get("imgpred_min", "Min")
    h_max = labels.get("imgpred_max", "Max")
    h_stdev = labels.get("imgpred_stdev", "Écart-type")
    h_docs = labels.get("imgpred_docs", "Docs")
    mean = float(aggregated.get("complexity_mean") or 0.0)
    median = float(aggregated.get("complexity_median") or 0.0)
    mn = float(aggregated.get("complexity_min") or 0.0)
    mx = float(aggregated.get("complexity_max") or 0.0)
    sd = float(aggregated.get("complexity_stdev") or 0.0)
    n_docs = int(aggregated.get("n_docs") or 0)
    color_mean = _color_for_score(mean)
    return (
        f'<div style="font-weight:600;margin:.4rem 0 .3rem 0">'
        f'{_e(h_complex)}</div>'
        '<table style="border-collapse:collapse;width:100%;'
        'font-size:.9rem;margin-bottom:.8rem">'
        f'<thead><tr>'
        f'<th style="padding:.4rem .6rem;text-align:right;'
        f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_mean)}</th>'
        f'<th style="padding:.4rem .6rem;text-align:right;'
        f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_median)}</th>'
        f'<th style="padding:.4rem .6rem;text-align:right;'
        f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_min)}</th>'
        f'<th style="padding:.4rem .6rem;text-align:right;'
        f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_max)}</th>'
        f'<th style="padding:.4rem .6rem;text-align:right;'
        f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_stdev)}</th>'
        f'<th style="padding:.4rem .6rem;text-align:right;'
        f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_docs)}</th>'
        f'</tr></thead>'
        f'<tbody><tr>'
        f'<td style="padding:.4rem .6rem;text-align:right;'
        f'background:{color_mean};font-family:monospace;font-weight:600">'
        f'{mean:.3f}</td>'
        f'<td style="padding:.4rem .6rem;text-align:right;'
        f'font-family:monospace">{median:.3f}</td>'
        f'<td style="padding:.4rem .6rem;text-align:right;'
        f'font-family:monospace">{mn:.3f}</td>'
        f'<td style="padding:.4rem .6rem;text-align:right;'
        f'font-family:monospace">{mx:.3f}</td>'
        f'<td style="padding:.4rem .6rem;text-align:right;'
        f'font-family:monospace">{sd:.3f}</td>'
        f'<td style="padding:.4rem .6rem;text-align:right;'
        f'font-family:monospace">{n_docs}</td>'
        f'</tr></tbody></table>'
    )


def _render_homogeneity_block(
    homogeneity: dict, labels: dict[str, str],
) -> str:
    h_homo = labels.get(
        "imgpred_homogeneity", "Homogénéité du corpus",
    )
    h_feat = labels.get("imgpred_feature", "Feature")
    h_mean = labels.get("imgpred_feat_mean", "Moyenne")
    h_stdev = labels.get("imgpred_feat_stdev", "Écart-type")
    h_norm = labels.get(
        "imgpred_feat_norm", "Contribution normalisée",
    )
    score = float(homogeneity.get("score") or 0.0)
    color = _color_for_score(score)
    parts = [
        f'<div style="font-weight:600;margin:.4rem 0 .3rem 0">'
        f'{_e(h_homo)} : '
        f'<span style="background:{color};padding:.1rem .4rem;'
        f'border-radius:.3rem;font-family:monospace">{score:.3f}</span>'
        f'</div>',
        '<table style="border-collapse:collapse;width:100%;'
        'font-size:.9rem">',
        '<thead><tr>',
    ]
    for col in (h_feat, h_mean, h_stdev, h_norm):
        parts.append(
            f'<th style="padding:.4rem .6rem;text-align:left;'
            f'border-bottom:1px solid #ccc;font-weight:600">'
            f'{_e(col)}</th>'
        )
    parts.append("</tr></thead><tbody>")
    per_feat = homogeneity.get("per_feature") or {}
    for key, label_key in _FEATURE_LABEL_KEYS.items():
        if key not in per_feat:
            continue
        slot = per_feat[key]
        feat_label = labels.get(label_key, key)
        feat_mean = float(slot.get("mean") or 0.0)
        feat_stdev = float(slot.get("stdev") or 0.0)
        feat_norm = float(slot.get("normalised") or 0.0)
        norm_color = _color_for_score(feat_norm)
        parts.append(
            f'<tr>'
            f'<td style="padding:.4rem .6rem">{_e(feat_label)}</td>'
            f'<td style="padding:.4rem .6rem;text-align:right;'
            f'font-family:monospace">{feat_mean:.3f}</td>'
            f'<td style="padding:.4rem .6rem;text-align:right;'
            f'font-family:monospace">{feat_stdev:.3f}</td>'
            f'<td style="padding:.4rem .6rem;text-align:right;'
            f'background:{norm_color};font-family:monospace">'
            f'{feat_norm:.3f}</td>'
            f'</tr>'
        )
    parts.append("</tbody></table>")
    return "".join(parts)


def build_image_predictive_html(
    aggregated: Optional[dict],
    labels: Optional[dict[str, str]] = None,
) -> str:
    """Construit la vue HTML « Profil d'image du corpus ».

    Parameters
    ----------
    aggregated:
        Sortie de ``aggregate_corpus_predictive``.  Si ``None``
        ou ``n_docs == 0``, retourne ``""``.
    labels:
        Dict i18n.  Clés sous le préfixe ``imgpred_*``.
    """
    if not aggregated:
        return ""
    if not aggregated.get("n_docs"):
        return ""
    labels = labels or {}
    title = labels.get(
        "imgpred_title", "Profil d'image du corpus",
    )
    note = labels.get(
        "imgpred_note",
        "Score de complexité paléographique combinant bruit, "
        "flou, faible contraste et rotation. Le score "
        "d'homogénéité signale si la moyenne globale est fiable "
        "(corpus uniforme) ou trompeuse (corpus hétérogène — "
        "voir alors la vue stratifiée).",
    )
    parts = [
        '<section class="imgpred-section" style="margin:1rem 0">',
        f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
        f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
        f'{_e(note)}</div>',
    ]
    parts.append(_render_complexity_block(aggregated, labels))
    homo = aggregated.get("homogeneity")
    if isinstance(homo, dict):
        parts.append(_render_homogeneity_block(homo, labels))
    parts.append("</section>")
    return "".join(parts)


__all__ = ["build_image_predictive_html"]