Initial Commit
This commit is contained in:
122
packages/media/image/emblem-solver/src/solver/greedy/greedy.ts
Normal file
122
packages/media/image/emblem-solver/src/solver/greedy/greedy.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { pack, unpackX, unpackY } from "../../helpers/helpers.js";
|
||||
import type { Rect } from "../../types/types.js";
|
||||
|
||||
/**
|
||||
* Greedy set-cover with a deterministic three-key score:
|
||||
*
|
||||
* 1. number of uncovered required pixels covered (primary)
|
||||
* 2. covered / area ratio (efficiency — prefer compact rects)
|
||||
* 3. proximity of rect centroid to the centroid of the remaining required
|
||||
* pixels (tiebreaker — keep selections clustered)
|
||||
*
|
||||
* When no candidate rect intersects the remaining uncovered set, fall back
|
||||
* to emitting 1×1 rects for every residual pixel. This guarantees the cover
|
||||
* is always complete even on pathological inputs.
|
||||
*/
|
||||
export function enhancedGreedyCover(
|
||||
candidates: ReadonlyArray<Rect>,
|
||||
required: boolean[][],
|
||||
width: number,
|
||||
height: number
|
||||
): Rect[] {
|
||||
const uncovered = new Set<number>();
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (required[y]?.[x]) uncovered.add(pack(x, y, width));
|
||||
}
|
||||
}
|
||||
if (uncovered.size === 0) return [];
|
||||
|
||||
const selected: Rect[] = [];
|
||||
const maxDist = Math.sqrt(width ** 2 + height ** 2);
|
||||
|
||||
while (uncovered.size > 0) {
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
for (const idx of uncovered) {
|
||||
cx += unpackX(idx, width);
|
||||
cy += unpackY(idx, width);
|
||||
}
|
||||
cx /= uncovered.size;
|
||||
cy /= uncovered.size;
|
||||
|
||||
let bestRect: Rect | null = null;
|
||||
let bestScore = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const rect of candidates) {
|
||||
let covered = 0;
|
||||
for (let ry = rect.y; ry < rect.y + rect.h; ry++) {
|
||||
for (let rx = rect.x; rx < rect.x + rect.w; rx++) {
|
||||
if (uncovered.has(pack(rx, ry, width))) covered++;
|
||||
}
|
||||
}
|
||||
if (covered === 0) continue;
|
||||
const area = rect.w * rect.h;
|
||||
const efficiency = covered / area;
|
||||
const rcx = rect.x + rect.w / 2;
|
||||
const rcy = rect.y + rect.h / 2;
|
||||
const dist = Math.sqrt((rcx - cx) ** 2 + (rcy - cy) ** 2);
|
||||
const proximity = 1 - dist / maxDist;
|
||||
const score = covered * 1000 + efficiency * 100 + proximity * 10;
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestRect = rect;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bestRect) {
|
||||
for (const idx of uncovered) {
|
||||
selected.push({
|
||||
x: unpackX(idx, width),
|
||||
y: unpackY(idx, width),
|
||||
w: 1,
|
||||
h: 1,
|
||||
});
|
||||
}
|
||||
uncovered.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
selected.push(bestRect);
|
||||
for (let ry = bestRect.y; ry < bestRect.y + bestRect.h; ry++) {
|
||||
for (let rx = bestRect.x; rx < bestRect.x + bestRect.w; rx++) {
|
||||
uncovered.delete(pack(rx, ry, width));
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-island independent cover: build a local `required` mask for each
|
||||
* island and run `enhancedGreedyCover` against the subset of candidates
|
||||
* that overlap it. This avoids wasting selection budget on spanning rects
|
||||
* between disconnected islands.
|
||||
*/
|
||||
export function perIslandCover(
|
||||
islands: ReadonlyArray<{ readonly pixels: Set<number> }>,
|
||||
candidates: ReadonlyArray<Rect>,
|
||||
width: number,
|
||||
height: number
|
||||
): Rect[] {
|
||||
const out: Rect[] = [];
|
||||
for (const island of islands) {
|
||||
const local: boolean[][] = Array.from({ length: height }, () =>
|
||||
Array(width).fill(false)
|
||||
);
|
||||
for (const idx of island.pixels) {
|
||||
(local[unpackY(idx, width)] as boolean[])[unpackX(idx, width)] =
|
||||
true;
|
||||
}
|
||||
const localCandidates = candidates.filter((r) => {
|
||||
for (let ry = r.y; ry < r.y + r.h; ry++) {
|
||||
for (let rx = r.x; rx < r.x + r.w; rx++) {
|
||||
if (local[ry]?.[rx]) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
out.push(...enhancedGreedyCover(localCandidates, local, width, height));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user