Mayo commited on
Commit
2aa1777
·
unverified ·
1 Parent(s): a4f1e73

add manga-ocr

Browse files
Cargo.lock CHANGED
@@ -130,9 +130,9 @@ dependencies = [
130
 
131
  [[package]]
132
  name = "anyhow"
133
- version = "1.0.97"
134
  source = "registry+https://github.com/rust-lang/crates.io-index"
135
- checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
136
  dependencies = [
137
  "backtrace",
138
  ]
@@ -3104,6 +3104,17 @@ version = "0.2.0"
3104
  source = "registry+https://github.com/rust-lang/crates.io-index"
3105
  checksum = "b8dd856d451cc0da70e2ef2ce95a18e39a93b7558bedf10201ad28503f918568"
3106
 
 
 
 
 
 
 
 
 
 
 
 
3107
  [[package]]
3108
  name = "markup5ever"
3109
  version = "0.11.0"
 
130
 
131
  [[package]]
132
  name = "anyhow"
133
+ version = "1.0.98"
134
  source = "registry+https://github.com/rust-lang/crates.io-index"
135
+ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
136
  dependencies = [
137
  "backtrace",
138
  ]
 
3104
  source = "registry+https://github.com/rust-lang/crates.io-index"
3105
  checksum = "b8dd856d451cc0da70e2ef2ce95a18e39a93b7558bedf10201ad28503f918568"
3106
 
3107
+ [[package]]
3108
+ name = "manga-ocr"
3109
+ version = "0.1.0"
3110
+ dependencies = [
3111
+ "anyhow",
3112
+ "clap",
3113
+ "image",
3114
+ "ndarray",
3115
+ "ort",
3116
+ ]
3117
+
3118
  [[package]]
3119
  name = "markup5ever"
3120
  version = "0.11.0"
Cargo.toml CHANGED
@@ -1,3 +1,3 @@
1
  [workspace]
2
- members = ["src-tauri", "yolo-v8", "comic-text-detector"]
3
  resolver = "2"
 
1
  [workspace]
2
+ members = ["src-tauri", "yolo-v8", "comic-text-detector", "manga-ocr"]
3
  resolver = "2"
manga-ocr/Cargo.toml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "manga-ocr"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+
6
+ [dependencies]
7
+ anyhow = "1.0.98"
8
+ clap = { version = "4.5.36", features = ["derive"] }
9
+ image = "0.25.6"
10
+ ndarray = "0.16.1"
11
+ ort = { version = "=2.0.0-rc.9", features = ["ndarray"] }
manga-ocr/src/main.rs ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::fs;
2
+
3
+ use clap::Parser;
4
+ use image::imageops::FilterType;
5
+ use ndarray::{Array, s};
6
+ use ort::inputs;
7
+ use ort::session::Session;
8
+ use ort::session::builder::GraphOptimizationLevel;
9
+
10
+ #[derive(Parser, Debug)]
11
+ struct Args {
12
+ #[arg(long)]
13
+ image: String,
14
+
15
+ #[arg(long)]
16
+ model: String,
17
+
18
+ #[arg(long)]
19
+ vocab: String,
20
+ }
21
+
22
+ fn main() -> anyhow::Result<()> {
23
+ let args = Args::parse();
24
+
25
+ let model = Session::builder()?
26
+ .with_optimization_level(GraphOptimizationLevel::Level3)?
27
+ .with_intra_threads(4)?
28
+ .commit_from_file(args.model)?;
29
+
30
+ let vocab = fs::read_to_string(args.vocab)
31
+ .map_err(|e| anyhow::anyhow!("Failed to read vocab file: {e}"))?
32
+ .lines()
33
+ .map(|s| s.to_string())
34
+ .collect::<Vec<_>>();
35
+
36
+ let image =
37
+ image::open(&args.image).map_err(|e| anyhow::anyhow!("Failed to open image: {e}"))?;
38
+ let image = image.grayscale().to_rgb8();
39
+ // Resize to 224x224
40
+ let image = image::imageops::resize(&image, 224, 224, FilterType::Lanczos3);
41
+
42
+ // Convert to float32 array and normalize
43
+ let mut tensor = Array::zeros((1, 3, 224, 224));
44
+ for (x, y, pixel) in image.enumerate_pixels() {
45
+ let x = x as usize;
46
+ let y = y as usize;
47
+
48
+ // Normalize from [0, 255] to [-1, 1]
49
+ tensor[[0, 0, y, x]] = (pixel[0] as f32 / 255.0 - 0.5) / 0.5;
50
+ tensor[[0, 1, y, x]] = (pixel[1] as f32 / 255.0 - 0.5) / 0.5;
51
+ tensor[[0, 2, y, x]] = (pixel[2] as f32 / 255.0 - 0.5) / 0.5;
52
+ }
53
+
54
+ // generate
55
+ let mut token_ids: Vec<i64> = vec![2i64]; // Start token
56
+
57
+ for _ in 0..300 {
58
+ // Create input tensors
59
+ let input = Array::from_shape_vec((1, token_ids.len()), token_ids.clone())?;
60
+ let inputs = inputs! {
61
+ "image" => tensor.view(),
62
+ "token_ids" => input,
63
+ }?;
64
+
65
+ // Run inference
66
+ let outputs = model.run(inputs)?;
67
+
68
+ // Extract logits from output
69
+ let logits = outputs["logits"].try_extract_tensor::<f32>()?;
70
+
71
+ // Get last token logits and find argmax
72
+ let logits_view = logits.view();
73
+ let last_token_logits = logits_view.slice(s![0, -1, ..]);
74
+ let (token_id, _) = last_token_logits
75
+ .iter()
76
+ .enumerate()
77
+ .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
78
+ .unwrap_or((0, &0.0));
79
+
80
+ token_ids.push(token_id as i64);
81
+
82
+ // Break if end token
83
+ if token_id as i64 == 3 {
84
+ break;
85
+ }
86
+ }
87
+
88
+ // decode tokens
89
+ let text = token_ids
90
+ .iter()
91
+ .filter(|&&id| id >= 5)
92
+ .filter_map(|&id| vocab.get(id as usize).cloned())
93
+ .collect::<Vec<_>>();
94
+
95
+ let text = text.join("");
96
+
97
+ println!("Generated text: {}", text);
98
+
99
+ Ok(())
100
+ }
scripts/export_manga_ocr_to_onnx.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import VisionEncoderDecoderModel
3
+
4
+ model = VisionEncoderDecoderModel.from_pretrained("kha-white/manga-ocr-base")
5
+ model.eval()
6
+
7
+ # Dummy input for the model
8
+ dummy_image = torch.randn(1, 3, 224, 224)
9
+ dummy_token_ids = torch.tensor([[2]])
10
+
11
+ # Export the model
12
+ torch.onnx.export(
13
+ model,
14
+ (dummy_image, dummy_token_ids),
15
+ "models/manga-ocr.onnx",
16
+ input_names=["image", "token_ids"],
17
+ output_names=["logits"],
18
+ dynamic_axes={
19
+ "image": {
20
+ 0: "batch_size",
21
+ },
22
+ "token_ids": {
23
+ 0: "batch_size",
24
+ 1: "sequence_length",
25
+ },
26
+ "logits": {
27
+ 0: "batch_size",
28
+ 1: "sequence_length",
29
+ },
30
+ },
31
+ )
scripts/manga_ocr_onnx_inference.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import jaconv
3
+ import numpy as np
4
+
5
+ from onnxruntime import InferenceSession
6
+ from PIL import Image
7
+
8
+
9
+ class MangaOCR:
10
+ def __init__(self, model_path: str, vocab_path: str):
11
+ self.session = InferenceSession(model_path)
12
+ self.vocab = self._load_vocab(vocab_path)
13
+
14
+ def __call__(self, image: Image.Image) -> str:
15
+ image = self._preprocess(image)
16
+ token_ids = self._generate(image)
17
+ text = self._decode(token_ids)
18
+ text = self._postprocess(text)
19
+
20
+ return text
21
+
22
+ def _load_vocab(self, vocab_file: str) -> list[str]:
23
+ with open(vocab_file, "r", encoding="utf8") as f:
24
+ vocab = f.read().splitlines()
25
+
26
+ return vocab
27
+
28
+ def _preprocess(self, image: Image.Image) -> np.ndarray:
29
+ # convert to grayscale
30
+ image = image.convert("L").convert("RGB")
31
+ # resize
32
+ image = image.resize((224, 224), resample=2)
33
+ # rescale
34
+ image = np.array(image, dtype=np.float32)
35
+ image /= 255
36
+ # normalize
37
+ image = (image - 0.5) / 0.5
38
+ # reshape from (224, 224, 3) to (3, 224, 224)
39
+ image = image.transpose((2, 0, 1))
40
+ # add batch size
41
+ image = image[None]
42
+
43
+ return image
44
+
45
+ def _generate(self, image: np.ndarray) -> np.ndarray:
46
+ token_ids = [2]
47
+
48
+ for _ in range(300):
49
+ [logits] = self.session.run(
50
+ output_names=["logits"],
51
+ input_feed={
52
+ "image": image,
53
+ "token_ids": np.array([token_ids]),
54
+ },
55
+ )
56
+
57
+ token_id = logits[0, -1, :].argmax()
58
+ token_ids.append(int(token_id))
59
+
60
+ if token_id == 3:
61
+ break
62
+
63
+ return token_ids
64
+
65
+ def _decode(self, token_ids: list[int]) -> str:
66
+ text = ""
67
+
68
+ for token_id in token_ids:
69
+ if token_id < 5:
70
+ continue
71
+
72
+ text += self.vocab[token_id]
73
+
74
+ return text
75
+
76
+ def _postprocess(self, text: str) -> str:
77
+ text = "".join(text.split())
78
+ text = text.replace("…", "...")
79
+ text = re.sub("[・.]{2,}", lambda x: (x.end() - x.start()) * ".", text)
80
+ text = jaconv.h2z(text, ascii=True, digit=True)
81
+
82
+ return text
83
+
84
+
85
+ if __name__ == "__main__":
86
+ import argparse
87
+
88
+ parser = argparse.ArgumentParser(description="Manga OCR with ONNX Runtime")
89
+ parser.add_argument("--image", type=str, help="Path to the input image")
90
+ parser.add_argument("--model", type=str, help="Path to the ONNX model file")
91
+ parser.add_argument("--vocab", type=str, help="Path to the vocabulary file")
92
+ args = parser.parse_args()
93
+
94
+ ocr = MangaOCR(args.model, args.vocab)
95
+ image = Image.open(args.image)
96
+ text = ocr(image)
97
+ print(text)