Mayo commited on
feat: split encoder and decoder to reuse encoder outputs for manga-ocr, close #7
Browse files- manga-ocr/src/main.rs +20 -6
- scripts/manga_ocr_onnx_inference.py +22 -9
- src-tauri/src/manga_ocr.rs +27 -9
manga-ocr/src/main.rs
CHANGED
|
@@ -13,7 +13,10 @@ struct Args {
|
|
| 13 |
image: String,
|
| 14 |
|
| 15 |
#[arg(long)]
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
#[arg(long)]
|
| 19 |
vocab: String,
|
|
@@ -22,10 +25,15 @@ struct Args {
|
|
| 22 |
fn main() -> anyhow::Result<()> {
|
| 23 |
let args = Args::parse();
|
| 24 |
|
| 25 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
.with_optimization_level(GraphOptimizationLevel::Level3)?
|
| 27 |
.with_intra_threads(4)?
|
| 28 |
-
.commit_from_file(args.
|
| 29 |
|
| 30 |
let vocab = fs::read_to_string(args.vocab)
|
| 31 |
.map_err(|e| anyhow::anyhow!("Failed to read vocab file: {e}"))?
|
|
@@ -51,6 +59,12 @@ fn main() -> anyhow::Result<()> {
|
|
| 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 |
|
|
@@ -58,12 +72,12 @@ fn main() -> anyhow::Result<()> {
|
|
| 58 |
// Create input tensors
|
| 59 |
let input = Array::from_shape_vec((1, token_ids.len()), token_ids.clone())?;
|
| 60 |
let inputs = inputs! {
|
| 61 |
-
"
|
| 62 |
-
"
|
| 63 |
}?;
|
| 64 |
|
| 65 |
// Run inference
|
| 66 |
-
let outputs =
|
| 67 |
|
| 68 |
// Extract logits from output
|
| 69 |
let logits = outputs["logits"].try_extract_tensor::<f32>()?;
|
|
|
|
| 13 |
image: String,
|
| 14 |
|
| 15 |
#[arg(long)]
|
| 16 |
+
encoder_model: String,
|
| 17 |
+
|
| 18 |
+
#[arg(long)]
|
| 19 |
+
decoder_model: String,
|
| 20 |
|
| 21 |
#[arg(long)]
|
| 22 |
vocab: String,
|
|
|
|
| 25 |
fn main() -> anyhow::Result<()> {
|
| 26 |
let args = Args::parse();
|
| 27 |
|
| 28 |
+
let encoder_model = Session::builder()?
|
| 29 |
+
.with_optimization_level(GraphOptimizationLevel::Level3)?
|
| 30 |
+
.with_intra_threads(4)?
|
| 31 |
+
.commit_from_file(args.encoder_model)?;
|
| 32 |
+
|
| 33 |
+
let decoder_model = Session::builder()?
|
| 34 |
.with_optimization_level(GraphOptimizationLevel::Level3)?
|
| 35 |
.with_intra_threads(4)?
|
| 36 |
+
.commit_from_file(args.decoder_model)?;
|
| 37 |
|
| 38 |
let vocab = fs::read_to_string(args.vocab)
|
| 39 |
.map_err(|e| anyhow::anyhow!("Failed to read vocab file: {e}"))?
|
|
|
|
| 59 |
tensor[[0, 2, y, x]] = (pixel[2] as f32 / 255.0 - 0.5) / 0.5;
|
| 60 |
}
|
| 61 |
|
| 62 |
+
let inputs = inputs! {
|
| 63 |
+
"pixel_values" => tensor.view(),
|
| 64 |
+
}?;
|
| 65 |
+
let outputs = encoder_model.run(inputs)?;
|
| 66 |
+
let encoder_hidden_states = outputs[0].try_extract_tensor::<f32>()?;
|
| 67 |
+
|
| 68 |
// generate
|
| 69 |
let mut token_ids: Vec<i64> = vec![2i64]; // Start token
|
| 70 |
|
|
|
|
| 72 |
// Create input tensors
|
| 73 |
let input = Array::from_shape_vec((1, token_ids.len()), token_ids.clone())?;
|
| 74 |
let inputs = inputs! {
|
| 75 |
+
"encoder_hidden_states" => encoder_hidden_states.view(),
|
| 76 |
+
"input_ids" => input,
|
| 77 |
}?;
|
| 78 |
|
| 79 |
// Run inference
|
| 80 |
+
let outputs = decoder_model.run(inputs)?;
|
| 81 |
|
| 82 |
// Extract logits from output
|
| 83 |
let logits = outputs["logits"].try_extract_tensor::<f32>()?;
|
scripts/manga_ocr_onnx_inference.py
CHANGED
|
@@ -1,19 +1,27 @@
|
|
| 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,
|
| 11 |
-
self.
|
|
|
|
| 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 |
|
|
@@ -43,14 +51,18 @@ class MangaOCR:
|
|
| 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.
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
"
|
| 53 |
-
"
|
| 54 |
},
|
| 55 |
)
|
| 56 |
|
|
@@ -87,11 +99,12 @@ if __name__ == "__main__":
|
|
| 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.
|
| 95 |
image = Image.open(args.image)
|
| 96 |
text = ocr(image)
|
| 97 |
print(text)
|
|
|
|
| 1 |
import re
|
| 2 |
import jaconv
|
| 3 |
import numpy as np
|
| 4 |
+
import time
|
| 5 |
|
| 6 |
from onnxruntime import InferenceSession
|
| 7 |
from PIL import Image
|
| 8 |
|
| 9 |
|
| 10 |
class MangaOCR:
|
| 11 |
+
def __init__(self, encoder_model_path: str, decoder_model_path: str, vocab_path: str):
|
| 12 |
+
self.encoder_session = InferenceSession(encoder_model_path)
|
| 13 |
+
self.decoder_session = InferenceSession(decoder_model_path)
|
| 14 |
self.vocab = self._load_vocab(vocab_path)
|
| 15 |
|
| 16 |
def __call__(self, image: Image.Image) -> str:
|
| 17 |
image = self._preprocess(image)
|
| 18 |
+
|
| 19 |
+
# count time
|
| 20 |
+
start = time.time()
|
| 21 |
token_ids = self._generate(image)
|
| 22 |
+
end = time.time()
|
| 23 |
+
print(f"Time taken: {end - start:.2f} seconds")
|
| 24 |
+
|
| 25 |
text = self._decode(token_ids)
|
| 26 |
text = self._postprocess(text)
|
| 27 |
|
|
|
|
| 51 |
return image
|
| 52 |
|
| 53 |
def _generate(self, image: np.ndarray) -> np.ndarray:
|
| 54 |
+
encoder_hidden_states = self.encoder_session.run(None, {
|
| 55 |
+
"pixel_values": image,
|
| 56 |
+
})[0]
|
| 57 |
+
|
| 58 |
token_ids = [2]
|
| 59 |
|
| 60 |
for _ in range(300):
|
| 61 |
+
[logits] = self.decoder_session.run(
|
| 62 |
+
None,
|
| 63 |
+
{
|
| 64 |
+
"encoder_hidden_states": encoder_hidden_states,
|
| 65 |
+
"input_ids": np.array([token_ids], dtype=np.int64),
|
| 66 |
},
|
| 67 |
)
|
| 68 |
|
|
|
|
| 99 |
|
| 100 |
parser = argparse.ArgumentParser(description="Manga OCR with ONNX Runtime")
|
| 101 |
parser.add_argument("--image", type=str, help="Path to the input image")
|
| 102 |
+
parser.add_argument("--encoder-model", type=str, help="Path to the ONNX model file")
|
| 103 |
+
parser.add_argument("--decoder-model", type=str, help="Path to the ONNX model file")
|
| 104 |
parser.add_argument("--vocab", type=str, help="Path to the vocabulary file")
|
| 105 |
args = parser.parse_args()
|
| 106 |
|
| 107 |
+
ocr = MangaOCR(args.encoder_model, args.decoder_model, args.vocab)
|
| 108 |
image = Image.open(args.image)
|
| 109 |
text = ocr(image)
|
| 110 |
print(text)
|
src-tauri/src/manga_ocr.rs
CHANGED
|
@@ -6,21 +6,28 @@ use ort::{inputs, session::Session};
|
|
| 6 |
|
| 7 |
#[derive(Debug)]
|
| 8 |
pub struct MangaOCR {
|
| 9 |
-
|
|
|
|
| 10 |
vocab: Vec<String>,
|
| 11 |
}
|
| 12 |
|
| 13 |
impl MangaOCR {
|
| 14 |
pub fn new() -> anyhow::Result<Self> {
|
| 15 |
let api = Api::new()?;
|
| 16 |
-
let repo = api.model("mayocream/
|
| 17 |
-
let
|
|
|
|
| 18 |
let vocab_path = repo.get("vocab.txt")?;
|
| 19 |
|
| 20 |
-
let
|
| 21 |
.with_optimization_level(ort::session::builder::GraphOptimizationLevel::Level3)?
|
| 22 |
.with_intra_threads(thread::available_parallelism()?.get())?
|
| 23 |
-
.commit_from_file(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
let vocab = std::fs::read_to_string(vocab_path)
|
| 26 |
.map_err(|e| anyhow::anyhow!("Failed to read vocab file: {e}"))?
|
|
@@ -28,7 +35,11 @@ impl MangaOCR {
|
|
| 28 |
.map(|s| s.to_string())
|
| 29 |
.collect::<Vec<_>>();
|
| 30 |
|
| 31 |
-
Ok(Self {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
pub fn inference(&self, image: &image::DynamicImage) -> anyhow::Result<String> {
|
|
@@ -48,6 +59,13 @@ impl MangaOCR {
|
|
| 48 |
tensor[[0, 2, y, x]] = (pixel[2] as f32 / 255.0 - 0.5) / 0.5;
|
| 49 |
}
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
// generate
|
| 52 |
let mut token_ids: Vec<i64> = vec![2i64]; // Start token
|
| 53 |
|
|
@@ -55,12 +73,12 @@ impl MangaOCR {
|
|
| 55 |
// Create input tensors
|
| 56 |
let input = ndarray::Array::from_shape_vec((1, token_ids.len()), token_ids.clone())?;
|
| 57 |
let inputs = inputs! {
|
| 58 |
-
"
|
| 59 |
-
"
|
| 60 |
}?;
|
| 61 |
|
| 62 |
// Run inference
|
| 63 |
-
let outputs = self.
|
| 64 |
|
| 65 |
// Extract logits from output
|
| 66 |
let logits = outputs["logits"].try_extract_tensor::<f32>()?;
|
|
|
|
| 6 |
|
| 7 |
#[derive(Debug)]
|
| 8 |
pub struct MangaOCR {
|
| 9 |
+
encoder_model: Session,
|
| 10 |
+
decoder_model: Session,
|
| 11 |
vocab: Vec<String>,
|
| 12 |
}
|
| 13 |
|
| 14 |
impl MangaOCR {
|
| 15 |
pub fn new() -> anyhow::Result<Self> {
|
| 16 |
let api = Api::new()?;
|
| 17 |
+
let repo = api.model("mayocream/manga-ocr-onnx".to_string());
|
| 18 |
+
let encoder_model_path = repo.get("encoder_model.onnx")?;
|
| 19 |
+
let decoder_model_path = repo.get("decoder_model.onnx")?;
|
| 20 |
let vocab_path = repo.get("vocab.txt")?;
|
| 21 |
|
| 22 |
+
let encoder_model = Session::builder()?
|
| 23 |
.with_optimization_level(ort::session::builder::GraphOptimizationLevel::Level3)?
|
| 24 |
.with_intra_threads(thread::available_parallelism()?.get())?
|
| 25 |
+
.commit_from_file(encoder_model_path)?;
|
| 26 |
+
|
| 27 |
+
let decoder_model = Session::builder()?
|
| 28 |
+
.with_optimization_level(ort::session::builder::GraphOptimizationLevel::Level3)?
|
| 29 |
+
.with_intra_threads(thread::available_parallelism()?.get())?
|
| 30 |
+
.commit_from_file(decoder_model_path)?;
|
| 31 |
|
| 32 |
let vocab = std::fs::read_to_string(vocab_path)
|
| 33 |
.map_err(|e| anyhow::anyhow!("Failed to read vocab file: {e}"))?
|
|
|
|
| 35 |
.map(|s| s.to_string())
|
| 36 |
.collect::<Vec<_>>();
|
| 37 |
|
| 38 |
+
Ok(Self {
|
| 39 |
+
encoder_model,
|
| 40 |
+
decoder_model,
|
| 41 |
+
vocab,
|
| 42 |
+
})
|
| 43 |
}
|
| 44 |
|
| 45 |
pub fn inference(&self, image: &image::DynamicImage) -> anyhow::Result<String> {
|
|
|
|
| 59 |
tensor[[0, 2, y, x]] = (pixel[2] as f32 / 255.0 - 0.5) / 0.5;
|
| 60 |
}
|
| 61 |
|
| 62 |
+
// save encoder hidden state
|
| 63 |
+
let inputs = inputs! {
|
| 64 |
+
"pixel_values" => tensor.view(),
|
| 65 |
+
}?;
|
| 66 |
+
let outputs = self.encoder_model.run(inputs)?;
|
| 67 |
+
let encoder_hidden_state = outputs[0].try_extract_tensor::<f32>()?;
|
| 68 |
+
|
| 69 |
// generate
|
| 70 |
let mut token_ids: Vec<i64> = vec![2i64]; // Start token
|
| 71 |
|
|
|
|
| 73 |
// Create input tensors
|
| 74 |
let input = ndarray::Array::from_shape_vec((1, token_ids.len()), token_ids.clone())?;
|
| 75 |
let inputs = inputs! {
|
| 76 |
+
"encoder_hidden_states" => encoder_hidden_state.view(),
|
| 77 |
+
"input_ids" => input,
|
| 78 |
}?;
|
| 79 |
|
| 80 |
// Run inference
|
| 81 |
+
let outputs = self.decoder_model.run(inputs)?;
|
| 82 |
|
| 83 |
// Extract logits from output
|
| 84 |
let logits = outputs["logits"].try_extract_tensor::<f32>()?;
|