Last active
March 10, 2025 05:43
-
-
Save yongjun21/567dc9d60b3994587f0109e85537f24c to your computer and use it in GitHub Desktop.
Some helpers functions for manipulating bitmask in sparse (run-based) format
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SparseBitmask, { | |
unionRunEnds, | |
intersectionRunEnds, | |
subtractionRunEnds, | |
} from "./SparseBitmask"; | |
import { | |
cutAndPasteBitmask3d, | |
getBoundingBox3d, | |
getCentroid3d, | |
getWindowedRunLengths3d, | |
resizeBitmask3d, | |
switchPrimaryAxis, | |
} from "./bitmaskHelpers3d"; | |
import { OrderedMap } from "./OrderedCollections"; | |
import { memoized, numberHasher } from "./memo"; | |
import { range } from "./base"; | |
interface TransformedToSparse { | |
toOutline(): SparseBitmask; | |
toFill(): SparseBitmask; | |
} | |
const memoizedDrawLine = memoized(drawLine, numberHasher(4), 500); | |
export function transformPolygonToSparse( | |
polygon: [number, number][][], | |
width: number, | |
height: number | |
): TransformedToSparse { | |
const bbox: [number, number, number, number] = [0, 0, width - 1, height - 1]; | |
const rings = polygon.map(vertices => { | |
const ring: [number, number][] = []; | |
for (const k of range(vertices.length)) { | |
const [x1, y1] = vertices[k]; | |
const [x2, y2] = k < vertices.length - 1 ? vertices[k + 1] : vertices[0]; | |
const i1 = Math.floor(x1); | |
const j1 = Math.floor(y1); | |
const i2 = Math.floor(x2); | |
const j2 = Math.floor(y2); | |
if (i1 - i2 > 1 || i2 - i1 > 1 || j1 - j2 > 1 || j2 - j1 > 1) { | |
memoizedDrawLine(x1, y1, x2, y2).forEach(pt => ring.push(pt)); | |
ring.pop(); | |
} else { | |
ring.push([i1, j1]); | |
} | |
} | |
ring.forEach(pt => { | |
if (pt[0] < bbox[0]) bbox[0] = pt[0]; | |
if (pt[1] < bbox[1]) bbox[1] = pt[1]; | |
if (pt[0] > bbox[2]) bbox[2] = pt[0]; | |
if (pt[1] > bbox[3]) bbox[3] = pt[1]; | |
}); | |
return ring; | |
}); | |
const adjacentPixels = new Map<number, number>(); | |
rings.forEach(ring => { | |
adjacentPixelsFromRing(ring, bbox, adjacentPixels); | |
}); | |
return outlineAndFillFromAdjacentPixels(adjacentPixels, width, height, bbox); | |
} | |
export function drawLine( | |
x1: number, | |
y1: number, | |
x2: number, | |
y2: number | |
): [number, number][] { | |
let i = Math.floor(x1); | |
let j = Math.floor(y1); | |
const iEnd = Math.floor(x2); | |
const jEnd = Math.floor(y2); | |
const dx = Math.abs(x2 - x1); | |
const dy = Math.abs(y2 - y1); | |
const di = x2 >= x1 ? 1 : -1; | |
const dj = y2 >= y1 ? 1 : -1; | |
let lhs = dx * (y2 >= y1 ? j + 1 - y1 : y1 - j); | |
let rhs = dy * (x2 >= x1 ? i + 1 - x1 : x1 - i); | |
const visited: [number, number][] = []; | |
visited.push([i, j]); | |
while (i !== iEnd || j !== jEnd) { | |
if (lhs < rhs) { | |
j += dj; | |
lhs += dx; | |
} else if (lhs > rhs) { | |
i += di; | |
rhs += dy; | |
} else if (dj > 0) { | |
j += dj; | |
lhs += dx; | |
} else { | |
i += di; | |
rhs += dy; | |
} | |
visited.push([i, j]); | |
} | |
return visited; | |
} | |
export function drawCircle(n: number): number[] { | |
/* | |
Using a digital differential analyzer to compute the circle edge efficiently without trigonometry, or Pythagoras. | |
Start from the left edge and draw in a clockwise direction one pixel at a time until x = y line is reached. | |
(i.e 9 o'clock to 10:30) | |
Fill in rest of the circle by mirroring the edges. | |
*/ | |
const indices: number[] = []; | |
const offset = (n - 1) / 2; | |
let x = -offset; | |
let y = n & 1 ? 0 : -0.5; | |
let d = x * x + y * y - (n * n) / 4; | |
let deltaUp = 1 - 2 * y; | |
let deltaRight = 2 * x + 1; | |
while (y > x) { | |
indices.push(y * n + x, y * n - x + 1); | |
if (y !== 0) indices.push(-y * n + x, -y * n - x + 1); | |
y -= 1; | |
d += deltaUp; | |
deltaUp += 2; | |
while (d > 0) { | |
indices.push(x * n + (y + 1), x * n - (y + 1) + 1); | |
indices.push(-x * n + (y + 1), -x * n - (y + 1) + 1); | |
x += 1; | |
d += deltaRight; | |
deltaRight += 2; | |
} | |
} | |
if (y === x) { | |
indices.push(y * n + x, y * n - x + 1); | |
if (y !== 0) indices.push(-y * n + x, -y * n - x + 1); | |
} | |
const indexOffset = offset * (n + 1); | |
let lastIndex = -1; | |
const sortedIndices: number[] = []; | |
indices | |
.sort((a, b) => a - b) | |
.forEach(index => { | |
const properIndex = index + indexOffset; | |
if (properIndex === lastIndex) { | |
sortedIndices.pop(); | |
lastIndex = -1; | |
} else { | |
sortedIndices.push(properIndex); | |
lastIndex = properIndex; | |
} | |
}); | |
return sortedIndices; | |
} | |
export function adjacentPixelsFromRing( | |
ring: [number, number][], | |
bbox: [number, number, number, number], | |
adjacentPixels = new Map<number, number>() | |
): Map<number, number> { | |
const offsetX = -bbox[0]; | |
const offsetY = -bbox[1]; | |
const width = bbox[2] - bbox[0] + 1; | |
ring.forEach(([i, j], k) => { | |
const [prevI, prevJ] = k > 0 ? ring[k - 1] : ring[ring.length - 1]; | |
const pixel = (j + offsetY) * width + (i + offsetX); | |
const prevPixel = (prevJ + offsetY) * width + (prevI + offsetX); | |
let currAdjacent = adjacentPixels.get(pixel) ?? 0; | |
let prevAdjacent = adjacentPixels.get(prevPixel) ?? 0; | |
// 1/2/4/8/16/32/64/128 correspond to North, East, South, West, NE, NW, SW, SE | |
if (pixel - prevPixel === width) { | |
currAdjacent ^= 1; | |
prevAdjacent ^= 4; | |
} | |
if (pixel - prevPixel === 1) { | |
currAdjacent ^= 8; | |
prevAdjacent ^= 2; | |
} | |
if (prevPixel - pixel === width) { | |
currAdjacent ^= 4; | |
prevAdjacent ^= 1; | |
} | |
if (prevPixel - pixel === 1) { | |
currAdjacent ^= 2; | |
prevAdjacent ^= 8; | |
} | |
if (pixel - prevPixel === width + 1) { | |
currAdjacent ^= 16; | |
prevAdjacent ^= 64; | |
} | |
if (pixel - prevPixel === width - 1) { | |
currAdjacent ^= 32; | |
prevAdjacent ^= 128; | |
} | |
if (prevPixel - pixel === width + 1) { | |
currAdjacent ^= 64; | |
prevAdjacent ^= 16; | |
} | |
if (prevPixel - pixel === width - 1) { | |
currAdjacent ^= 128; | |
prevAdjacent ^= 32; | |
} | |
adjacentPixels.set(pixel, currAdjacent); | |
adjacentPixels.set(prevPixel, prevAdjacent); | |
}); | |
return adjacentPixels; | |
} | |
export function outlineAndFillFromAdjacentPixels( | |
adjacentPixels: Map<number, number>, | |
width: number, | |
height: number, | |
bbox: [number, number, number, number] = [0, 0, width - 1, height - 1] | |
): TransformedToSparse { | |
let outline: SparseBitmask; | |
let fill: SparseBitmask; | |
return { | |
toOutline() { | |
if (!outline) { | |
const overflowWidth = bbox[2] - bbox[0] + 1; | |
outline = new SparseBitmask(width * height, false); | |
adjacentPixels.forEach((_, pixel) => { | |
const i = Math.min(Math.max(0, pixel % overflowWidth), width - 1); | |
const j = Math.min( | |
Math.max(0, Math.trunc(pixel / overflowWidth)), | |
height - 1 | |
); | |
outline.set(j * width + i, 1); | |
}); | |
} | |
return outline; | |
}, | |
toFill() { | |
if (!fill) { | |
const overflowWidth = bbox[2] - bbox[0] + 1; | |
const overflowHeight = bbox[3] - bbox[1] + 1; | |
const outline = new SparseBitmask( | |
overflowWidth * overflowHeight, | |
false | |
); | |
adjacentPixels.forEach((_, pixel) => { | |
outline.set(pixel, 1); | |
}); | |
const beforeCut = new SparseBitmask( | |
overflowWidth * overflowHeight, | |
true | |
); | |
let runState = 0; | |
let nextRowStart = overflowWidth; | |
for (const pixel of outline.getIndices()) { | |
if (pixel >= nextRowStart) { | |
if (runState !== 0) { | |
beforeCut.flip(nextRowStart); | |
} | |
runState = 0; | |
while (pixel >= nextRowStart) { | |
nextRowStart += overflowWidth; | |
} | |
} | |
const junction = adjacentPixels.get(pixel) ?? 0; | |
let nextState = runState; | |
nextState ^= (junction & 16 ? 2 : 0) + (junction & 128 ? 1 : 0); | |
nextState ^= (junction & 1 ? 2 : 0) + (junction & 4 ? 1 : 0); | |
nextState ^= (junction & 32 ? 2 : 0) + (junction & 64 ? 1 : 0); | |
if (runState === 0) { | |
beforeCut.flip(pixel); | |
} | |
if (nextState === 0 && pixel + 1 < overflowWidth * overflowHeight) { | |
beforeCut.flip(pixel + 1); | |
} | |
runState = nextState; | |
} | |
fill = | |
overflowWidth > width || overflowHeight > height | |
? cutBitmask( | |
beforeCut, | |
overflowWidth, | |
overflowHeight, | |
-bbox[0], | |
-bbox[1], | |
width, | |
height | |
) | |
: beforeCut; | |
} | |
return fill; | |
}, | |
}; | |
} | |
export function cutAndPasteBitmask( | |
from: SparseBitmask, | |
fromWidth: number, | |
fromHeight: number, | |
cutX: number, | |
cutY: number, | |
cutWidth: number, | |
cutHeight: number, | |
pasteX: number, | |
pasteY: number, | |
toWidth: number, | |
toHeight: number, | |
to = new SparseBitmask(toWidth * toHeight, from.isRunEnds) | |
): SparseBitmask { | |
return cutAndPasteBitmask3d( | |
from, | |
fromWidth, | |
fromHeight, | |
1, | |
cutX, | |
cutY, | |
0, | |
cutWidth, | |
cutHeight, | |
1, | |
pasteX, | |
pasteY, | |
0, | |
toWidth, | |
toHeight, | |
1, | |
to | |
); | |
} | |
export function cutBitmask( | |
from: SparseBitmask, | |
fromWidth: number, | |
fromHeight: number, | |
x: number, | |
y: number, | |
toWidth: number, | |
toHeight: number | |
): SparseBitmask { | |
if (x === 0 && y === 0 && fromWidth === toWidth && fromHeight === toHeight) { | |
return from; | |
} | |
return cutAndPasteBitmask3d( | |
from, | |
fromWidth, | |
fromHeight, | |
1, | |
x, | |
y, | |
0, | |
toWidth, | |
toHeight, | |
1, | |
0, | |
0, | |
0, | |
toWidth, | |
toHeight, | |
1 | |
); | |
} | |
export function pasteBitmask( | |
from: SparseBitmask, | |
fromWidth: number, | |
fromHeight: number, | |
x: number, | |
y: number, | |
toWidth: number, | |
toHeight: number, | |
to = new SparseBitmask(toWidth * toHeight, from.isRunEnds) | |
): SparseBitmask { | |
return cutAndPasteBitmask3d( | |
from, | |
fromWidth, | |
fromHeight, | |
1, | |
0, | |
0, | |
0, | |
fromWidth, | |
fromHeight, | |
1, | |
x, | |
y, | |
0, | |
toWidth, | |
toHeight, | |
1, | |
to | |
); | |
} | |
export function offsetBitmask( | |
mask: SparseBitmask, | |
offsetX: number, | |
offsetY: number, | |
width: number, | |
height: number | |
): SparseBitmask { | |
return cutAndPasteBitmask3d( | |
mask, | |
width, | |
height, | |
1, | |
0, | |
0, | |
0, | |
width, | |
height, | |
1, | |
offsetX, | |
offsetY, | |
0, | |
width, | |
height, | |
1 | |
); | |
} | |
export function* runLengthsToRunEndIndices( | |
runLengths: Iterable<number>, | |
windowSize: number | |
): Iterable<number> { | |
let expectedSign = -1; | |
let offset = 0; | |
let k = 1; | |
for (const runLength of runLengths) { | |
if (runLength === windowSize) { | |
k = windowSize; | |
} else if (runLength < 0) { | |
if (expectedSign < 0) { | |
yield offset; | |
expectedSign = 1; | |
} | |
offset -= runLength * k; | |
k = 1; | |
} else if (runLength > 0) { | |
if (expectedSign > 0) { | |
yield offset; | |
expectedSign = -1; | |
} | |
offset += runLength * k; | |
k = 1; | |
} | |
} | |
} | |
export function* runLengthsToOneBitIndices( | |
runLengths: Iterable<number>, | |
windowSize: number | |
): Iterable<number> { | |
let offset = 0; | |
let k = 1; | |
for (const runLength of runLengths) { | |
if (runLength === windowSize) { | |
k = windowSize; | |
} else if (runLength < 0) { | |
const runEnd = offset - runLength * k; | |
while (offset < runEnd) { | |
yield offset; | |
offset += 1; | |
} | |
k = 1; | |
} else if (runLength > 0) { | |
offset += runLength * k; | |
k = 1; | |
} | |
} | |
} | |
export function getWeight( | |
mask: SparseBitmask, | |
width: number, | |
height: number, | |
depth = 1 | |
): number { | |
if (mask.length !== width * height * depth) { | |
throw new Error("mismatch parameters"); | |
} | |
if (!mask.isRunEnds) return mask.getIndicesCount(); | |
let lastIndex = -1; | |
let weight = 0; | |
for (const index of mask.getIndices()) { | |
if (lastIndex >= 0) { | |
weight += index - lastIndex; | |
lastIndex = -1; | |
} else { | |
lastIndex = index; | |
} | |
} | |
if (lastIndex >= 0) { | |
weight += width * height * depth - lastIndex; | |
} | |
return weight; | |
} | |
export function getFilledRatio( | |
mask: SparseBitmask, | |
width: number, | |
height: number, | |
depth = 1 | |
): number { | |
if (mask.length !== width * height * depth) { | |
throw new Error("mismatch parameters"); | |
} | |
return getWeight(mask, width, height, depth) / (width * height * depth); | |
} | |
export function getCentroid( | |
mask: SparseBitmask, | |
width: number, | |
height: number | |
): [number, number] { | |
const centroid = getCentroid3d(mask, width, height, 1); | |
return [centroid[0], centroid[1]]; | |
} | |
export function getWeightedCentroid( | |
masks: SparseBitmask[], | |
width: number, | |
height: number | |
): [number, number] { | |
if (masks.length === 0) return [width / 2, height / 2]; | |
let sumX = 0; | |
let sumY = 0; | |
let weight = 0; | |
for (const mask of masks) { | |
const [x, y] = getCentroid(mask, width, height); | |
const w = getWeight(mask, width, height); | |
weight += w; | |
sumX += x * w; | |
sumY += y * w; | |
} | |
return [sumX / weight, sumY / weight]; | |
} | |
export function getBoundingBox( | |
mask: SparseBitmask, | |
width: number, | |
height: number | |
): [number, number, number, number] { | |
const bbox = getBoundingBox3d(mask, width, height, 1); | |
return [bbox[0], bbox[1], bbox[3], bbox[4]]; | |
} | |
export function getOutlineFromFill( | |
mask: SparseBitmask, | |
width: number, | |
height: number | |
): SparseBitmask { | |
if (mask.length !== width * height) { | |
throw new Error("mismatch parameters"); | |
} | |
const p4 = mask.asRunEndIndices(); | |
// filter pixels where at least one adjacent pixel is unpainted | |
const p0 = offsetBitmask(p4, -1, -1, width, height); | |
const p1 = offsetBitmask(p4, 0, -1, width, height); | |
const p2 = offsetBitmask(p4, 1, -1, width, height); | |
const p3 = offsetBitmask(p4, -1, 0, width, height); | |
const p5 = offsetBitmask(p4, 1, 0, width, height); | |
const p6 = offsetBitmask(p4, -1, 1, width, height); | |
const p7 = offsetBitmask(p4, 0, 1, width, height); | |
const p8 = offsetBitmask(p4, 1, 1, width, height); | |
let intersect: Iterable<number> = p0.getIndices(); | |
intersect = intersectionRunEnds(intersect, p1.getIndices()); | |
intersect = intersectionRunEnds(intersect, p2.getIndices()); | |
intersect = intersectionRunEnds(intersect, p3.getIndices()); | |
intersect = intersectionRunEnds(intersect, p5.getIndices()); | |
intersect = intersectionRunEnds(intersect, p6.getIndices()); | |
intersect = intersectionRunEnds(intersect, p7.getIndices()); | |
intersect = intersectionRunEnds(intersect, p8.getIndices()); | |
const outline = new SparseBitmask(width * height, true).setIndices( | |
subtractionRunEnds(p4.getIndices(), intersect), | |
true | |
); | |
return outline; | |
} | |
export function getExtOutlineFromFill( | |
mask: SparseBitmask, | |
width: number, | |
height: number | |
): SparseBitmask { | |
if (mask.length !== width * height) { | |
throw new Error("mismatch parameters"); | |
} | |
const p4 = mask.asRunEndIndices(); | |
// merge all adjacent pixels and remove the original mask | |
const p0 = offsetBitmask(p4, -1, -1, width, height); | |
const p1 = offsetBitmask(p4, 0, -1, width, height); | |
const p2 = offsetBitmask(p4, 1, -1, width, height); | |
const p3 = offsetBitmask(p4, -1, 0, width, height); | |
const p5 = offsetBitmask(p4, 1, 0, width, height); | |
const p6 = offsetBitmask(p4, -1, 1, width, height); | |
const p7 = offsetBitmask(p4, 0, 1, width, height); | |
const p8 = offsetBitmask(p4, 1, 1, width, height); | |
let union: Iterable<number> = p0.getIndices(); | |
union = unionRunEnds(union, p1.getIndices()); | |
union = unionRunEnds(union, p2.getIndices()); | |
union = unionRunEnds(union, p3.getIndices()); | |
union = unionRunEnds(union, p5.getIndices()); | |
union = unionRunEnds(union, p6.getIndices()); | |
union = unionRunEnds(union, p7.getIndices()); | |
union = unionRunEnds(union, p8.getIndices()); | |
return new SparseBitmask(width * height, true).setIndices( | |
subtractionRunEnds(union, p4.getIndices()), | |
true | |
); | |
} | |
export interface PixelIsland { | |
id: number; | |
type: 1 | 0; | |
mask: SparseBitmask; | |
weight: number; | |
parent: number; | |
} | |
export interface AugmentedPixelIsland extends PixelIsland { | |
children: Set<number>; | |
nestedWeight: number; | |
} | |
/** | |
* Return the set of pixel islands from a mask | |
* ReturnType: | |
* - id: the id of the island | |
* - type: 0 for hole, 1 for island | |
* - mask: the island or hole bitmask as a single contiguous block | |
* (may contain holes or other islands within) | |
* - weight: the weight of the island (in pixel count and excluding children) | |
* - parent: the id of the island's parent (0 for background) | |
* For more sophisticated downstream operations call augmentIslands | |
*/ | |
export function getIslands( | |
mask: SparseBitmask, | |
width: number, | |
height: number | |
): PixelIsland[] { | |
if (mask.length !== width * height) { | |
throw new Error("mismatch parameters"); | |
} | |
if (mask.getIndicesCount() === 0) { | |
const island: PixelIsland = { | |
id: 0, | |
type: 0, | |
mask: new SparseBitmask(mask.length, true).setIndices([0], true), | |
weight: mask.length, | |
parent: 0, | |
}; | |
return [island]; | |
} | |
const runLengths = [...getWindowedRunLengths3d(mask, width, height)]; | |
const assignments: number[] = []; | |
const reassigned = new Map<number, number>(); | |
const parents = new Map<number, number>(); | |
let minA = -1; | |
let maxA = 1; | |
// ensures zero fills connected to top edge are assigned 0 | |
let prevRowOffsets: number[] = [width]; | |
let prevRowAssignments: number[] = [0]; | |
let currRowOffsets: number[] = []; | |
let currRowAssignments: number[] = []; | |
let prevPointer = 0; | |
let currOffset = 0; | |
let colMode = false; | |
// adding an additional full width run length | |
// ensures zero fills connected to bottom edge are assigned 0 | |
runLengths.push(width, 1); | |
runLengths.forEach(r => { | |
if (currOffset >= width) { | |
// reset everything once finish processing a row | |
prevRowOffsets = currRowOffsets; | |
prevRowAssignments = currRowAssignments; | |
currRowOffsets = []; | |
currRowAssignments = []; | |
prevPointer = 0; | |
currOffset = 0; | |
} | |
const connected = new Set<number>(); | |
let currA: number; | |
if (r === width) { | |
colMode = true; | |
currA = 0; | |
} else if (r < 0) { | |
const run = colMode ? -width : r; | |
let parent = 0; | |
if (prevRowOffsets[prevPointer] > currOffset) { | |
// prev run is connected, add to connected if is same sign | |
const prevA = prevRowAssignments[prevPointer]; | |
if (prevA < 0) connected.add(prevA); | |
else parent = prevA; | |
} | |
while (prevRowOffsets[prevPointer] < currOffset - run) { | |
// check next run, add to connected if is same sign | |
prevPointer += 1; | |
const prevA = prevRowAssignments[prevPointer]; | |
if (prevA < 0) connected.add(prevA); | |
else parent = prevA; | |
} | |
currA = minA; | |
if (connected.size > 0) { | |
// take smallest assignment (absolute value) among connected runs (from prev row) | |
// and assign it to the curr run (in curr row) | |
connected.forEach(prevA => { | |
if (prevA > currA) currA = prevA; | |
}); | |
} else { | |
// otherwise create a new assignment by incrementing counter | |
minA -= 1; | |
parents.set(currA, parent); | |
} | |
currOffset -= run; | |
colMode = false; | |
} else { | |
const run = colMode ? width : r; | |
let parent = 0; | |
if (prevRowOffsets[prevPointer] > currOffset) { | |
const prevA = prevRowAssignments[prevPointer]; | |
if (prevA >= 0) connected.add(prevA); | |
else parent = prevA; | |
} | |
while (prevRowOffsets[prevPointer] < currOffset + run) { | |
prevPointer += 1; | |
const prevA = prevRowAssignments[prevPointer]; | |
if (prevA >= 0) connected.add(prevA); | |
else parent = prevA; | |
} | |
currA = maxA; | |
if (currOffset <= 0 || currOffset + run >= width) { | |
// ensures zero fills connected to side edges are assigned 0d | |
connected.add(0); | |
} | |
if (connected.size > 0) { | |
connected.forEach(prevA => { | |
if (prevA < currA) currA = prevA; | |
}); | |
} else { | |
maxA += 1; | |
parents.set(currA, parent); | |
} | |
currOffset += run; | |
colMode = false; | |
} | |
// reassign connected runs (from prev row) to curr run's assignment (in curr row) | |
connected.forEach(prevA => { | |
if (prevA !== currA) { | |
reassigned.set(prevA, currA); | |
} | |
}); | |
currRowOffsets.push(currOffset); | |
currRowAssignments.push(currA); | |
assignments.push(currA); | |
}); | |
// remove the extra run length | |
runLengths.pop(); | |
runLengths.pop(); | |
assignments.pop(); | |
assignments.pop(); | |
// finalize assignment from smallest to largest (absolute value) | |
for (const k of range(maxA)) { | |
const curr = reassigned.get(k); | |
if (curr != null) { | |
const next = reassigned.get(curr); | |
if (next != null) { | |
reassigned.set(k, next); | |
} | |
} | |
} | |
for (const k of range(-1, minA, -1)) { | |
const curr = reassigned.get(k); | |
if (curr != null) { | |
const next = reassigned.get(curr); | |
if (next != null) { | |
reassigned.set(k, next); | |
} | |
} | |
} | |
const finalAssignments = assignments.map( | |
assignment => reassigned.get(assignment) ?? assignment | |
); | |
const islandSet = new Set(finalAssignments); | |
const islandMap = new Map<number, SparseBitmask>(); | |
islandSet.add(0); | |
islandSet.forEach(i => { | |
islandMap.set(i, new SparseBitmask(mask.length, true)); | |
}); | |
let index = 0; | |
let k = 1; | |
runLengths.forEach((r, i) => { | |
if (r === width) { | |
k = width; | |
} else { | |
const currMask = islandMap.get(finalAssignments[i])!; | |
currMask.flip(index); | |
if (r < 0) index -= r * k; | |
else index += r * k; | |
currMask.flip(index); | |
k = 1; | |
} | |
}); | |
const islands: PixelIsland[] = []; | |
islandMap.forEach((mask, id) => { | |
const weight = getWeight(mask, width, height); | |
if (id === 0 || weight > 0) { | |
let parentId = parents.get(id) ?? 0; | |
parentId = reassigned.get(parentId) ?? parentId; | |
const island: PixelIsland = { | |
id, | |
type: id < 0 ? 1 : 0, | |
mask, | |
weight, | |
parent: parentId, | |
}; | |
islands.push(island); | |
} | |
}); | |
return islands; | |
} | |
/** | |
* Augment the islands with children and nested weight | |
* and sort them largest to smallest by nested weight | |
*/ | |
export function augmentIslands(islands: PixelIsland[]): AugmentedPixelIsland[] { | |
const nestedWeight = computeIslandsNestedWeight(islands); | |
const children = new Map<number, Set<number>>(); | |
islands.forEach(island => { | |
children.set(island.id, new Set()); | |
}); | |
islands.forEach(island => { | |
const parentId = island.parent; | |
if (island.id !== parentId) { | |
children.get(parentId)!.add(island.id); | |
} | |
}); | |
return islands | |
.map(island => ({ | |
...island, | |
children: children.get(island.id)!, | |
nestedWeight: nestedWeight.get(island.id)!, | |
})) | |
.sort((a, b) => b.nestedWeight - a.nestedWeight); | |
} | |
export function computeIslandsNestedWeight( | |
islands: PixelIsland[] | |
): Map<number, number> { | |
const nestedWeight = new Map<number, number>(); | |
const relations = new Map< | |
number, | |
{ parent: number; children: Set<number> } | |
>(); | |
islands.forEach(island => { | |
relations.set(island.id, { parent: island.parent, children: new Set() }); | |
nestedWeight.set(island.id, island.weight); | |
}); | |
islands.forEach(island => { | |
const parentId = island.parent; | |
if (island.id !== parentId) { | |
relations.get(parentId)!.children.add(island.id); | |
} | |
}); | |
while (relations.size > 1) { | |
relations.forEach(({ children, parent }, self) => { | |
if (children.size === 0) { | |
nestedWeight.set( | |
parent, | |
nestedWeight.get(parent)! + nestedWeight.get(self)! | |
); | |
relations.get(parent)!.children.delete(self); | |
relations.delete(self); | |
} | |
}); | |
} | |
return nestedWeight; | |
} | |
export function resizeBitmask( | |
mask: SparseBitmask, | |
fromWidth: number, | |
fromHeight: number, | |
toWidth: number, | |
toHeight: number | |
): SparseBitmask { | |
return resizeBitmask3d(mask, fromWidth, fromHeight, 1, toWidth, toHeight, 1); | |
} | |
export function flipAxes( | |
mask: SparseBitmask, | |
width: number, | |
height: number | |
): SparseBitmask { | |
return switchPrimaryAxis(mask, width, height, 1); | |
} | |
interface RGBA { | |
r: number; | |
g: number; | |
b: number; | |
a: number; | |
} | |
let cachedCanvas: OffscreenCanvas; | |
export async function bitmaskToImage( | |
mask: SparseBitmask, | |
width: number, | |
height: number, | |
fill: RGBA, | |
outline: RGBA = { r: 0, g: 0, b: 0, a: 0 } | |
): Promise<string> { | |
if (mask.length !== width * height) { | |
throw new Error("mismatch parameters"); | |
} | |
const imageData = new ImageData(width, height); | |
const { data } = imageData; | |
if (fill.a > 0) { | |
const { r, g, b, a } = fill; | |
for (const index of mask.getIndices(false)) { | |
data[index * 4] = r; | |
data[index * 4 + 1] = g; | |
data[index * 4 + 2] = b; | |
data[index * 4 + 3] = a; | |
} | |
} | |
if (outline.a > 0) { | |
const { r, g, b, a } = outline; | |
const outlineMask = getOutlineFromFill(mask, width, height); | |
for (const index of outlineMask.getIndices(false)) { | |
data[index * 4] = r; | |
data[index * 4 + 1] = g; | |
data[index * 4 + 2] = b; | |
data[index * 4 + 3] = a; | |
} | |
} | |
if (!cachedCanvas) { | |
cachedCanvas = new OffscreenCanvas(width, height); | |
} | |
cachedCanvas.width = width; | |
cachedCanvas.height = height; | |
const ctx = cachedCanvas.getContext("2d")!; | |
ctx.putImageData(imageData, 0, 0); | |
const blob = await cachedCanvas.convertToBlob(); | |
return URL.createObjectURL(blob); | |
} | |
export function optimizeBitmaskToImage( | |
maxShorterSide: number | |
): ( | |
...args: Parameters<typeof bitmaskToImage> | |
) => Promise<[string, number, number, number, number]> { | |
return ( | |
mask: SparseBitmask, | |
width: number, | |
height: number, | |
fill: RGBA, | |
outline: RGBA = { r: 0, g: 0, b: 0, a: 0 } | |
) => { | |
const bbox = getBoundingBox(mask, width, height); | |
const cutWidth = bbox[2] - bbox[0]; | |
const cutHeight = bbox[3] - bbox[1]; | |
const cut = cutBitmask( | |
mask, | |
width, | |
height, | |
bbox[0], | |
bbox[1], | |
cutWidth, | |
cutHeight | |
); | |
const shorterSide = Math.min(cutWidth, cutHeight); | |
const targetScale = | |
shorterSide <= maxShorterSide ? 1 : maxShorterSide / shorterSide; | |
const resizedWidth = Math.ceil(cutWidth * targetScale); | |
const resizedHeight = Math.ceil(cutHeight * targetScale); | |
const resizedMask = resizeBitmask3d( | |
cut, | |
cutWidth, | |
cutHeight, | |
1, | |
resizedWidth, | |
resizedHeight, | |
1 | |
); | |
return bitmaskToImage( | |
resizedMask, | |
resizedWidth, | |
resizedHeight, | |
fill, | |
outline | |
).then( | |
url => | |
[url, bbox[0], bbox[1], bbox[2] - bbox[0], bbox[3] - bbox[1]] as [ | |
string, | |
number, | |
number, | |
number, | |
number | |
] | |
); | |
}; | |
} | |
export function bitmaskFromWeightedRuns( | |
runWeights: OrderedMap<number, number>, | |
maskSize: number, | |
threshold = 1 | |
): SparseBitmask { | |
const indices: number[] = []; | |
let accumulatedWeight = 0; | |
let curr = 0; | |
for (const [index, weight] of runWeights) { | |
if (index >= maskSize) break; | |
accumulatedWeight += weight; | |
if ( | |
(curr && accumulatedWeight < threshold) || | |
(!curr && accumulatedWeight >= threshold) | |
) { | |
indices.push(index); | |
curr = 1 - curr; | |
} | |
} | |
return new SparseBitmask(maskSize, true).setIndices(indices, true); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment