Spaces:
Sleeping
Sleeping
File size: 5,675 Bytes
99ad1af | 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 | """Métriques structurelles ALTO — Sprint A14-S15.
Métriques typées ``(ALTO_XML, ALTO_XML)`` qui mesurent la fidélité
**documentaire** d'un ALTO produit par un pipeline (par exemple un
reconstructeur post-correction LLM ou un VLM avec module
ALTO_reconstruction) face à la GT ALTO du corpus.
Distinct de ``picarones/measurements/alto_metrics.py`` (legacy)
qui calcule CER/WER sur le **texte extrait** des deux ALTO. Ici
on mesure la **structure** : nombre de lignes, présence de bbox,
ordre de lecture cohérent.
Métriques livrées au S15
------------------------
- ``compute_alto_validity(ref, hyp)`` — 1.0 si l'hypothèse a une
structure cohérente (≥ 1 page, ≥ 1 bloc, ≥ 1 ligne). Détecte
les ALTO vides ou tronqués.
- ``compute_line_count_ratio(ref, hyp)`` — ``min(n_hyp, n_ref) /
max(n_hyp, n_ref)`` ∈ [0, 1]. 1.0 = même nombre de lignes.
- ``compute_word_box_coverage(ref, hyp)`` — fraction des
``AltoString`` de l'hypothèse qui ont une ``bbox``. 1.0 = tous
les mots ont une boîte (cas idéal pour un reconstructeur ALTO).
Reportées à des sprints suivants (post-livraison)
-------------------------------------------------
- ``textline_alignment`` (IoU des bbox de lignes) — exige un
algorithme d'alignement bipartite par bbox.
- ``reading_order_consistency`` (Kendall tau sur les IDs de
lignes) — exige un mapping ID → position.
- ``layout_f1`` (ICDAR 2015) — déjà implémenté dans
``evaluation/metrics/layout.py`` (migré au S10) sur des
``Region`` génériques ; un wrapper ALTO peut être ajouté plus
tard.
Convention de signature
-----------------------
Les payloads attendus sont des ``AltoDocument`` parsés (par le
``payload_loader`` du service applicatif). Si le caller passe
des bytes XML brut, il doit appeler ``parse_alto`` lui-même
en amont.
higher_is_better
----------------
Toutes les métriques de ce module ∈ [0, 1] avec ``higher_is_better=True``
(1.0 = parfait, 0.0 = pire). Cohérent avec le schéma ICDAR pour
les métriques de fidélité documentaire.
"""
from __future__ import annotations
from picarones.formats.alto.types import AltoDocument
def _count_lines(doc: AltoDocument) -> int:
"""Compte le nombre total de ``AltoLine`` dans un document."""
return sum(
len(block.lines)
for page in doc.pages
for block in page.blocks
)
def _count_strings(doc: AltoDocument) -> int:
"""Compte le nombre total de ``AltoString`` dans un document."""
return sum(
len(line.strings)
for page in doc.pages
for block in page.blocks
for line in block.lines
)
def compute_alto_validity(
reference: AltoDocument,
hypothesis: AltoDocument,
) -> float:
"""Vérifie que l'hypothèse a une structure ALTO cohérente.
Cohérence = au moins 1 page ET au moins 1 bloc ET au moins
1 ligne dans l'hypothèse. Détecte les ALTO vides, tronqués,
ou produits par un reconstructeur défaillant.
Returns
-------
float
1.0 si l'hypothèse est structurellement cohérente,
0.0 sinon.
Notes
-----
On ne compare PAS la cohérence à la référence ici — la
référence est juste passée pour homogénéité d'API avec les
autres métriques. Un ALTO de référence vide (cas dégénéré)
n'invalide pas l'hypothèse.
"""
if not hypothesis.pages:
return 0.0
has_block = any(page.blocks for page in hypothesis.pages)
if not has_block:
return 0.0
has_line = any(
block.lines
for page in hypothesis.pages
for block in page.blocks
)
if not has_line:
return 0.0
return 1.0
def compute_line_count_ratio(
reference: AltoDocument,
hypothesis: AltoDocument,
) -> float:
"""Ratio min/max du nombre de lignes des deux ALTO.
Returns
-------
float
``min(n_hyp, n_ref) / max(n_hyp, n_ref)`` ∈ [0, 1].
1.0 = même nombre de lignes. 0.0 si l'un des deux n'a
aucune ligne (cas dégénéré).
Permet de détecter un reconstructeur qui invente ou perd des
lignes vs la GT. Ne dit RIEN sur l'alignement spatial —
c'est ``textline_alignment`` (post-livraison) qui mesurera
cette dimension.
"""
n_ref = _count_lines(reference)
n_hyp = _count_lines(hypothesis)
if n_ref == 0 and n_hyp == 0:
return 1.0 # convention : deux vides identiques
if n_ref == 0 or n_hyp == 0:
return 0.0
return min(n_ref, n_hyp) / max(n_ref, n_hyp)
def compute_word_box_coverage(
reference: AltoDocument,
hypothesis: AltoDocument,
) -> float:
"""Fraction des ``AltoString`` de l'hypothèse qui ont une ``bbox``.
Returns
-------
float
``n_strings_with_bbox / n_strings_total`` ∈ [0, 1].
1.0 = tous les mots ont une boîte (cas idéal pour un
reconstructeur ALTO). 0.0 si l'hypothèse n'a aucun mot.
La référence n'est pas utilisée dans le calcul, mais elle est
passée pour homogénéité d'API. Un caller qui veut comparer
"candidat a-t-il autant de bbox que la GT" peut mesurer
``compute_word_box_coverage(gt, hyp) / compute_word_box_coverage(hyp, gt)``
ou utiliser un calcul dédié.
"""
total = _count_strings(hypothesis)
if total == 0:
return 0.0
with_bbox = sum(
1
for page in hypothesis.pages
for block in page.blocks
for line in block.lines
for s in line.strings
if s.bbox is not None
)
return with_bbox / total
__all__ = [
"compute_alto_validity",
"compute_line_count_ratio",
"compute_word_box_coverage",
]
|