Class: Rpdfium::Util::ColumnInference
- Inherits:
-
Object
- Object
- Rpdfium::Util::ColumnInference
- Defined in:
- lib/rpdfium/util/column_inference.rb
Overview
Inferenza di colonne dati su PDF non-tabellari.
Identifica gruppi di word che appartengono alla stessa “colonna” verticale di un layout (es. una colonna di importi in un modulo prestampato) anche quando non ci sono linee disegnate.
L’algoritmo opera in tre passaggi:
-
**Cluster per coordinata X** — raggruppa le word con la stessa x0 (left-aligned) o x1 (right-aligned, tipico dei numeri) entro la tolleranza configurabile.
-
**Spezza per gap verticali** — se due word consecutive in un gruppo hanno un gap verticale “anomalo” (> 3× la mediana, o > 40pt), le separa in colonne distinte. Risolve casi tipo “codice fiscale in alto + tabella sotto” che condividono la stessa X.
-
**Filtra per densità** — una colonna “vera” ha valori regolarmente equispaziati (coefficiente di variazione dei gap < soglia). Esclude falsi positivi come valori isolati che si trovano per caso allineati.
Constant Summary collapse
- DEFAULT_X_TOLERANCE =
3.0- DEFAULT_MIN_SIZE =
3- DEFAULT_CV_THRESHOLD =
0.15- DEFAULT_GAP_MULTIPLIER =
3.0- DEFAULT_GAP_ABSOLUTE =
40.0
Instance Method Summary collapse
-
#cluster_by(words, coord) ⇒ Object
Cluster di word per una specifica coordinata.
-
#dense_enough?(col_values) ⇒ Boolean
Una colonna è “abbastanza densa” se ha almeno min_size valori e il coefficiente di variazione (std_dev/mean) dei gap verticali è sotto la soglia.
-
#infer(words) ⇒ Array<Array<Hash>>
Inferisce le colonne dai word forniti.
-
#initialize(x_tolerance: DEFAULT_X_TOLERANCE, min_size: DEFAULT_MIN_SIZE, cv_threshold: DEFAULT_CV_THRESHOLD, gap_multiplier: DEFAULT_GAP_MULTIPLIER, gap_absolute: DEFAULT_GAP_ABSOLUTE) ⇒ ColumnInference
constructor
A new instance of ColumnInference.
Constructor Details
#initialize(x_tolerance: DEFAULT_X_TOLERANCE, min_size: DEFAULT_MIN_SIZE, cv_threshold: DEFAULT_CV_THRESHOLD, gap_multiplier: DEFAULT_GAP_MULTIPLIER, gap_absolute: DEFAULT_GAP_ABSOLUTE) ⇒ ColumnInference
Returns a new instance of ColumnInference.
44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/rpdfium/util/column_inference.rb', line 44 def initialize(x_tolerance: DEFAULT_X_TOLERANCE, min_size: DEFAULT_MIN_SIZE, cv_threshold: DEFAULT_CV_THRESHOLD, gap_multiplier: DEFAULT_GAP_MULTIPLIER, gap_absolute: DEFAULT_GAP_ABSOLUTE) @x_tolerance = x_tolerance @min_size = min_size @cv_threshold = cv_threshold @gap_multiplier = gap_multiplier @gap_absolute = gap_absolute end |
Instance Method Details
#cluster_by(words, coord) ⇒ Object
Cluster di word per una specifica coordinata.
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 |
# File 'lib/rpdfium/util/column_inference.rb', line 77 def cluster_by(words, coord) sorted = words.sort_by { |v| v[coord] } x_groups = [] current = [] sorted.each do |v| if current.empty? || (v[coord] - current.last[coord]).abs <= @x_tolerance current << v else x_groups << current current = [v] end end x_groups << current columns = [] x_groups.each do |group| sorted_y = group.sort_by { |v| v[:top] } gaps = sorted_y.each_cons(2).map { |a, b| b[:top] - a[:top] } if gaps.empty? columns << sorted_y if dense_enough?(sorted_y) next end median_gap = gaps.sort[gaps.size / 2] threshold = [median_gap * @gap_multiplier, @gap_absolute].max sub = [sorted_y.first] sorted_y.each_cons(2) do |a, b| gap = b[:top] - a[:top] if gap > threshold columns << sub if dense_enough?(sub) sub = [b] else sub << b end end columns << sub if dense_enough?(sub) end columns end |
#dense_enough?(col_values) ⇒ Boolean
Una colonna è “abbastanza densa” se ha almeno min_size valori e il coefficiente di variazione (std_dev/mean) dei gap verticali è sotto la soglia. CV bassa = spacing regolare = colonna ripetitiva vera (vs. valori sparsi accidentalmente allineati).
123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/rpdfium/util/column_inference.rb', line 123 def dense_enough?(col_values) return false if col_values.size < @min_size sorted_y = col_values.sort_by { |v| v[:top] } gaps = sorted_y.each_cons(2).map { |a, b| b[:top] - a[:top] } return true if gaps.size < 2 mean = gaps.sum / gaps.size.to_f variance = gaps.map { |g| (g - mean)**2 }.sum / gaps.size std_dev = Math.sqrt(variance) cv = mean.zero? ? Float::INFINITY : std_dev / mean cv < @cv_threshold end |
#infer(words) ⇒ Array<Array<Hash>>
Inferisce le colonne dai word forniti. Usa sia x0 (left-align) che x1 (right-align) come criteri di allineamento, ritorna l’unione delle colonne identificate.
63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/rpdfium/util/column_inference.rb', line 63 def infer(words) return [] if words.empty? by_x0 = cluster_by(words, :x0) by_x1 = cluster_by(words, :x1) # Unione: una word può apparire in più colonne. È compito del # chiamante decidere come gestire (es. preferire la prima # colonna, o quella più grande). Qui ritorniamo tutte. (by_x0 + by_x1) end |