use super::lines::LINE_FEATURES;
use super::BlockVectors;
pub const REACH: usize = 2;
pub const CONTEXT_AT: [usize; 12] = [0, 1, 3, 5, 8, 10, 11, 12, 15, 16, 20, 21];
pub const BLOCK_PREFIX: usize = 30;
pub const WIDTH: usize = 23 + BLOCK_PREFIX + 4 * (CONTEXT_AT.len() + 1);
#[doc(hidden)]
pub fn heading_line_features() -> Vec<String> {
let mut out: Vec<String> = LINE_FEATURES.iter().map(|s| s.to_string()).collect();
out.extend(
super::features::BLOCK_FEATURES[..BLOCK_PREFIX]
.iter()
.map(|n| format!("of_block_{n}")),
);
for off in offsets() {
let side = if off < 0 { "up" } else { "down" };
for &c in &CONTEXT_AT {
out.push(format!("{side}{}_{}", off.unsigned_abs(), LINE_FEATURES[c]));
}
out.push(format!("{side}{}_same_block", off.unsigned_abs()));
}
out
}
fn offsets() -> impl Iterator<Item = isize> {
let reach = REACH as isize;
(-reach..=reach).filter(|o| *o != 0)
}
pub const LINE_CLASSES: [&str; 4] = ["other", "heading", "list", "table"];
pub const HEADING_THRESHOLD: f64 = 0.3;
pub fn class_probabilities(row: &[f64; WIDTH]) -> [f64; 4] {
let margins = super::models::line_classes::predict_margins(row);
let top = margins.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let mut out = margins.map(|m| (m - top).exp());
let sum: f64 = out.iter().sum();
for v in &mut out {
*v /= sum;
}
out
}
pub fn page_class_probabilities(page: &[BlockVectors]) -> Vec<Vec<[f64; 4]>> {
class_probabilities_of_rows(&page_rows(page))
}
pub fn class_probabilities_of_rows(rows: &[Vec<[f64; WIDTH]>]) -> Vec<Vec<[f64; 4]>> {
rows.iter()
.map(|rows| rows.iter().map(class_probabilities).collect())
.collect()
}
pub fn page_rows(page: &[BlockVectors]) -> Vec<Vec<[f64; WIDTH]>> {
let flat: Vec<(usize, &[f64; 23])> = page
.iter()
.enumerate()
.flat_map(|(b, v)| v.lines.iter().map(move |line| (b, line)))
.collect();
let mut out: Vec<Vec<[f64; WIDTH]>> = page
.iter()
.map(|v| Vec::with_capacity(v.lines.len()))
.collect();
for (at, &(block, own)) in flat.iter().enumerate() {
let mut row = [f64::NAN; WIDTH];
row[..23].copy_from_slice(own);
row[23..23 + BLOCK_PREFIX].copy_from_slice(&page[block].block[..BLOCK_PREFIX]);
let mut col = 23 + BLOCK_PREFIX;
for off in offsets() {
let j = at as isize + off;
if j >= 0 && (j as usize) < flat.len() {
let (other_block, other) = flat[j as usize];
for &c in &CONTEXT_AT {
row[col] = other[c];
col += 1;
}
row[col] = if other_block == block { 1.0 } else { 0.0 };
col += 1;
} else {
col += CONTEXT_AT.len() + 1;
}
}
out[block].push(row);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn block(kind: &'static str, lines: usize, mark: f64) -> BlockVectors {
BlockVectors {
page: 0,
kind,
block: [mark; 30],
lines: (0..lines)
.map(|k| {
let mut line = [0.0; 23];
for (i, v) in line.iter_mut().enumerate() {
*v = mark * 100.0 + k as f64 * 10.0 + i as f64 / 100.0;
}
line
})
.collect(),
line_boxes: Vec::new(),
style: Vec::new(),
}
}
#[test]
fn the_names_are_as_wide_as_a_row() {
assert_eq!(heading_line_features().len(), WIDTH);
assert_eq!(WIDTH, 105);
}
#[test]
fn the_line_above_a_block_is_the_last_line_of_the_block_before_it() {
let page = [block("paragraph", 2, 1.0), block("paragraph", 1, 2.0)];
let rows = page_rows(&page);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].len(), 2);
assert_eq!(rows[1].len(), 1);
let names = heading_line_features();
let at = |n: &str| names.iter().position(|x| x == n).expect(n);
let first_of_second = &rows[1][0];
assert_eq!(first_of_second[0], page[1].lines[0][0]);
assert_eq!(first_of_second[at("of_block_size_ratio")], 2.0);
assert_eq!(first_of_second[at("up1_size_ratio")], page[0].lines[1][11]);
assert_eq!(first_of_second[at("up1_same_block")], 0.0);
assert_eq!(first_of_second[at("up2_same_block")], 0.0);
assert_eq!(first_of_second[at("up2_bold")], page[0].lines[0][12]);
assert!(first_of_second[at("down1_bold")].is_nan());
assert!(first_of_second[at("down1_same_block")].is_nan());
let first = &rows[0][0];
assert!(first[at("up1_bold")].is_nan());
assert_eq!(first[at("down1_same_block")], 1.0);
assert_eq!(first[at("down2_same_block")], 0.0);
}
}