feat: visualize beam spread around peaks

This commit is contained in:
Codex
2026-08-28 15:48:02 +09:00
parent 95f2ca19a6
commit b678d1f9c7
8 changed files with 414 additions and 2 deletions
+71 -1
View File
@@ -411,6 +411,74 @@ function peaksForBand(band, evaluations) {
return Array.isArray(peaks) ? peaks : [];
}
function traceBeamSpreadEllipse(context, spread, scan, plot, radiusScale) {
const centroid = spread?.centroid;
const ellipse = spread?.ellipse;
if (
!Array.isArray(centroid) ||
!finiteNumber(centroid[0]) ||
!finiteNumber(centroid[1]) ||
!finiteNumber(ellipse?.majorRmsRadiusM) ||
!finiteNumber(ellipse?.minorRmsRadiusM) ||
!finiteNumber(ellipse?.angleDeg) ||
ellipse.majorRmsRadiusM < 0 ||
ellipse.minorRmsRadiusM < 0
) {
return false;
}
const major = ellipse.majorRmsRadiusM * radiusScale;
const minor = ellipse.minorRmsRadiusM * radiusScale;
if (!(major > 0) && !(minor > 0)) return false;
const angle = (ellipse.angleDeg * Math.PI) / 180;
const cosine = Math.cos(angle);
const sine = Math.sin(angle);
const segmentCount = 64;
context.beginPath();
for (let segment = 0; segment <= segmentCount; segment += 1) {
const phase = (segment / segmentCount) * Math.PI * 2;
const alongMajor = major * Math.cos(phase);
const alongMinor = minor * Math.sin(phase);
const position = [
centroid[0] + alongMajor * cosine - alongMinor * sine,
centroid[1] + alongMajor * sine + alongMinor * cosine,
];
const point = scanPositionToCanvas(position, scan, plot);
if (segment === 0) context.moveTo(point.x, point.y);
else context.lineTo(point.x, point.y);
}
context.closePath();
return true;
}
function drawBeamSpreadContours(context, bands, evaluations, scan, plot) {
context.save();
context.beginPath();
context.rect(plot.x, plot.y, plot.width, plot.height);
context.clip();
for (const band of bands) {
if (!band || band.visible === false) continue;
const color = /^#[0-9a-f]{6}$/i.test(band.color ?? "") ? band.color : "#65f2bf";
for (const peak of peaksForBand(band, evaluations).slice(0, 128)) {
const spread = peak?.beamSpread;
if (!spread || spread.calibrated !== false) continue;
context.strokeStyle = color;
context.fillStyle = color;
context.lineWidth = 1.2;
context.setLineDash([]);
if (traceBeamSpreadEllipse(context, spread, scan, plot, 2)) {
context.globalAlpha = 0.07;
context.fill();
context.globalAlpha = 0.45;
context.stroke();
}
context.setLineDash([4, 4]);
context.globalAlpha = 0.78;
if (traceBeamSpreadEllipse(context, spread, scan, plot, 1)) context.stroke();
}
}
context.restore();
}
function drawBandPeakMarkers(context, bands, evaluations, scan, plot) {
for (const band of bands) {
if (!band || band.visible === false) continue;
@@ -483,6 +551,7 @@ function normalizeSceneModel(model) {
value.dynamicRangeDb ?? value.experiment?.beamforming?.dynamicRangeDb ?? DEFAULT_DYNAMIC_RANGE_DB,
opacity: value.opacity ?? 0.78,
showTruth: value.showTruth ?? true,
showBeamSpread: value.showBeamSpread ?? true,
};
}
@@ -518,6 +587,7 @@ function drawScene(context, size, sourceModel, canvas) {
// Map storage is row-major from yMax to yMin, which is the Canvas top-to-bottom order.
context.drawImage(heatmap.canvas, plot.x, plot.y, plot.width, plot.height);
context.restore();
if (model.showBeamSpread) drawBeamSpreadContours(context, bands, model.evaluations, scan, plot);
drawBandPeakMarkers(context, bands, model.evaluations, scan, plot);
drawBandLegend(context, heatmap.activeBands, plot);
}
@@ -530,7 +600,7 @@ function drawScene(context, size, sourceModel, canvas) {
* `render()` coalesces repeated updates into one animation frame. Use
* `renderNow()` only for deterministic capture/tests. Accepted model fields:
* `{ experiment|scan, result|bands, scene|sources, image, imagePlane,
* evaluations, dynamicRangeDb, opacity, showTruth }`.
* evaluations, dynamicRangeDb, opacity, showTruth, showBeamSpread }`.
*/
export function createSceneVisualization(canvas) {
if (!canvas) throw new TypeError("scene canvas가 필요합니다.");