Manga Reader: lector web autoalojado con modo libro 3D y estante
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// Detector de rostros de anime, en un Worker para no trabar el estante.
|
||||
// Evalúa en JavaScript la cascada LBP «lbpcascade_animeface» de nagadomi: https://github.com/nagadomi/lbpcascade_animeface
|
||||
// La cascada (animeface-cascade.json) es MIT, (C) 2011 nagadomi@nurs.or.jp; el aviso completo va en el propio archivo.
|
||||
const integral = (gray, w, h) => {
|
||||
const I = new Int32Array((w + 1) * (h + 1))
|
||||
for (let y = 0; y < h; y++) {
|
||||
let row = 0
|
||||
for (let x = 0; x < w; x++) {
|
||||
row += gray[y * w + x]
|
||||
I[(y + 1) * (w + 1) + x + 1] = I[y * (w + 1) + x + 1] + row
|
||||
}
|
||||
}
|
||||
return I
|
||||
}
|
||||
function equalize(gray) {
|
||||
const hist = new Int32Array(256), n = gray.length
|
||||
for (let i = 0; i < n; i++) hist[gray[i]]++
|
||||
let i = 0
|
||||
while (i < 255 && !hist[i]) i++
|
||||
const out = new Uint8Array(n), first = hist[i]
|
||||
if (first === n) return out.fill(i)
|
||||
const scale = 255 / (n - first), lut = new Uint8Array(256)
|
||||
let sum = 0
|
||||
for (i++; i < 256; i++) { sum += hist[i]; lut[i] = Math.max(0, Math.min(255, Math.round(sum * scale))) }
|
||||
for (let k = 0; k < n; k++) out[k] = lut[gray[k]]
|
||||
return out
|
||||
}
|
||||
function shrink(gray, w, h, W, H) { // promedio por área, como INTER_LINEAR al reducir poco
|
||||
const out = new Uint8Array(W * H), fx = w / W, fy = h / H
|
||||
for (let y = 0; y < H; y++) {
|
||||
const sy = (y + 0.5) * fy - 0.5, y0 = Math.max(0, Math.floor(sy)), y1 = Math.min(h - 1, y0 + 1), ty = sy - Math.floor(sy)
|
||||
for (let x = 0; x < W; x++) {
|
||||
const sx = (x + 0.5) * fx - 0.5, x0 = Math.max(0, Math.floor(sx)), x1 = Math.min(w - 1, x0 + 1), tx = sx - Math.floor(sx)
|
||||
const a = gray[y0 * w + x0] * (1 - tx) + gray[y0 * w + x1] * tx, b = gray[y1 * w + x0] * (1 - tx) + gray[y1 * w + x1] * tx
|
||||
out[y * W + x] = a * (1 - ty) + b * ty + 0.5
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
function scan(cascade, gray, W, H, step, emit) {
|
||||
const I = integral(gray, W, H), S = W + 1, { stages, features, w: cw, h: ch } = cascade
|
||||
for (let wy = 0; wy + ch <= H; wy += step) {
|
||||
for (let wx = 0; wx + cw <= W; wx += step) {
|
||||
let pass = true
|
||||
for (let s = 0; s < stages.length && pass; s++) {
|
||||
const [limit, weak] = stages[s]
|
||||
let sum = 0
|
||||
for (let q = 0; q < weak.length; q++) {
|
||||
const [f, subset, leaf] = weak[q], r = features[f], fx = wx + r[0], fy = wy + r[1], bw = r[2], bh = r[3]
|
||||
const a = fy * S + fx, y1 = a + bh * S, y2 = a + 2 * bh * S, y3 = a + 3 * bh * S
|
||||
const p0 = I[a], p1 = I[a + bw], p2 = I[a + 2 * bw], p3 = I[a + 3 * bw]
|
||||
const p4 = I[y1], p5 = I[y1 + bw], p6 = I[y1 + 2 * bw], p7 = I[y1 + 3 * bw]
|
||||
const p8 = I[y2], p9 = I[y2 + bw], p10 = I[y2 + 2 * bw], p11 = I[y2 + 3 * bw]
|
||||
const p12 = I[y3], p13 = I[y3 + bw], p14 = I[y3 + 2 * bw], p15 = I[y3 + 3 * bw]
|
||||
const c = p10 - p9 - p6 + p5
|
||||
const code = ((p5 - p4 - p1 + p0 >= c) << 7) | ((p6 - p5 - p2 + p1 >= c) << 6) | ((p7 - p6 - p3 + p2 >= c) << 5) | ((p11 - p10 - p7 + p6 >= c) << 4) | ((p15 - p14 - p11 + p10 >= c) << 3) | ((p14 - p13 - p10 + p9 >= c) << 2) | ((p13 - p12 - p9 + p8 >= c) << 1) | (p9 - p8 - p5 + p4 >= c)
|
||||
sum += leaf[subset[code >> 5] & (1 << (code & 31)) ? 0 : 1]
|
||||
}
|
||||
if (sum < limit) pass = false
|
||||
}
|
||||
if (pass) emit(wx, wy)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Agrupa ventanas parecidas, como groupRectangles de OpenCV.
|
||||
function group(rects, minNeighbors, eps = 0.2) {
|
||||
const parent = rects.map((_, i) => i), find = i => (parent[i] === i ? i : (parent[i] = find(parent[i])))
|
||||
const close = (a, b) => { const d = eps * 0.5 * (Math.min(a.w, b.w) + Math.min(a.h, b.h)); return Math.abs(a.x - b.x) <= d && Math.abs(a.y - b.y) <= d && Math.abs(a.x + a.w - b.x - b.w) <= d && Math.abs(a.y + a.h - b.y - b.h) <= d }
|
||||
for (let i = 0; i < rects.length; i++) for (let j = 0; j < i; j++) if (close(rects[i], rects[j])) parent[find(i)] = find(j)
|
||||
const by = new Map()
|
||||
rects.forEach((r, i) => { const k = find(i); const g = by.get(k) || { x: 0, y: 0, w: 0, h: 0, n: 0 }; g.x += r.x; g.y += r.y; g.w += r.w; g.h += r.h; g.n++; by.set(k, g) })
|
||||
const out = [...by.values()].filter(g => g.n > minNeighbors).map(g => ({ x: Math.round(g.x / g.n), y: Math.round(g.y / g.n), w: Math.round(g.w / g.n), h: Math.round(g.h / g.n), n: g.n }))
|
||||
// Quita los que quedan dentro de otro mayor con más apoyos.
|
||||
return out.filter(a => !out.some(b => b !== a && b.n >= a.n && a.x >= b.x - b.w * 0.2 && a.y >= b.y - b.h * 0.2 && a.x + a.w <= b.x + b.w * 1.2 && a.y + a.h <= b.y + b.h * 1.2 && b.w > a.w))
|
||||
}
|
||||
/** data: RGBA. Devuelve los rostros en píxeles de la imagen, con `n` = ventanas que los apoyan (más es más fiable). */
|
||||
function detectFaces(cascade, data, w, h, { scale = 1.1, minNeighbors = 5, minSize = 24 } = {}) {
|
||||
const raw = new Uint8Array(w * h)
|
||||
for (let i = 0; i < raw.length; i++) raw[i] = data[i * 4] * 0.299 + data[i * 4 + 1] * 0.587 + data[i * 4 + 2] * 0.114 + 0.5
|
||||
const gray = equalize(raw), rects = []
|
||||
for (let f = 1; ; f *= scale) {
|
||||
const size = Math.round(cascade.w * f)
|
||||
const W = Math.round(w / f), H = Math.round(h / f)
|
||||
if (W < cascade.w || H < cascade.h) break
|
||||
if (size < minSize) continue
|
||||
scan(cascade, f === 1 ? gray : shrink(gray, w, h, W, H), W, H, f > 2 ? 1 : 2, (x, y) => rects.push({ x: Math.round(x * f), y: Math.round(y * f), w: size, h: size }))
|
||||
}
|
||||
return group(rects, minNeighbors)
|
||||
}
|
||||
|
||||
let cascade = null
|
||||
self.onmessage = async ({ data: { id, pixels, w, h } }) => {
|
||||
try {
|
||||
cascade ||= await fetch(`/animeface-cascade.json${self.location.search}`).then(r => { if (!r.ok) throw new Error(r.status); return r.json() })
|
||||
self.postMessage({ id, faces: detectFaces(cascade, new Uint8ClampedArray(pixels), w, h, { minNeighbors: 3 }) })
|
||||
} catch (e) { self.postMessage({ id, faces: null }) }
|
||||
}
|
||||
Reference in New Issue
Block a user