Files
foliate-js/panel-detection/detector.js
T
john-okeefe ffaceaf962 Add panel detection module with multi-tier fallback system
Implement a comprehensive panel detection system for manga and comics
with automatic fallback chain for maximum compatibility.

Core detector (detector.js):
- PanelDetector class with in-memory caching
- Lazy-loading of OpenCV and TensorFlow.js
- Validation logic to filter poor detections
- Cache management to avoid re-detection

OpenCV edge detection (opencv.js):
- Canny edge detection for panel boundaries
- Contour finding with bounding box extraction
- Size and aspect ratio filtering
- Reading order sorting (top-to-bottom, left-to-right)

ML-based detection (coco-ssd.js):
- COCO-SSD pre-trained model integration
- Object detection for irregular panel layouts
- Rectangular filtering for panel-like regions
- Handles edge cases where edge detection fails

Grid-based fallback (grid.js):
- Lightweight 3x3 grid detection
- Empty cell detection via alpha channel analysis
- Adjacent panel merging algorithm
- Always works as final fallback

The detection pipeline tries OpenCV first (fast, accurate),
falls back to ML detection if validation fails,
and uses grid detection as ultimate baseline.
2026-04-13 16:43:25 -04:00

123 lines
3.4 KiB
JavaScript

// panel-detection/detector.js
// Main panel detector with lazy-loaded fallback chain
export class PanelDetector {
#opencv = null;
#model = null;
#cache = new Map();
async detectPanels(doc, index, force = false) {
const cacheKey = `${doc.location?.pathname || ""}-${index}`;
if (!force && this.#cache.has(cacheKey)) {
return this.#cache.get(cacheKey);
}
const imageData = this.#extractImageData(doc);
if (!imageData) {
return { panels: [], method: "no-image", confidence: 0 };
}
const result = await this.#runDetectionPipeline(imageData);
this.#cache.set(cacheKey, result);
return result;
}
#extractImageData(doc) {
const img = doc.querySelector("img") || doc.querySelector("canvas");
if (!img) return null;
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth || img.width;
canvas.height = img.naturalHeight || img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, canvas.width, canvas.height);
}
async #runDetectionPipeline(imageData) {
const { detectPanelsOpenCV } = await import("./opencv.js");
const { detectPanelsML } = await import("./coco-ssd.js");
const { detectPanelsGrid } = await import("./grid.js");
if (!this.#opencv) {
try {
this.#opencv = await this.#loadOpenCV();
} catch (e) {
console.warn("Failed to load OpenCV:", e);
}
}
if (this.#opencv) {
try {
const panels = await detectPanelsOpenCV(imageData, this.#opencv);
if (this.#validatePanels(panels, imageData)) {
return { panels, method: "opencv", confidence: 0.85 };
}
} catch (e) {
console.warn("OpenCV detection failed:", e);
}
}
if (!this.#model) {
try {
this.#model = await this.#loadModel();
} catch (e) {
console.warn("Failed to load ML model:", e);
}
}
if (this.#model) {
try {
const panels = await detectPanelsML(imageData, this.#model);
if (this.#validatePanels(panels, imageData)) {
return { panels, method: "ml", confidence: 0.7 };
}
} catch (e) {
console.warn("ML detection failed:", e);
}
}
const panels = detectPanelsGrid(imageData);
return { panels, method: "grid", confidence: 0.4 };
}
#validatePanels(panels, imageData) {
if (!panels || panels.length === 0) return false;
if (panels.length > 30) return false;
const imgArea = imageData.width * imageData.height;
let totalPanelArea = 0;
for (const panel of panels) {
const panelArea = ((panel.width * panel.height) / 10000) * imgArea;
totalPanelArea += panelArea;
}
const coverage = totalPanelArea / imgArea;
return coverage > 0.1 && coverage < 0.95;
}
async #loadOpenCV() {
const { default: cv } = await import("@techstark/opencv-js");
await new Promise((resolve, reject) => {
const check = () => {
if (cv && cv.Mat) resolve();
else if (!cv || cv.readyState === "complete")
reject(new Error("OpenCV failed to load"));
else setTimeout(check, 50);
};
check();
});
return cv;
}
async #loadModel() {
const tf = await import("@tensorflow/tfjs");
const cocoSsd = await import("@tensorflow-models/coco-ssd");
return await cocoSsd.load({ base: "lite_mobilenet_v2" });
}
clear() {
this.#cache.clear();
}
}