use crate::core::geometry::TBox;
use crate::recognition::tesseract_blob::BlobNBox;
use crate::utils::Result;
pub struct TextlineProjection {
scale_factor: u32,
projection: Vec<Vec<u32>>,
x_origin: i32,
y_origin: i32,
image_width: u32,
image_height: u32,
}
impl TextlineProjection {
pub fn new(resolution: u32) -> Self {
let scale_factor = (resolution / 100).max(1);
Self {
scale_factor,
projection: Vec::new(),
x_origin: 0,
y_origin: 0,
image_width: 0,
image_height: 0,
}
}
pub fn construct_projection(
&mut self,
blobs: &[BlobNBox],
image_width: u32,
image_height: u32,
) -> Result<()> {
self.image_width = image_width;
self.image_height = image_height;
self.x_origin = 0;
self.y_origin = image_height as i32;
let proj_width = (image_width + self.scale_factor - 1) / self.scale_factor;
let proj_height = (image_height + self.scale_factor - 1) / self.scale_factor;
self.projection = vec![vec![0; proj_width as usize]; proj_height as usize];
for blob in blobs {
self.project_blob(blob);
}
self.smooth_projection();
Ok(())
}
fn project_blob(&mut self, blob: &BlobNBox) {
let bbox = blob.bounding_box();
let width = bbox.width() as u32;
let top = self.image_y_to_projection_y(bbox.top() as u32);
let bottom = self.image_y_to_projection_y(bbox.bottom() as u32);
for y in bottom..=top {
if y < self.projection.len() as u32 {
let left = self.image_x_to_projection_x(bbox.left() as u32);
let right = self.image_x_to_projection_x(bbox.right() as u32);
for x in left..=right {
if x < self.projection[y as usize].len() as u32 {
self.projection[y as usize][x as usize] += width;
}
}
}
}
}
fn smooth_projection(&mut self) {
if self.projection.is_empty() {
return;
}
let height = self.projection.len();
let width = if height > 0 {
self.projection[0].len()
} else {
0
};
let mut smoothed = vec![vec![0u32; width]; height];
for y in 0..height {
for x in 0..width {
let mut sum = 0u32;
let mut count = 0u32;
for dy in -1..=1 {
for dx in -1..=1 {
let ny = (y as i32 + dy).max(0).min(height as i32 - 1) as usize;
let nx = (x as i32 + dx).max(0).min(width as i32 - 1) as usize;
sum += self.projection[ny][nx];
count += 1;
}
}
smoothed[y][x] = if count > 0 { sum / count } else { 0 };
}
}
self.projection = smoothed;
}
fn image_x_to_projection_x(&self, x: u32) -> u32 {
(x / self.scale_factor).min(self.projection[0].len() as u32 - 1)
}
fn image_y_to_projection_y(&self, y: u32) -> u32 {
let proj_height = self.projection.len() as u32;
let inverted_y = self.image_height - 1 - y;
(inverted_y / self.scale_factor).min(proj_height - 1)
}
pub fn evaluate_box(&self, bbox: &TBox) -> i32 {
let top_gradient = self.best_mean_gradient_in_row(
bbox.left() as u32,
bbox.right() as u32,
bbox.top() as u32,
true,
);
let bottom_gradient = -self.best_mean_gradient_in_row(
bbox.left() as u32,
bbox.right() as u32,
bbox.bottom() as u32,
false,
);
let left_gradient = self.best_mean_gradient_in_column(
bbox.left() as u32,
bbox.bottom() as u32,
bbox.top() as u32,
true,
);
let right_gradient = -self.best_mean_gradient_in_column(
bbox.right() as u32,
bbox.bottom() as u32,
bbox.top() as u32,
false,
);
let top_clipped = top_gradient.max(0);
let bottom_clipped = bottom_gradient.max(0);
let left_clipped = left_gradient.max(0);
let right_clipped = right_gradient.max(0);
std::cmp::max(top_clipped, bottom_clipped) - std::cmp::max(left_clipped, right_clipped)
}
fn best_mean_gradient_in_row(&self, x1: u32, x2: u32, y: u32, increasing: bool) -> i32 {
let proj_y = self.image_y_to_projection_y(y);
if proj_y >= self.projection.len() as u32 {
return 0;
}
let proj_x1 = self.image_x_to_projection_x(x1);
let proj_x2 = self.image_x_to_projection_x(x2);
if proj_x1 >= self.projection[proj_y as usize].len() as u32
|| proj_x2 >= self.projection[proj_y as usize].len() as u32
{
return 0;
}
let mut best_gradient = 0i32;
for offset in -2..=2 {
let sample_y = if proj_y as i32 + offset >= 0
&& (proj_y as i32 + offset) < self.projection.len() as i32
{
(proj_y as i32 + offset) as usize
} else {
continue;
};
let mut gradient_sum = 0i32;
let mut count = 0;
for x in proj_x1..=proj_x2 {
if x < self.projection[sample_y].len() as u32 {
let value = self.projection[sample_y][x as usize] as i32;
if increasing {
gradient_sum += value;
} else {
gradient_sum -= value;
}
count += 1;
}
}
if count > 0 {
let gradient = gradient_sum / count as i32;
if gradient.abs() > best_gradient.abs() {
best_gradient = gradient;
}
}
}
best_gradient
}
fn best_mean_gradient_in_column(&self, x: u32, y1: u32, y2: u32, increasing: bool) -> i32 {
let proj_x = self.image_x_to_projection_x(x);
if proj_x >= self.projection[0].len() as u32 {
return 0;
}
let proj_y1 = self.image_y_to_projection_y(y1);
let proj_y2 = self.image_y_to_projection_y(y2);
let mut gradient_sum = 0i32;
let mut count = 0;
let start_y = proj_y1.min(proj_y2);
let end_y = proj_y1.max(proj_y2);
for y in start_y..=end_y {
if y < self.projection.len() as u32 && proj_x < self.projection[y as usize].len() as u32
{
let value = self.projection[y as usize][proj_x as usize] as i32;
if increasing {
gradient_sum += value;
} else {
gradient_sum -= value;
}
count += 1;
}
}
if count > 0 {
gradient_sum / count as i32
} else {
0
}
}
pub fn find_line_centers(&self, threshold_ratio: f32) -> Vec<u32> {
if self.projection.is_empty() {
return Vec::new();
}
let max_value = self
.projection
.iter()
.flat_map(|row| row.iter())
.max()
.copied()
.unwrap_or(0) as f32;
let threshold = (max_value * threshold_ratio) as u32;
let mut peaks = Vec::new();
let height = self.projection.len();
for x in 0..self.projection[0].len() {
let mut in_peak = false;
let mut peak_max = 0u32;
let mut peak_max_y = 0usize;
for y in 0..height {
let value = self.projection[y][x];
if value >= threshold {
if !in_peak {
in_peak = true;
peak_max = value;
peak_max_y = y;
} else if value > peak_max {
peak_max = value;
peak_max_y = y;
}
} else if in_peak {
let image_y = self.projection_y_to_image_y(peak_max_y as u32);
peaks.push(image_y);
in_peak = false;
}
}
if in_peak {
let image_y = self.projection_y_to_image_y(peak_max_y as u32);
peaks.push(image_y);
}
}
peaks.sort();
peaks.dedup();
peaks
}
fn projection_y_to_image_y(&self, proj_y: u32) -> u32 {
let inverted_y = proj_y * self.scale_factor;
self.image_height - 1 - inverted_y
}
}
#[derive(Debug, Clone)]
pub struct TextRow {
blobs: Vec<BlobNBox>,
baseline_m: f32,
baseline_c: f32,
mean_height: f32,
line_spacing: f32,
x_height: f32,
}
impl TextRow {
pub fn new() -> Self {
Self {
blobs: Vec::new(),
baseline_m: 0.0,
baseline_c: 0.0,
mean_height: 0.0,
line_spacing: 0.0,
x_height: 0.0,
}
}
pub fn add_blob(&mut self, blob: BlobNBox) {
self.blobs.push(blob);
}
pub fn fit_baseline(&mut self) -> Result<()> {
if self.blobs.is_empty() {
return Ok(());
}
let mut points = Vec::new();
for blob in &self.blobs {
let bbox = blob.bounding_box();
let x = (bbox.left() + bbox.right()) / 2;
let y = bbox.bottom();
points.push((x as f32, y as f32));
}
let n = points.len() as f32;
let sum_x: f32 = points.iter().map(|(x, _)| x).sum();
let sum_y: f32 = points.iter().map(|(_, y)| y).sum();
let sum_xy: f32 = points.iter().map(|(x, y)| x * y).sum();
let sum_x2: f32 = points.iter().map(|(x, _)| x * x).sum();
let denominator = n * sum_x2 - sum_x * sum_x;
if denominator.abs() < 1e-6 {
self.baseline_m = 0.0;
self.baseline_c = sum_y / n;
} else {
self.baseline_m = (n * sum_xy - sum_x * sum_y) / denominator;
self.baseline_c = (sum_y * sum_x2 - sum_x * sum_xy) / denominator;
}
let heights: Vec<f32> = self
.blobs
.iter()
.map(|b| b.bounding_box().height() as f32)
.collect();
self.mean_height = heights.iter().sum::<f32>() / heights.len() as f32;
self.x_height = self.mean_height * 0.7;
self.line_spacing = self.mean_height * 1.5;
Ok(())
}
pub fn blobs(&self) -> &[BlobNBox] {
&self.blobs
}
pub fn baseline_slope(&self) -> f32 {
self.baseline_m
}
pub fn baseline_intercept(&self) -> f32 {
self.baseline_c
}
pub fn mean_height(&self) -> f32 {
self.mean_height
}
}
impl Default for TextRow {
fn default() -> Self {
Self::new()
}
}
pub fn group_blobs_into_rows(
blobs: &[BlobNBox],
image_width: u32,
image_height: u32,
) -> Result<Vec<TextRow>> {
if blobs.is_empty() {
return Ok(Vec::new());
}
let mut projection = TextlineProjection::new(300); projection.construct_projection(blobs, image_width, image_height)?;
let line_centers = projection.find_line_centers(0.3);
if line_centers.is_empty() {
return group_blobs_into_rows_simple(blobs);
}
let mut rows: Vec<TextRow> = vec![TextRow::new(); line_centers.len()];
for blob in blobs {
let bbox = blob.bounding_box();
let blob_center_y = (bbox.top() + bbox.bottom()) / 2;
let mut best_row_idx = 0;
let mut min_distance = i32::MAX;
for (idx, &line_y) in line_centers.iter().enumerate() {
let distance = (blob_center_y - line_y as i32).abs();
if distance < min_distance {
min_distance = distance;
best_row_idx = idx;
}
}
let line_y = line_centers[best_row_idx];
let blob_height = bbox.height();
let tolerance = blob_height.max(5) / 2;
if (blob_center_y - line_y as i32).abs() <= tolerance as i32 {
rows[best_row_idx].add_blob(blob.clone());
}
}
let mut result_rows = Vec::new();
for mut row in rows {
if !row.blobs().is_empty() {
row.blobs
.sort_by(|a, b| a.bounding_box().left().cmp(&b.bounding_box().left()));
row.fit_baseline()?;
result_rows.push(row);
}
}
Ok(result_rows)
}
fn group_blobs_into_rows_simple(blobs: &[BlobNBox]) -> Result<Vec<TextRow>> {
if blobs.is_empty() {
return Ok(Vec::new());
}
let mut sorted_blobs = blobs.to_vec();
sorted_blobs.sort_by(|a, b| {
let a_center_y = (a.bounding_box().top() + a.bounding_box().bottom()) / 2;
let b_center_y = (b.bounding_box().top() + b.bounding_box().bottom()) / 2;
a_center_y.cmp(&b_center_y)
});
let mut rows = Vec::new();
let mut current_row = TextRow::new();
let mut last_y = None;
for blob in sorted_blobs {
let center_y = (blob.bounding_box().top() + blob.bounding_box().bottom()) / 2;
let height = blob.bounding_box().height();
let tolerance = height.max(5) / 2;
if let Some(last_y_pos) = last_y {
if center_y > last_y_pos + tolerance {
if !current_row.blobs().is_empty() {
current_row.fit_baseline()?;
rows.push(current_row);
current_row = TextRow::new();
}
}
}
current_row.add_blob(blob);
last_y = Some(center_y);
}
if !current_row.blobs().is_empty() {
current_row.fit_baseline()?;
rows.push(current_row);
}
Ok(rows)
}