Mayo commited on
refactor(ml): shared TextRegion/TextDirection types for pipeline engines
Browse files- koharu-ml/Cargo.toml +0 -1
- koharu-ml/bin/mit48px-ocr.rs +2 -2
- koharu-ml/src/comic_text_bubble_detector/mod.rs +4 -5
- koharu-ml/src/comic_text_detector/mod.rs +3 -4
- koharu-ml/src/comic_text_detector/postprocess.rs +12 -12
- koharu-ml/src/font_detector/mod.rs +1 -1
- koharu-ml/src/lama/mod.rs +6 -6
- koharu-ml/src/lib.rs +3 -0
- koharu-ml/src/mit48px_ocr/mod.rs +2 -3
- koharu-ml/src/types.rs +79 -0
- koharu-ml/tests/inpaint.rs +2 -2
- koharu-ml/tests/ocr.rs +2 -2
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
|
| 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<
|
| 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<
|
| 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<
|
| 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 =
|
| 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<
|
| 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|
|
| 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<
|
| 22 |
pub mask: GrayImage,
|
| 23 |
}
|
| 24 |
|
| 25 |
pub fn refine_segmentation_mask(
|
| 26 |
_image: &DynamicImage,
|
| 27 |
pred_mask: &GrayImage,
|
| 28 |
-
blocks: &[
|
| 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
|
| 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: &
|
| 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: &
|
| 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: &
|
| 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: &
|
| 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: &
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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
|
| 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<&[
|
| 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: &[
|
| 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: &
|
| 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 =
|
| 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: &[
|
| 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
|
| 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 =
|
| 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 =
|
| 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,
|