2026-05-24-15-37-16 修正mask线条桥接与DICOM复位入口
This commit is contained in:
@@ -1666,6 +1666,155 @@ function fillInternalMaskHoles(
|
||||
return patchedPixels;
|
||||
}
|
||||
|
||||
function addSegmentIntersectionsToRows(rows: number[][], width: number, height: number, segment: PlaneSegment) {
|
||||
const { a, b } = segment;
|
||||
if (!Number.isFinite(a.x) || !Number.isFinite(a.y) || !Number.isFinite(b.x) || !Number.isFinite(b.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaY = b.y - a.y;
|
||||
if (Math.abs(deltaY) < 0.01) {
|
||||
return;
|
||||
}
|
||||
|
||||
const minY = Math.max(0, Math.floor(Math.min(a.y, b.y)));
|
||||
const maxY = Math.min(height - 1, Math.ceil(Math.max(a.y, b.y)));
|
||||
for (let row = minY; row <= maxY; row += 1) {
|
||||
const sampleY = row + 0.5;
|
||||
const crosses = (sampleY >= a.y && sampleY < b.y) || (sampleY >= b.y && sampleY < a.y);
|
||||
if (!crosses) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const t = (sampleY - a.y) / deltaY;
|
||||
const x = a.x + (b.x - a.x) * t;
|
||||
if (Number.isFinite(x)) {
|
||||
rows[row].push(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function groupPlaneSegmentsByConnectivity(segments: PlaneSegment[], tolerance = 1.35) {
|
||||
if (segments.length <= 1) {
|
||||
return segments.length ? [segments] : [];
|
||||
}
|
||||
|
||||
const parents = segments.map((_, index) => index);
|
||||
const find = (index: number): number => {
|
||||
if (parents[index] !== index) {
|
||||
parents[index] = find(parents[index]);
|
||||
}
|
||||
return parents[index];
|
||||
};
|
||||
const union = (left: number, right: number) => {
|
||||
const leftRoot = find(left);
|
||||
const rightRoot = find(right);
|
||||
if (leftRoot !== rightRoot) {
|
||||
parents[rightRoot] = leftRoot;
|
||||
}
|
||||
};
|
||||
const buckets = new Map<string, Array<{ x: number; y: number; index: number }>>();
|
||||
const cellSize = Math.max(tolerance, 0.1);
|
||||
const toleranceSquared = tolerance * tolerance;
|
||||
const cellKey = (x: number, y: number) => `${x},${y}`;
|
||||
|
||||
segments.forEach((segment, index) => {
|
||||
[segment.a, segment.b].forEach((point) => {
|
||||
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cellX = Math.floor(point.x / cellSize);
|
||||
const cellY = Math.floor(point.y / cellSize);
|
||||
for (let dx = -1; dx <= 1; dx += 1) {
|
||||
for (let dy = -1; dy <= 1; dy += 1) {
|
||||
const candidates = buckets.get(cellKey(cellX + dx, cellY + dy));
|
||||
candidates?.forEach((candidate) => {
|
||||
const distanceSquared = (candidate.x - point.x) ** 2 + (candidate.y - point.y) ** 2;
|
||||
if (distanceSquared <= toleranceSquared) {
|
||||
union(index, candidate.index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const key = cellKey(cellX, cellY);
|
||||
const bucket = buckets.get(key) ?? [];
|
||||
bucket.push({ x: point.x, y: point.y, index });
|
||||
buckets.set(key, bucket);
|
||||
});
|
||||
});
|
||||
|
||||
const groups = new Map<number, PlaneSegment[]>();
|
||||
segments.forEach((segment, index) => {
|
||||
const root = find(index);
|
||||
const group = groups.get(root) ?? [];
|
||||
group.push(segment);
|
||||
groups.set(root, group);
|
||||
});
|
||||
|
||||
return [...groups.values()].sort((left, right) => right.length - left.length);
|
||||
}
|
||||
|
||||
function closeSmallMaskGaps(
|
||||
maskData: ImageData,
|
||||
width: number,
|
||||
height: number,
|
||||
rgb: { r: number; g: number; b: number },
|
||||
alpha: number,
|
||||
maxGap = 2,
|
||||
) {
|
||||
const toFill = new Set<number>();
|
||||
const hasPixel = (x: number, y: number) => maskData.data[(y * width + x) * 4 + 3] > 0;
|
||||
const mark = (x: number, y: number) => {
|
||||
if (x >= 0 && x < width && y >= 0 && y < height && !hasPixel(x, y)) {
|
||||
toFill.add(y * width + x);
|
||||
}
|
||||
};
|
||||
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
let lastFilled = -1;
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
if (!hasPixel(x, y)) {
|
||||
continue;
|
||||
}
|
||||
const gap = x - lastFilled - 1;
|
||||
if (lastFilled >= 0 && gap > 0 && gap <= maxGap) {
|
||||
for (let fillX = lastFilled + 1; fillX < x; fillX += 1) {
|
||||
mark(fillX, y);
|
||||
}
|
||||
}
|
||||
lastFilled = x;
|
||||
}
|
||||
}
|
||||
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
let lastFilled = -1;
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
if (!hasPixel(x, y)) {
|
||||
continue;
|
||||
}
|
||||
const gap = y - lastFilled - 1;
|
||||
if (lastFilled >= 0 && gap > 0 && gap <= maxGap) {
|
||||
for (let fillY = lastFilled + 1; fillY < y; fillY += 1) {
|
||||
mark(x, fillY);
|
||||
}
|
||||
}
|
||||
lastFilled = y;
|
||||
}
|
||||
}
|
||||
|
||||
toFill.forEach((index) => {
|
||||
const offset = index * 4;
|
||||
maskData.data[offset] = rgb.r;
|
||||
maskData.data[offset + 1] = rgb.g;
|
||||
maskData.data[offset + 2] = rgb.b;
|
||||
maskData.data[offset + 3] = alpha;
|
||||
});
|
||||
|
||||
return toFill.size;
|
||||
}
|
||||
|
||||
function drawFallbackClosedRegion(
|
||||
context: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
@@ -1688,7 +1837,17 @@ function drawFallbackClosedRegion(
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sorted = [...points].sort((left, right) => (
|
||||
const uniquePoints: Point2D[] = [];
|
||||
points.forEach((point) => {
|
||||
if (!uniquePoints.some((current) => pointDistanceSquared(current, point) < 1e-6)) {
|
||||
uniquePoints.push(point);
|
||||
}
|
||||
});
|
||||
if (uniquePoints.length < 3) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sorted = [...uniquePoints].sort((left, right) => (
|
||||
Math.abs(left.x - right.x) > 1e-6 ? left.x - right.x : left.y - right.y
|
||||
));
|
||||
const cross = (origin: Point2D, a: Point2D, b: Point2D) => (
|
||||
@@ -1709,7 +1868,7 @@ function drawFallbackClosedRegion(
|
||||
upper.push(point);
|
||||
});
|
||||
const hull = [...lower.slice(0, -1), ...upper.slice(0, -1)];
|
||||
const ordered = hull.length >= 3 ? hull : points;
|
||||
const ordered = hull.length >= 3 ? hull : uniquePoints;
|
||||
|
||||
context.save();
|
||||
context.globalAlpha = clamp(opacity, 0.1, 1) * 0.62;
|
||||
@@ -1741,35 +1900,6 @@ function fillSegmentsAsSolidMask(
|
||||
return 0;
|
||||
}
|
||||
|
||||
const rows: number[][] = Array.from({ length: height }, () => []);
|
||||
segments.forEach((segment) => {
|
||||
const { a, b } = segment;
|
||||
if (!Number.isFinite(a.x) || !Number.isFinite(a.y) || !Number.isFinite(b.x) || !Number.isFinite(b.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaY = b.y - a.y;
|
||||
if (Math.abs(deltaY) < 0.01) {
|
||||
return;
|
||||
}
|
||||
|
||||
const minY = Math.max(0, Math.floor(Math.min(a.y, b.y)));
|
||||
const maxY = Math.min(height - 1, Math.ceil(Math.max(a.y, b.y)));
|
||||
for (let row = minY; row <= maxY; row += 1) {
|
||||
const sampleY = row + 0.5;
|
||||
const crosses = (sampleY >= a.y && sampleY < b.y) || (sampleY >= b.y && sampleY < a.y);
|
||||
if (!crosses) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const t = (sampleY - a.y) / deltaY;
|
||||
const x = a.x + (b.x - a.x) * t;
|
||||
if (Number.isFinite(x)) {
|
||||
rows[row].push(x);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const rgb = parseHexColor(color);
|
||||
const alpha = Math.round(clamp(opacity, 0.1, 1) * 190);
|
||||
const maskCanvas = document.createElement('canvas');
|
||||
@@ -1781,51 +1911,67 @@ function fillSegmentsAsSolidMask(
|
||||
}
|
||||
const maskData = maskContext.createImageData(width, height);
|
||||
let filledPixels = 0;
|
||||
const groups = groupPlaneSegmentsByConnectivity(segments);
|
||||
const fallbackGroups: PlaneSegment[][] = [];
|
||||
|
||||
rows.forEach((intersections, row) => {
|
||||
if (intersections.length < 2) {
|
||||
return;
|
||||
}
|
||||
groups.forEach((group) => {
|
||||
const rows: number[][] = Array.from({ length: height }, () => []);
|
||||
group.forEach((segment) => addSegmentIntersectionsToRows(rows, width, height, segment));
|
||||
let groupPixels = 0;
|
||||
|
||||
intersections.sort((left, right) => left - right);
|
||||
const cleaned: number[] = [];
|
||||
intersections.forEach((x) => {
|
||||
const previous = cleaned[cleaned.length - 1];
|
||||
if (previous === undefined || Math.abs(previous - x) > 0.35) {
|
||||
cleaned.push(x);
|
||||
rows.forEach((intersections, row) => {
|
||||
if (intersections.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
intersections.sort((left, right) => left - right);
|
||||
const cleaned: number[] = [];
|
||||
intersections.forEach((x) => {
|
||||
const previous = cleaned[cleaned.length - 1];
|
||||
if (previous === undefined || Math.abs(previous - x) > 0.35) {
|
||||
cleaned.push(x);
|
||||
}
|
||||
});
|
||||
|
||||
for (let index = 0; index + 1 < cleaned.length; index += 2) {
|
||||
const rawStartX = cleaned[index];
|
||||
const rawEndX = cleaned[index + 1];
|
||||
if (rawEndX < 0 || rawStartX > width - 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startX = clamp(Math.ceil(rawStartX), 0, width - 1);
|
||||
const endX = clamp(Math.floor(rawEndX), 0, width - 1);
|
||||
if (endX < startX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let x = startX; x <= endX; x += 1) {
|
||||
const offset = (row * width + x) * 4;
|
||||
if (maskData.data[offset + 3] === 0) {
|
||||
maskData.data[offset] = rgb.r;
|
||||
maskData.data[offset + 1] = rgb.g;
|
||||
maskData.data[offset + 2] = rgb.b;
|
||||
maskData.data[offset + 3] = alpha;
|
||||
groupPixels += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (let index = 0; index + 1 < cleaned.length; index += 2) {
|
||||
const rawStartX = cleaned[index];
|
||||
const rawEndX = cleaned[index + 1];
|
||||
if (rawEndX < 0 || rawStartX > width - 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startX = clamp(Math.ceil(rawStartX), 0, width - 1);
|
||||
const endX = clamp(Math.floor(rawEndX), 0, width - 1);
|
||||
if (endX < startX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let x = startX; x <= endX; x += 1) {
|
||||
const offset = (row * width + x) * 4;
|
||||
maskData.data[offset] = rgb.r;
|
||||
maskData.data[offset + 1] = rgb.g;
|
||||
maskData.data[offset + 2] = rgb.b;
|
||||
maskData.data[offset + 3] = alpha;
|
||||
filledPixels += 1;
|
||||
}
|
||||
filledPixels += groupPixels;
|
||||
if (groupPixels < Math.max(12, Math.round(group.length * 0.45)) && group.length >= 3) {
|
||||
fallbackGroups.push(group);
|
||||
}
|
||||
});
|
||||
|
||||
filledPixels += closeSmallMaskGaps(maskData, width, height, rgb, alpha);
|
||||
filledPixels += fillInternalMaskHoles(maskData, width, height, rgb, alpha);
|
||||
maskContext.putImageData(maskData, 0, 0);
|
||||
context.drawImage(maskCanvas, 0, 0);
|
||||
if (filledPixels < Math.max(12, Math.round(segments.length * 0.45)) && segments.length >= 3) {
|
||||
filledPixels += drawFallbackClosedRegion(context, width, height, segments, color, opacity);
|
||||
}
|
||||
fallbackGroups.forEach((group) => {
|
||||
filledPixels += drawFallbackClosedRegion(context, width, height, group, color, opacity);
|
||||
});
|
||||
|
||||
if (filledPixels === 0) {
|
||||
context.save();
|
||||
|
||||
Reference in New Issue
Block a user