Mayo commited on
Commit
3017cfb
·
unverified ·
1 Parent(s): beaf962

refactor(ml): shared TextRegion/TextDirection types for pipeline engines

Browse files
koharu-ml/Cargo.toml CHANGED
@@ -12,7 +12,6 @@ keywords.workspace = true
12
  publish.workspace = true
13
 
14
  [dependencies]
15
- koharu-core = { workspace = true }
16
  koharu-runtime = { workspace = true }
17
  anyhow = { workspace = true }
18
  clap = { workspace = true }
 
12
  publish.workspace = true
13
 
14
  [dependencies]
 
15
  koharu-runtime = { workspace = true }
16
  anyhow = { workspace = true }
17
  clap = { workspace = true }
koharu-ml/bin/mit48px-ocr.rs CHANGED
@@ -1,5 +1,5 @@
1
  use clap::Parser;
2
- use koharu_core::TextBlock;
3
  use koharu_ml::mit48px_ocr::{Mit48pxBlockPrediction, Mit48pxOcr, Mit48pxPrediction};
4
  use koharu_runtime::{ComputePolicy, RuntimeManager, default_app_data_root};
5
 
@@ -54,7 +54,7 @@ async fn main() -> anyhow::Result<()> {
54
  };
55
 
56
  let output = if let Some(blocks_path) = &cli.blocks_json {
57
- let blocks: Vec<TextBlock> = serde_json::from_str(&std::fs::read_to_string(blocks_path)?)?;
58
  let predictions = model.inference_text_blocks(&image, &blocks)?;
59
  for prediction in &predictions {
60
  println!(
 
1
  use clap::Parser;
2
+ use koharu_ml::TextRegion;
3
  use koharu_ml::mit48px_ocr::{Mit48pxBlockPrediction, Mit48pxOcr, Mit48pxPrediction};
4
  use koharu_runtime::{ComputePolicy, RuntimeManager, default_app_data_root};
5
 
 
54
  };
55
 
56
  let output = if let Some(blocks_path) = &cli.blocks_json {
57
+ let blocks: Vec<TextRegion> = serde_json::from_str(&std::fs::read_to_string(blocks_path)?)?;
58
  let predictions = model.inference_text_blocks(&image, &blocks)?;
59
  for prediction in &predictions {
60
  println!(
koharu-ml/src/comic_text_bubble_detector/mod.rs CHANGED
@@ -4,12 +4,11 @@ use std::{collections::BTreeMap, time::Instant};
4
 
5
  use anyhow::{Context, Result, bail};
6
  use image::{DynamicImage, GenericImageView, imageops::FilterType};
7
- use koharu_core::TextBlock;
8
  use koharu_runtime::RuntimeManager;
9
  use serde::{Deserialize, Serialize};
10
  use tracing::instrument;
11
 
12
- use crate::{Device, device, loading};
13
 
14
  use self::model::{RTDetrV2ForObjectDetection, RTDetrV2Outputs};
15
 
@@ -134,7 +133,7 @@ pub struct ComicTextBubbleDetection {
134
  pub image_width: u32,
135
  pub image_height: u32,
136
  pub detections: Vec<ComicTextBubbleRegion>,
137
- pub text_blocks: Vec<TextBlock>,
138
  }
139
 
140
  #[derive(Debug, Clone, Serialize)]
@@ -459,7 +458,7 @@ fn post_process_object_detection(
459
  fn detections_to_text_blocks(
460
  image_dimensions: (u32, u32),
461
  detections: &[ComicTextBubbleRegion],
462
- ) -> Vec<TextBlock> {
463
  let text_boxes = merge_text_regions(
464
  detections
465
  .iter()
@@ -480,7 +479,7 @@ fn detections_to_text_blocks(
480
  continue;
481
  }
482
 
483
- let block = TextBlock {
484
  x: bbox[0],
485
  y: bbox[1],
486
  width,
 
4
 
5
  use anyhow::{Context, Result, bail};
6
  use image::{DynamicImage, GenericImageView, imageops::FilterType};
 
7
  use koharu_runtime::RuntimeManager;
8
  use serde::{Deserialize, Serialize};
9
  use tracing::instrument;
10
 
11
+ use crate::{Device, device, loading, types::TextRegion};
12
 
13
  use self::model::{RTDetrV2ForObjectDetection, RTDetrV2Outputs};
14
 
 
133
  pub image_width: u32,
134
  pub image_height: u32,
135
  pub detections: Vec<ComicTextBubbleRegion>,
136
+ pub text_blocks: Vec<TextRegion>,
137
  }
138
 
139
  #[derive(Debug, Clone, Serialize)]
 
458
  fn detections_to_text_blocks(
459
  image_dimensions: (u32, u32),
460
  detections: &[ComicTextBubbleRegion],
461
+ ) -> Vec<TextRegion> {
462
  let text_boxes = merge_text_regions(
463
  detections
464
  .iter()
 
479
  continue;
480
  }
481
 
482
+ let block = TextRegion {
483
  x: bbox[0],
484
  y: bbox[1],
485
  width,
koharu-ml/src/comic_text_detector/mod.rs CHANGED
@@ -9,11 +9,10 @@ use anyhow::{Context, bail};
9
  use candle_core::{DType, Device, IndexOp, Tensor};
10
  use candle_transformers::object_detection::{Bbox, non_maximum_suppression};
11
  use image::{DynamicImage, GenericImageView, GrayImage};
12
- use koharu_core::TextBlock;
13
  use koharu_runtime::RuntimeManager;
14
  use tracing::instrument;
15
 
16
- use crate::{device, loading};
17
 
18
  pub use postprocess::{
19
  ComicTextDetection, Quad, crop_text_block_bbox, extract_text_block_regions,
@@ -353,7 +352,7 @@ fn tensor_channel_to_gray_resized(
353
  ))
354
  }
355
 
356
- fn bboxes_to_text_blocks(mut bboxes: Vec<Bbox<usize>>) -> Vec<TextBlock> {
357
  bboxes.sort_unstable_by(|a, b| {
358
  let ay = a.ymin + (a.ymax - a.ymin) * 0.5;
359
  let by = b.ymin + (b.ymax - b.ymin) * 0.5;
@@ -362,7 +361,7 @@ fn bboxes_to_text_blocks(mut bboxes: Vec<Bbox<usize>>) -> Vec<TextBlock> {
362
 
363
  bboxes
364
  .into_iter()
365
- .map(|bbox| TextBlock {
366
  x: bbox.xmin,
367
  y: bbox.ymin,
368
  width: bbox.xmax - bbox.xmin,
 
9
  use candle_core::{DType, Device, IndexOp, Tensor};
10
  use candle_transformers::object_detection::{Bbox, non_maximum_suppression};
11
  use image::{DynamicImage, GenericImageView, GrayImage};
 
12
  use koharu_runtime::RuntimeManager;
13
  use tracing::instrument;
14
 
15
+ use crate::{device, loading, types::TextRegion};
16
 
17
  pub use postprocess::{
18
  ComicTextDetection, Quad, crop_text_block_bbox, extract_text_block_regions,
 
352
  ))
353
  }
354
 
355
+ fn bboxes_to_text_blocks(mut bboxes: Vec<Bbox<usize>>) -> Vec<TextRegion> {
356
  bboxes.sort_unstable_by(|a, b| {
357
  let ay = a.ymin + (a.ymax - a.ymin) * 0.5;
358
  let by = b.ymin + (b.ymax - b.ymin) * 0.5;
 
361
 
362
  bboxes
363
  .into_iter()
364
+ .map(|bbox| TextRegion {
365
  x: bbox.xmin,
366
  y: bbox.ymin,
367
  width: bbox.xmax - bbox.xmin,
koharu-ml/src/comic_text_detector/postprocess.rs CHANGED
@@ -1,3 +1,4 @@
 
1
  use image::{
2
  DynamicImage, GrayImage, Luma, Rgb, RgbImage,
3
  imageops::{self},
@@ -7,7 +8,6 @@ use imageproc::{
7
  geometric_transformations::{Interpolation, Projection, warp_into},
8
  morphology::dilate,
9
  };
10
- use koharu_core::{TextBlock, TextDirection};
11
 
12
  const FINAL_MASK_DILATE_RADIUS: u8 = 2;
13
 
@@ -18,14 +18,14 @@ pub struct ComicTextDetection {
18
  pub shrink_map: GrayImage,
19
  pub threshold_map: GrayImage,
20
  pub line_polygons: Vec<Quad>,
21
- pub text_blocks: Vec<TextBlock>,
22
  pub mask: GrayImage,
23
  }
24
 
25
  pub fn refine_segmentation_mask(
26
  _image: &DynamicImage,
27
  pred_mask: &GrayImage,
28
- blocks: &[TextBlock],
29
  ) -> GrayImage {
30
  let width = pred_mask.width();
31
  let height = pred_mask.height();
@@ -52,7 +52,7 @@ pub fn refine_segmentation_mask(
52
  }
53
 
54
  // Apply a threshold mask: Pixels are preserved exclusively if their probability
55
- // exceeds the core threshold (`super::BINARY_THRESHOLD`) and they reside within a known TextBlock geometry.
56
  let base = GrayImage::from_fn(width, height, |x, y| {
57
  if in_bounds_mask.get_pixel(x, y)[0] != 0
58
  && pred_mask.get_pixel(x, y)[0] > super::BINARY_THRESHOLD
@@ -76,12 +76,12 @@ pub fn refine_segmentation_mask(
76
  })
77
  }
78
 
79
- pub fn crop_text_block_bbox(image: &DynamicImage, block: &TextBlock) -> DynamicImage {
80
  let [x1, y1, x2, y2] = expanded_text_block_crop_bounds(image.width(), image.height(), block);
81
  image.crop_imm(x1, y1, x2.saturating_sub(x1), y2.saturating_sub(y1))
82
  }
83
 
84
- pub fn extract_text_block_regions(image: &DynamicImage, block: &TextBlock) -> Vec<DynamicImage> {
85
  let Some(line_polygons) = block.line_polygons.as_ref() else {
86
  return vec![crop_text_block_bbox(image, block)];
87
  };
@@ -107,7 +107,7 @@ pub fn extract_text_block_regions(image: &DynamicImage, block: &TextBlock) -> Ve
107
  fn expanded_text_block_crop_bounds(
108
  image_width: u32,
109
  image_height: u32,
110
- block: &TextBlock,
111
  ) -> [u32; 4] {
112
  let should_expand = block.detector.as_deref() == Some("ctd")
113
  || block
@@ -167,7 +167,7 @@ fn expanded_text_block_crop_bounds(
167
  [x1, y1, x2, y2]
168
  }
169
 
170
- fn warp_line_region(image: &RgbImage, block: &TextBlock, line: &Quad) -> Option<RgbImage> {
171
  let expanded = maybe_expand_ctd_line(block, line);
172
  let clipped = clip_quad(&expanded, image.width() as f32, image.height() as f32);
173
  let bbox = quad_bbox(&clipped);
@@ -240,7 +240,7 @@ fn warp_line_region(image: &RgbImage, block: &TextBlock, line: &Quad) -> Option<
240
  }
241
  }
242
 
243
- fn maybe_expand_ctd_line(block: &TextBlock, line: &Quad) -> Quad {
244
  let should_expand = block.detector.as_deref() == Some("ctd")
245
  && block.source_direction == Some(TextDirection::Horizontal);
246
  if !should_expand {
@@ -350,7 +350,7 @@ mod tests {
350
  }
351
  });
352
 
353
- let block = TextBlock {
354
  x: 10.0,
355
  y: 11.0,
356
  width: 4.0, // Limits to roughly [10, 11] to [14, 15]
@@ -375,7 +375,7 @@ mod tests {
375
  #[test]
376
  fn extract_text_block_regions_falls_back_to_bbox_without_lines() {
377
  let image = DynamicImage::ImageRgb8(RgbImage::from_pixel(24, 24, Rgb([255, 255, 255])));
378
- let block = TextBlock {
379
  x: 4.0,
380
  y: 5.0,
381
  width: 10.0,
@@ -392,7 +392,7 @@ mod tests {
392
  #[test]
393
  fn crop_text_block_bbox_expands_ctd_crop() {
394
  let image = DynamicImage::ImageRgb8(RgbImage::from_pixel(48, 48, Rgb([255, 255, 255])));
395
- let block = TextBlock {
396
  x: 10.0,
397
  y: 12.0,
398
  width: 12.0,
 
1
+ use crate::types::{TextDirection, TextRegion};
2
  use image::{
3
  DynamicImage, GrayImage, Luma, Rgb, RgbImage,
4
  imageops::{self},
 
8
  geometric_transformations::{Interpolation, Projection, warp_into},
9
  morphology::dilate,
10
  };
 
11
 
12
  const FINAL_MASK_DILATE_RADIUS: u8 = 2;
13
 
 
18
  pub shrink_map: GrayImage,
19
  pub threshold_map: GrayImage,
20
  pub line_polygons: Vec<Quad>,
21
+ pub text_blocks: Vec<TextRegion>,
22
  pub mask: GrayImage,
23
  }
24
 
25
  pub fn refine_segmentation_mask(
26
  _image: &DynamicImage,
27
  pred_mask: &GrayImage,
28
+ blocks: &[TextRegion],
29
  ) -> GrayImage {
30
  let width = pred_mask.width();
31
  let height = pred_mask.height();
 
52
  }
53
 
54
  // Apply a threshold mask: Pixels are preserved exclusively if their probability
55
+ // exceeds the core threshold (`super::BINARY_THRESHOLD`) and they reside within a known TextRegion geometry.
56
  let base = GrayImage::from_fn(width, height, |x, y| {
57
  if in_bounds_mask.get_pixel(x, y)[0] != 0
58
  && pred_mask.get_pixel(x, y)[0] > super::BINARY_THRESHOLD
 
76
  })
77
  }
78
 
79
+ pub fn crop_text_block_bbox(image: &DynamicImage, block: &TextRegion) -> DynamicImage {
80
  let [x1, y1, x2, y2] = expanded_text_block_crop_bounds(image.width(), image.height(), block);
81
  image.crop_imm(x1, y1, x2.saturating_sub(x1), y2.saturating_sub(y1))
82
  }
83
 
84
+ pub fn extract_text_block_regions(image: &DynamicImage, block: &TextRegion) -> Vec<DynamicImage> {
85
  let Some(line_polygons) = block.line_polygons.as_ref() else {
86
  return vec![crop_text_block_bbox(image, block)];
87
  };
 
107
  fn expanded_text_block_crop_bounds(
108
  image_width: u32,
109
  image_height: u32,
110
+ block: &TextRegion,
111
  ) -> [u32; 4] {
112
  let should_expand = block.detector.as_deref() == Some("ctd")
113
  || block
 
167
  [x1, y1, x2, y2]
168
  }
169
 
170
+ fn warp_line_region(image: &RgbImage, block: &TextRegion, line: &Quad) -> Option<RgbImage> {
171
  let expanded = maybe_expand_ctd_line(block, line);
172
  let clipped = clip_quad(&expanded, image.width() as f32, image.height() as f32);
173
  let bbox = quad_bbox(&clipped);
 
240
  }
241
  }
242
 
243
+ fn maybe_expand_ctd_line(block: &TextRegion, line: &Quad) -> Quad {
244
  let should_expand = block.detector.as_deref() == Some("ctd")
245
  && block.source_direction == Some(TextDirection::Horizontal);
246
  if !should_expand {
 
350
  }
351
  });
352
 
353
+ let block = TextRegion {
354
  x: 10.0,
355
  y: 11.0,
356
  width: 4.0, // Limits to roughly [10, 11] to [14, 15]
 
375
  #[test]
376
  fn extract_text_block_regions_falls_back_to_bbox_without_lines() {
377
  let image = DynamicImage::ImageRgb8(RgbImage::from_pixel(24, 24, Rgb([255, 255, 255])));
378
+ let block = TextRegion {
379
  x: 4.0,
380
  y: 5.0,
381
  width: 10.0,
 
392
  #[test]
393
  fn crop_text_block_bbox_expands_ctd_crop() {
394
  let image = DynamicImage::ImageRgb8(RgbImage::from_pixel(48, 48, Rgb([255, 255, 255])));
395
+ let block = TextRegion {
396
  x: 10.0,
397
  y: 12.0,
398
  width: 12.0,
koharu-ml/src/font_detector/mod.rs CHANGED
@@ -31,7 +31,7 @@ koharu_runtime::declare_hf_model_package!(
31
  order: 141,
32
  );
33
 
34
- pub use koharu_core::{FontPrediction, NamedFontPrediction, TextDirection, TopFont};
35
 
36
  pub struct FontDetector {
37
  model: models::Model,
 
31
  order: 141,
32
  );
33
 
34
+ pub use crate::types::{FontPrediction, NamedFontPrediction, TextDirection, TopFont};
35
 
36
  pub struct FontDetector {
37
  model: models::Model,
koharu-ml/src/lama/mod.rs CHANGED
@@ -1,6 +1,7 @@
1
  mod fft;
2
  mod model;
3
 
 
4
  use anyhow::{Result, bail};
5
  use candle_core::{DType, Device, Tensor};
6
  use image::{
@@ -11,7 +12,6 @@ use imageproc::{
11
  contours::find_contours, distance_transform::Norm, drawing::draw_polygon_mut, edges::canny,
12
  filter::gaussian_blur_f32, morphology::dilate, point::Point,
13
  };
14
- use koharu_core::TextBlock;
15
  use koharu_runtime::RuntimeManager;
16
  use tracing::instrument;
17
 
@@ -90,7 +90,7 @@ impl Lama {
90
  &self,
91
  image: &DynamicImage,
92
  mask: &DynamicImage,
93
- text_blocks: Option<&[TextBlock]>,
94
  ) -> Result<DynamicImage> {
95
  if image.dimensions() != mask.dimensions() {
96
  bail!(
@@ -132,7 +132,7 @@ impl Lama {
132
  &self,
133
  image: &RgbImage,
134
  mask: &GrayImage,
135
- text_blocks: &[TextBlock],
136
  ) -> Result<RgbImage> {
137
  let (im_w, im_h) = image.dimensions();
138
  let mut inpainted = image.clone();
@@ -235,7 +235,7 @@ impl Lama {
235
  }
236
  }
237
 
238
- fn block_xyxy(block: &TextBlock, width: u32, height: u32) -> Option<Xyxy> {
239
  let x1 = block.x.floor().max(0.0) as u32;
240
  let y1 = block.y.floor().max(0.0) as u32;
241
  let x2 = (block.x + block.width).ceil().max(block.x.floor()) as u32;
@@ -570,10 +570,10 @@ mod tests {
570
  enlarge_window, extract_balloon_mask, try_fill_balloon,
571
  };
572
  use crate::inpainting::restore_alpha_channel;
 
573
  use image::{GrayImage, Luma, Rgb, RgbImage};
574
  use imageproc::drawing::draw_hollow_rect_mut;
575
  use imageproc::rect::Rect;
576
- use koharu_core::TextBlock;
577
 
578
  const ALPHA_RING_RADIUS: u8 = 7;
579
 
@@ -688,7 +688,7 @@ mod tests {
688
 
689
  #[test]
690
  fn block_xyxy_rounds_and_clamps_document_coords() {
691
- let block = TextBlock {
692
  x: 10.2,
693
  y: 20.7,
694
  width: 15.1,
 
1
  mod fft;
2
  mod model;
3
 
4
+ use crate::types::TextRegion;
5
  use anyhow::{Result, bail};
6
  use candle_core::{DType, Device, Tensor};
7
  use image::{
 
12
  contours::find_contours, distance_transform::Norm, drawing::draw_polygon_mut, edges::canny,
13
  filter::gaussian_blur_f32, morphology::dilate, point::Point,
14
  };
 
15
  use koharu_runtime::RuntimeManager;
16
  use tracing::instrument;
17
 
 
90
  &self,
91
  image: &DynamicImage,
92
  mask: &DynamicImage,
93
+ text_blocks: Option<&[TextRegion]>,
94
  ) -> Result<DynamicImage> {
95
  if image.dimensions() != mask.dimensions() {
96
  bail!(
 
132
  &self,
133
  image: &RgbImage,
134
  mask: &GrayImage,
135
+ text_blocks: &[TextRegion],
136
  ) -> Result<RgbImage> {
137
  let (im_w, im_h) = image.dimensions();
138
  let mut inpainted = image.clone();
 
235
  }
236
  }
237
 
238
+ fn block_xyxy(block: &TextRegion, width: u32, height: u32) -> Option<Xyxy> {
239
  let x1 = block.x.floor().max(0.0) as u32;
240
  let y1 = block.y.floor().max(0.0) as u32;
241
  let x2 = (block.x + block.width).ceil().max(block.x.floor()) as u32;
 
570
  enlarge_window, extract_balloon_mask, try_fill_balloon,
571
  };
572
  use crate::inpainting::restore_alpha_channel;
573
+ use crate::types::TextRegion;
574
  use image::{GrayImage, Luma, Rgb, RgbImage};
575
  use imageproc::drawing::draw_hollow_rect_mut;
576
  use imageproc::rect::Rect;
 
577
 
578
  const ALPHA_RING_RADIUS: u8 = 7;
579
 
 
688
 
689
  #[test]
690
  fn block_xyxy_rounds_and_clamps_document_coords() {
691
+ let block = TextRegion {
692
  x: 10.2,
693
  y: 20.7,
694
  width: 15.1,
koharu-ml/src/lib.rs CHANGED
@@ -14,6 +14,9 @@ pub mod paddleocr_vl;
14
  pub mod pp_doclayout_v3;
15
  pub mod probability_map;
16
  pub mod speech_bubble_segmentation;
 
 
 
17
 
18
  use anyhow::Result;
19
  use candle_core::utils::{cuda_is_available, metal_is_available};
 
14
  pub mod pp_doclayout_v3;
15
  pub mod probability_map;
16
  pub mod speech_bubble_segmentation;
17
+ pub mod types;
18
+
19
+ pub use types::{FontPrediction, NamedFontPrediction, Quad, TextDirection, TextRegion, TopFont};
20
 
21
  use anyhow::Result;
22
  use candle_core::utils::{cuda_is_available, metal_is_available};
koharu-ml/src/mit48px_ocr/mod.rs CHANGED
@@ -6,14 +6,13 @@ use anyhow::{Context, Result};
6
  use candle_core::{DType, Device, Tensor};
7
  use candle_nn::VarBuilder;
8
  use image::{DynamicImage, RgbImage, imageops::FilterType};
9
- use koharu_core::TextBlock;
10
  use koharu_runtime::RuntimeManager;
11
  use serde::{Deserialize, Serialize};
12
  use tracing::instrument;
13
 
14
  use model::{Mit48pxModel, RawPrediction};
15
 
16
- use crate::{comic_text_detector::extract_text_block_regions, device, loading};
17
 
18
  const OCR_CHUNK_SIZE: usize = 16;
19
  const HF_REPO: &str = "mayocream/mit48px-ocr";
@@ -139,7 +138,7 @@ impl Mit48pxOcr {
139
  pub fn inference_text_blocks(
140
  &self,
141
  image: &DynamicImage,
142
- blocks: &[TextBlock],
143
  ) -> Result<Vec<Mit48pxBlockPrediction>> {
144
  let mut regions = Vec::new();
145
  let mut block_indices = Vec::new();
 
6
  use candle_core::{DType, Device, Tensor};
7
  use candle_nn::VarBuilder;
8
  use image::{DynamicImage, RgbImage, imageops::FilterType};
 
9
  use koharu_runtime::RuntimeManager;
10
  use serde::{Deserialize, Serialize};
11
  use tracing::instrument;
12
 
13
  use model::{Mit48pxModel, RawPrediction};
14
 
15
+ use crate::{comic_text_detector::extract_text_block_regions, device, loading, types::TextRegion};
16
 
17
  const OCR_CHUNK_SIZE: usize = 16;
18
  const HF_REPO: &str = "mayocream/mit48px-ocr";
 
138
  pub fn inference_text_blocks(
139
  &self,
140
  image: &DynamicImage,
141
+ blocks: &[TextRegion],
142
  ) -> Result<Vec<Mit48pxBlockPrediction>> {
143
  let mut regions = Vec::new();
144
  let mut block_indices = Vec::new();
koharu-ml/src/types.rs ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Self-contained primitive types for ML detection/recognition outputs.
2
+ //!
3
+ //! Detector/OCR/font-prediction modules return these; the app layer
4
+ //! (`koharu-app`) maps them into scene `TextData` / `Op` values.
5
+
6
+ use serde::{Deserialize, Serialize};
7
+
8
+ /// Reading axis of a detected text region.
9
+ #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
10
+ pub enum TextDirection {
11
+ #[default]
12
+ Horizontal,
13
+ Vertical,
14
+ }
15
+
16
+ /// A four-point polygon, ordered clockwise from the top-left.
17
+ pub type Quad = [[f32; 2]; 4];
18
+
19
+ /// A rectangle-ish detected text region with geometry + detector metadata.
20
+ /// This is what detectors emit; the app layer maps it into a scene `TextData` node.
21
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
22
+ #[serde(rename_all = "camelCase")]
23
+ pub struct TextRegion {
24
+ pub x: f32,
25
+ pub y: f32,
26
+ pub width: f32,
27
+ pub height: f32,
28
+ pub confidence: f32,
29
+ pub line_polygons: Option<Vec<Quad>>,
30
+ pub source_direction: Option<TextDirection>,
31
+ pub rotation_deg: Option<f32>,
32
+ pub detected_font_size_px: Option<f32>,
33
+ pub detector: Option<String>,
34
+ }
35
+
36
+ /// Font-prediction output from a font-detection model.
37
+ #[derive(Debug, Clone, Serialize, Deserialize)]
38
+ pub struct FontPrediction {
39
+ pub top_fonts: Vec<TopFont>,
40
+ pub named_fonts: Vec<NamedFontPrediction>,
41
+ pub direction: TextDirection,
42
+ pub text_color: [u8; 3],
43
+ pub stroke_color: [u8; 3],
44
+ pub font_size_px: f32,
45
+ pub stroke_width_px: f32,
46
+ pub line_height: f32,
47
+ pub angle_deg: f32,
48
+ }
49
+
50
+ impl Default for FontPrediction {
51
+ fn default() -> Self {
52
+ Self {
53
+ top_fonts: Vec::new(),
54
+ named_fonts: Vec::new(),
55
+ direction: TextDirection::Horizontal,
56
+ text_color: [0, 0, 0],
57
+ stroke_color: [0, 0, 0],
58
+ font_size_px: 0.0,
59
+ stroke_width_px: 0.0,
60
+ line_height: 1.0,
61
+ angle_deg: 0.0,
62
+ }
63
+ }
64
+ }
65
+
66
+ #[derive(Debug, Clone, Serialize, Deserialize)]
67
+ pub struct NamedFontPrediction {
68
+ pub index: usize,
69
+ pub name: String,
70
+ pub language: Option<String>,
71
+ pub probability: f32,
72
+ pub serif: bool,
73
+ }
74
+
75
+ #[derive(Debug, Clone, Serialize, Deserialize)]
76
+ pub struct TopFont {
77
+ pub index: usize,
78
+ pub score: f32,
79
+ }
koharu-ml/tests/inpaint.rs CHANGED
@@ -1,7 +1,7 @@
1
  use std::path::Path;
2
 
3
  use image::GenericImageView;
4
- use koharu_core::TextBlock;
5
  use koharu_ml::aot_inpainting::AotInpainting;
6
  use koharu_ml::lama::Lama;
7
 
@@ -69,7 +69,7 @@ async fn lama_block_aware_inpainting_returns_same_size() -> anyhow::Result<()> {
69
 
70
  assert!(found, "mask fixture should contain a non-empty region");
71
 
72
- let block = TextBlock {
73
  x: min_x as f32,
74
  y: min_y as f32,
75
  width: max_x.saturating_sub(min_x).saturating_add(1) as f32,
 
1
  use std::path::Path;
2
 
3
  use image::GenericImageView;
4
+ use koharu_ml::TextRegion;
5
  use koharu_ml::aot_inpainting::AotInpainting;
6
  use koharu_ml::lama::Lama;
7
 
 
69
 
70
  assert!(found, "mask fixture should contain a non-empty region");
71
 
72
+ let block = TextRegion {
73
  x: min_x as f32,
74
  y: min_y as f32,
75
  width: max_x.saturating_sub(min_x).saturating_add(1) as f32,
koharu-ml/tests/ocr.rs CHANGED
@@ -1,8 +1,8 @@
1
  use std::path::Path;
2
 
3
- use koharu_core::{TextBlock, TextDirection};
4
  use koharu_ml::comic_text_detector::extract_text_block_regions;
5
  use koharu_ml::paddleocr_vl::{PaddleOcrVl, PaddleOcrVlTask};
 
6
 
7
  mod support;
8
 
@@ -11,7 +11,7 @@ mod support;
11
  async fn paddleocr_vl_reads_dialog_image_via_default_block_path() -> anyhow::Result<()> {
12
  let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
13
  let image = image::open(fixtures.join("1.jpg"))?.crop_imm(66, 26, 270, 48);
14
- let block = TextBlock {
15
  x: 0.0,
16
  y: 0.0,
17
  width: image.width() as f32,
 
1
  use std::path::Path;
2
 
 
3
  use koharu_ml::comic_text_detector::extract_text_block_regions;
4
  use koharu_ml::paddleocr_vl::{PaddleOcrVl, PaddleOcrVlTask};
5
+ use koharu_ml::{TextDirection, TextRegion};
6
 
7
  mod support;
8
 
 
11
  async fn paddleocr_vl_reads_dialog_image_via_default_block_path() -> anyhow::Result<()> {
12
  let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
13
  let image = image::open(fixtures.join("1.jpg"))?.crop_imm(66, 26, 270, 48);
14
+ let block = TextRegion {
15
  x: 0.0,
16
  y: 0.0,
17
  width: image.width() as f32,