Files
foliate-js/panel-detection/grid.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

128 lines
3.3 KiB
JavaScript

// panel-detection/grid.js
// Grid-based panel detection (lightweight fallback)
export function detectPanelsGrid(imageData, rows = 3, cols = 3) {
const panels = [];
const cellWidth = imageData.width / cols;
const cellHeight = imageData.height / rows;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const startX = Math.floor(x * cellWidth);
const startY = Math.floor(y * cellHeight);
const cellData = extractCell(
imageData,
startX,
startY,
cellWidth,
cellHeight,
);
if (!isEmpty(cellData)) {
panels.push({
id: `grid-${panels.length}`,
x: (x / cols) * 100,
y: (y / rows) * 100,
width: (1 / cols) * 100,
height: (1 / rows) * 100,
reading_order: panels.length,
});
}
}
}
return mergeAdjacentPanels(panels);
}
function extractCell(imageData, startX, startY, width, height) {
const w = Math.floor(width);
const h = Math.floor(height);
const cellData = new Uint8ClampedArray(w * h * 4);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const srcIdx = ((startY + y) * imageData.width + (startX + x)) * 4;
const destIdx = (y * w + x) * 4;
cellData[destIdx] = imageData.data[srcIdx];
cellData[destIdx + 1] = imageData.data[srcIdx + 1];
cellData[destIdx + 2] = imageData.data[srcIdx + 2];
cellData[destIdx + 3] = imageData.data[srcIdx + 3];
}
}
return { data: cellData, width: w, height: h };
}
function isEmpty(cellData) {
let emptyPixels = 0;
const totalPixels = cellData.width * cellData.height;
for (let i = 3; i < cellData.data.length; i += 4) {
if (cellData.data[i] < 10) emptyPixels++;
}
return emptyPixels / totalPixels > 0.95;
}
function mergeAdjacentPanels(panels) {
const merged = [];
const used = new Set();
for (let i = 0; i < panels.length; i++) {
if (used.has(i)) continue;
let current = { ...panels[i] };
used.add(i);
let changed = true;
while (changed) {
changed = false;
for (let j = i + 1; j < panels.length; j++) {
if (used.has(j)) continue;
if (isAdjacent(current, panels[j])) {
current = mergePanels(current, panels[j]);
used.add(j);
changed = true;
}
}
}
merged.push(current);
}
return merged;
}
function isAdjacent(p1, p2) {
const tolerance = 5;
if (
Math.abs(p1.y - p2.y) < tolerance &&
Math.abs(p1.height - p2.height) < tolerance
) {
return (
Math.abs(p1.x + p1.width - p2.x) < tolerance ||
Math.abs(p2.x + p2.width - p1.x) < tolerance
);
}
if (
Math.abs(p1.x - p2.x) < tolerance &&
Math.abs(p1.width - p2.width) < tolerance
) {
return (
Math.abs(p1.y + p1.height - p2.y) < tolerance ||
Math.abs(p2.y + p2.height - p1.y) < tolerance
);
}
return false;
}
function mergePanels(p1, p2) {
const minX = Math.min(p1.x, p2.x);
const minY = Math.min(p1.y, p2.y);
const maxX = Math.max(p1.x + p1.width, p2.x + p2.width);
const maxY = Math.max(p1.y + p1.height, p2.y + p2.height);
return {
id: p1.id,
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
reading_order: Math.min(p1.reading_order, p2.reading_order),
};
}