mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Update PanelDetector to load OpenCV, TensorFlow.js, and COCO-SSD from local vendor directory instead of importing npm packages. This ensures the libraries are available after build and avoids runtime dependency resolution issues. - Change imports to use ../vendor paths instead of npm packages - Add caching to prevent reloading libraries - Add error handling for failed library loads with console warnings - Update OpenCV loading to handle both default and named exports
140 lines
3.8 KiB
JavaScript
140 lines
3.8 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() {
|
|
if (this.#opencv) return this.#opencv;
|
|
|
|
try {
|
|
const cv = await import("../vendor/opencv/opencv.js");
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const check = () => {
|
|
if (cv && cv.Mat) resolve();
|
|
else if (cv.readyState === "complete")
|
|
reject(new Error("OpenCV failed to load"));
|
|
else setTimeout(check, 50);
|
|
};
|
|
check();
|
|
});
|
|
|
|
this.#opencv = cv.default || cv;
|
|
return this.#opencv;
|
|
} catch (e) {
|
|
console.warn("Failed to load OpenCV:", e);
|
|
return null;
|
|
}
|
|
}
|
|
async #loadModel() {
|
|
if (this.#model) return this.#model;
|
|
|
|
try {
|
|
await import("../vendor/tfjs/tf.min.js");
|
|
const cocoSsd = await import("../vendor/coco-ssd/coco-ssd.min.js");
|
|
this.#model = await cocoSsd.load({ base: "lite_mobilenet_v2" });
|
|
return this.#model;
|
|
} catch (e) {
|
|
console.warn("Failed to load ML model:", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.#cache.clear();
|
|
}
|
|
}
|