Downloads · 30 days
0
Kellenok/anglenet
anglenet is a machine learning model from Kellenok. Use it for the machine learning task on the model card, and read the license before you ship it in a product. The card lists the license as apache-2.0.
AngleNet predicts the reading orientation and rotation angle (0° to 180°) for manga text boxes. It is a lightweight post-processing model (+0.22 ms per box / 1.0 MB) for meiki.text.detect.v0. It converts straight boxe…
Downloads · 30 days
0
Access
Public
Updated Aug 26, 2026
Repo size
32.2 MB
Likes
1
Public
Click a slice to open those files.
.onnx32.2 MB · 100%
From the Hugging Face model README
AngleNet predicts the reading orientation and rotation angle (0° to 180°) for manga text boxes. It is a lightweight post-processing model (+0.22 ms per box / 1.0 MB) for meiki.text.detect.v0. It converts straight boxes into oriented polygons and keeps the 95.66% detector precision.
Native meiki recognition has high error on vertical manga text (~23% end-to-end CER). Adding AngleNet directly to meiki recognition does not fix this limit. This pipeline uses PP-OCRv6 Small Rec instead. Unrotated boxes still fail on tilted text (58.4% CER). AngleNet fixes this problem. It reduces median slant error from 9.15° to 2.87° and lowers tilted dialogue CER from 58.4% to 11.4%. The full pipeline reaches 12.8% end-to-end CER (8.9% matched CER, 86.54% Tilt IoU >= 0.5).
Evaluated on 300 unseen manga and webtoon test pages (2,631 dialogue lines) with a shared PP-OCRv6 Small recognizer (23.0 MB FP16) to isolate the exact impact of bounding box detection and reading orientation on recognition accuracy.
Bold = Best, <u>Underline</u> = 2nd place.
| Detection pipeline | Footprint (FP16) | Detection latency * | Recall | Clean precision | Tilted CER | Matched CER | End-to-end CER |
|---|---|---|---|---|---|---|---|
| meiki.text.detect.v0 + AngleNet v0.1 | 23.5 MB (22.5 + 1.0) | <u>233.9 ms</u> (+5.8 ms overhead) ⚡ | <u>96.35%</u> | 95.66% | 11.4% | 8.9% | <u>12.8%</u> |
| meiki.text.detect.v0 (Base AABB, No AngleNet) | 22.5 MB | 228.1 ms | <u>96.35%</u> | 95.66% | 58.4% (+47.0%) | <u>9.8%</u> | 13.4% |
| PP-OCRv6 DBNet Medium | 49.0 MB | 1850.0 ms | 96.95% | <u>92.40%</u> | <u>16.8%</u> | 9.9% | 12.4% |
| PP-OCRv6 DBNet Small | <u>15.2 MB</u> | 580.0 ms | 96.20% | 91.20% | 19.4% | 10.3% | 13.0% |
| PP-OCRv6 DBNet Tiny | 8.4 MB | 310.0 ms | 95.20% | 91.80% | 24.5% | 10.8% | 13.7% |
Bold = Best, <u>Underline</u> = 2nd place. Sorted by Tilt IoU >= 0.5.
| Model / Pipeline | Params | FP32 size | FP16 size | Detection latency * | Clean precision | GT-fit | Median slant error | Tilt IoU >= 0.5 |
|---|---|---|---|---|---|---|---|---|
| PP-OCRv6 medium (DBNet 960px) | 24.5M | 98.0 MB | 49.0 MB | 1850.0 ms | 92.40% | 96.95 | 1.82° | 88.46% |
| Meiki + AngleNet v0.1 pro (128x128) | 11.2M + 3.8M | 45.0 + 14.4 MB | 22.5 + 7.3 MB | 298.7 ms (+70.6 ms Teacher) | 95.55% | <u>95.40</u> | 2.58° | <u>86.54%</u> |
| Meiki + AngleNet v0.1 distill (96x96) | 11.2M + 516K | 45.0 + 2.0 MB | 22.5 + 1.0 MB | 239.1 ms (+11.0 ms) | 95.66% | 95.15 | 3.11° | <u>86.54%</u> |
| Meiki + AngleNet v0.1 base (64x64) | 11.2M + 516K | 45.0 + 2.0 MB | 22.5 + 1.0 MB | 234.0 ms (+5.9 ms) | <u>95.62%</u> | 94.91 | 3.56° | <u>86.54%</u> |
| PP-OCRv6 small (DBNet 960px) | 7.6M | 30.4 MB | 15.2 MB | 580.0 ms | 91.20% | 95.18 | <u>2.15°</u> | 84.62% |
| Meiki + AngleNet v0.1 distill (64x64) | 11.2M + 516K | 45.0 + 2.0 MB | 22.5 + 1.0 MB | <u>233.9 ms</u> (+5.8 ms) ⚡ | 95.52% | 95.24 | 2.87° | 82.69% |
| PP-OCRv6 tiny_base (DBNet 960px) | 4.2M | 16.8 MB | 8.4 MB | 310.0 ms | 89.90% | 94.74 | 2.45° | 80.77% |
| Yomitoku DBNet v2.1 (960px) | 11.2M | 44.8 MB | 22.4 MB | 2183.3 ms | 88.50% | 91.28 | 3.20° | 76.92% |
| ScreenAI | - | - | - | 450.0 ms | 86.40% | 91.02 | 3.40° | 75.00% |
| Google Lens | - | - | - | - | 84.20% | 89.14 | 4.10° | 69.23% |
| Meiki (conf=0.44, No AngleNet) | 11.2M | 45.0 MB | 22.5 MB | 228.1 ms | 95.66% | 90.69 | 9.15° | 32.69% |
* Measured per page on strictly 1 single CPU thread (single-core edge/mobile/WASM baseline, ONNX Runtime).
v0.1 base, v0.1 distill):
v0.1 pro):
The model was trained on 345,265 samples:
import cv2, math, numpy as np, onnxruntime as ort
from huggingface_hub import hf_hub_download
# Load models (auto-download from Hugging Face)
meiki_path = hf_hub_download(repo_id="rtr46/meiki.text.detect.v0", filename="meiki.text.detect.small.v0.onnx")
angle_path = hf_hub_download(repo_id="Kellenok/anglenet", filename="anglenet_v0_1_distill_64x64.onnx")
meiki_sess = ort.InferenceSession(meiki_path)
angle_sess = ort.InferenceSession(angle_path)
def letterbox_crop(crop_gray, target_size=64):
h, w = crop_gray.shape[:2]
scale = min(target_size / max(1, w), target_size / max(1, h))
nw, nh = max(1, int(w * scale)), max(1, int(h * scale))
resized = cv2.resize(crop_gray, (nw, nh), interpolation=cv2.INTER_AREA)
pad = np.zeros((target_size, target_size), dtype=np.uint8)
pad[(target_size - nh) // 2 : (target_size - nh) // 2 + nh,
(target_size - nw) // 2 : (target_size - nw) // 2 + nw] = resized
return (pad.astype(np.float32) / 255.0)[None, None, :, :]
def get_rotated_polygon(x1, y1, x2, y2, rot_deg, width_scale=1.05):
cx, cy = (x1 + x2) / 2.0, (y1 + y2) / 2.0
w, h = (x2 - x1) * width_scale, (y2 - y1)
rad = math.radians(rot_deg)
cos_a, sin_a = math.cos(rad), math.sin(rad)
hw, hh = w / 2.0, h / 2.0
corners = [(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)]
return [[round(cx + dx*cos_a - dy*sin_a, 1), round(cy + dx*sin_a + dy*cos_a, 1)] for dx, dy in corners]
def detect_and_orient(image_bgr, conf_thresh=0.44):
H, W = image_bgr.shape[:2]
img_gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# 1. Run Meiki Detector (640x640)
img_640 = cv2.resize(image_bgr, (640, 640)).transpose(2, 0, 1)[None].astype(np.float32) / 255.0
orig_sz = np.array([[H, W]], dtype=np.int64)
_, boxes, scores = meiki_sess.run(None, {"images": img_640, "orig_target_sizes": orig_sz})
# 2. Filter boxes by confidence
valid = scores[0] >= conf_thresh
boxes = boxes[0][valid]
# 3. Predict angles with AngleNet
polygons = []
for (x1, y1, x2, y2) in boxes:
w, h = x2 - x1, y2 - y1
if w < 4 or h < 4:
continue
# 10% context margin around crop
pw, ph = int(w * 0.10), int(h * 0.10)
crop = img_gray[max(0, int(y1 - ph)):min(H, int(y2 + ph)), max(0, int(x1 - pw)):min(W, int(x2 + pw))]
# Infer AngleNet (0.11 ms on CPU)
input_tensor = letterbox_crop(crop, target_size=64)
csl_logits, tilt_logit = angle_sess.run(None, {"input": input_tensor})
# CSL continuous trigonometric decoding
probs = np.exp(csl_logits[0] - np.max(csl_logits[0]))
probs /= np.sum(probs)
bins = np.arange(180, dtype=np.float32)
pred_deg = (0.5 * np.degrees(np.arctan2(
np.sum(probs * np.sin(np.radians(2.0 * bins))),
np.sum(probs * np.cos(np.radians(2.0 * bins)))
))) % 180.0
# Compute rotation delta
is_vert = (h >= w)
rot_delta = (pred_deg - 90.0) if is_vert else (pred_deg if pred_deg <= 90.0 else pred_deg - 180.0)
is_tilted = (abs(rot_delta) >= 0.5) and ((max(w, h) / max(1.0, min(w, h))) >= 1.1)
poly = get_rotated_polygon(x1, y1, x2, y2, rot_delta, width_scale=1.05) if is_tilted else [[x1, y1], [x2, y1], [x2, y2], [x1, y2]]
polygons.append(poly)
return polygons