Select Git revision
-
Leo McElroy authoredLeo McElroy authored
index.js 11.67 KiB
import { render, html, svg } from 'https://unpkg.com/uhtml?module';
import { initVideoCamera } from "./initVideoCamera.js"
import { openBackground } from "./openBackground.js"
import * as draw from "./drawPatterns.js";
import { drawRaw } from "./drawRaw.js";
import { downloadImg } from "./downloadImg.js";
import { downloadText } from "./downloadText.js";
const state = {
reference: null,
background: null,
projectedHeight: 0,
backgroundDrawing: "blank", // lines, crosses
y: 450,
lineWidth: 10,
download: false,
threshold: 40,
overlay: false,
filterReference: true,
camera: {
focalLength: 1460,
width: 1920,
height: 1080, // 1080?
},
projector: {
focalLength: 1750,
width: 1280,
height: 720,
},
cameraPos: [
3.70792444,
-44.1464592,
-36.78435859
]
}
const resizeGetWidthHeightCtx = () => {
const nwd = state.background;
const container = nwd.querySelector(".container");
const [ canvas, ctx ] = getCanvasCtx(".scan-window", nwd);
const { width, height } = container.getBoundingClientRect();
canvas.width = width;
canvas.height = height;
state.projectedHeight = height;
return { width, height, ctx}
}
const getCanvasCtx = (selector, container = document) => {
const canvas = container.querySelector(selector);
const ctx = canvas.getContext("2d");
return [canvas, ctx];
}
const backgrounds = {
"blank": () => {
const { width, height, ctx } = resizeGetWidthHeightCtx();
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
},
"vertical-lines": () => {
const { width, height, ctx } = resizeGetWidthHeightCtx();
draw.verticalLines(width, height, ctx);
},
"gaussian": () => {
const { width, height, ctx } = resizeGetWidthHeightCtx();
const stdev = 5;
draw.gaussian(ctx, state.y, width, height, stdev, stdev*5);
},
"crosses": () => {
const { width, height, ctx } = resizeGetWidthHeightCtx();
draw.crosses(width, height, ctx);
},
"rectangle":() => {
const { width, height, ctx } = resizeGetWidthHeightCtx();
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = "white";
ctx.fillRect(0, state.y - state.lineWidth/2, width, state.lineWidth);
},
}
async function init() {
const video = document.querySelector("video");
const canvas = document.querySelector("#raw-canvas");
initVideoCamera(video);
drawRaw(canvas, video, state);
}
const view = (state) => html`
<div class="canvas-containers">
<video id="video" width="1920" height="1080"></video>
<canvas id="raw-canvas" @click=${getMouseCoordinates}></canvas>
<canvas id="bg-canvas"></canvas>
<canvas id="output-canvas"></canvas>
<canvas id="height-map"></canvas>
</div>
<div class="toolbox">
<button @click=${init}>init video</button>
<button @click=${() => {
state.background = openBackground();
}}>open background</button>
<button @click=${() => {
backgrounds[state.backgroundDrawing]();
}}>draw background</button>
<span>
Background:
<select @change=${e => {
const chosen = e.target.value;
state.backgroundDrawing = chosen;
if (chosen in backgrounds) backgrounds[chosen]();
else console.log("unknown background:", chosen);
}}>
<option value="blank">blank</option>
<option value="vertical-lines">vertical-lines</option>
<option value="crosses">crosses</option>
<option value="gaussian">gaussian</option>
<option value="rectangle">rectangle</option>
</select>
</span>
<span>
Y-Value: <input
type="number"
@change=${(e) => changeY(e)}
.value=${state.y}/>
</span>
<span>
Threshold: <input
type="number"
@change=${(e) => {state.threshold = Number(e.target.value); snapshot(state);}}
.value=${state.threshold}/>
</span>
<span>
Overlay Lines: <input
type="checkbox"
@change=${() => {state.overlay = !state.overlay} }
.value="${state.overlay}"/>
</span>
<span>
Filter Reference: <input
type="checkbox"
@change=${() => {state.filterReference = !state.filterReference; snapshot(state);} }
.checked=${state.filterReference}
.value=${state.filterReference}/>
</span>
<button @click=${async () => {
const [ canvas, ctx ] = getCanvasCtx("#raw-canvas");
state.reference = ctx.getImageData(0, 0, canvas.width, canvas.height);
const [ bgCanvas, bgCtx ] = getCanvasCtx("#bg-canvas");
bgCanvas.width = state.camera.width;
bgCanvas.height = state.camera.height;
// drawImage
// ctx.putImageData(state.reference, 0, 0, state.camera.width, state.camera.height, // source rectangle
// 0, 0, bgCanvas.width, bgCanvas.height);
bgCtx.putImageData(state.reference, 0, 0);
const outCanvas = document.querySelector("#output-canvas");
outCanvas.width = canvas.width;
outCanvas.height = canvas.height;
snapshot(state);
}}>set camera reference</button>
<button @click=${() => snapshot(state)}>snapshot</button>
<button @click=${() => getHeight()}>process</button>
<button @click=${() => scanDownload()}>scan</button>
<button @click=${() => downloadImg(`${state.y}-scan`, document.querySelector("#output-canvas"))}>download</button>
</div>
`
render(document.body, view(state));
function getMouseCoordinates(e) {
const canvas = document.querySelector("#raw-canvas");
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
console.log(x, y);
return {x, y}
}
function sleep(ms = 0) {
return new Promise(r => setTimeout(r, ms));
}
function snapshot(state) {
const { camera, threshold, reference } = state;
const width = camera.width;
const height = camera.height;
const rawCtx = document.querySelector("#raw-canvas").getContext("2d");
const frame = rawCtx.getImageData(0, 0, width, height);
var l = frame.data.length / 4;
for (var i = 0; i < l; i++) {
const bg = (reference.data[i * 4 + 0] + reference.data[i * 4 + 1] + reference.data[i * 4 + 2]) / 3;
const grey = (frame.data[i * 4 + 0] + frame.data[i * 4 + 1] + frame.data[i * 4 + 2]) / 3;
let result = grey;
if (state.filterReference) result = grey - bg;
if (threshold > 0) result = result > threshold ? 255 : 0;
frame.data[i * 4 + 0] = result;
frame.data[i * 4 + 1] = result;
frame.data[i * 4 + 2] = result;
}
document.querySelector("#output-canvas").getContext("2d").putImageData(frame, 0, 0);
}
// frep -> gerber -> osh park
// stencil font
function changeY(e) {
state.y = Number(e.target.value);
backgrounds[state.backgroundDrawing]();
snapshot(state);
}
async function scanDownload(e) {
const pts = [];
for (let i = 50; i < 500; i++) {
state.y = i;
backgrounds[state.backgroundDrawing]();
await sleep(50);
snapshot(state);
pts.push(getHeight());
}
const realPts = pts.map(x => x[0]).flat();
const cameraPts = pts.map(x => x[1]).flat();
console.log({realPts, cameraPts})
const n = realPts.length;
const ply = `ply
format ascii 1.0
element vertex ${n}
property float32 x
property float32 y
property float32 z
end_header
${realPts.map(([x, y, z]) => `${x} ${y} ${z}`).join("\n")}
`
const [ oCanvas, oCtx ] = getCanvasCtx("#output-canvas");
const [ heightMapCanvas, heightMapCtx ] = getCanvasCtx("#height-map");
const w = oCanvas.width;
const h = oCanvas.height;
heightMapCanvas.width = w;
heightMapCanvas.height = h;
const max = 700;
const min = 100;
const buffer = draw.fillBuffer(w, h, () => [0, 0, 0, 0]);
cameraPts.forEach( (pt, i) => {
const [x, y] = pt;
const z = realPts[i][2];
// const index = (Math.round(y)*w+x)*4;
const val = 255 - lerp(0, 255, (z-min)/(max-min));
// buffer[index] = val;
// buffer[index + 1] = 0;
// buffer[index + 2] = 0;
// buffer[index + 3] = 255;
setPxs(x, y, w, buffer, val);
} )
heightMapCtx.putImageData(new ImageData(buffer, w), 0, 0);
console.log(ply);
downloadText("scan.ply", ply);
}
const lerp = (x, y, a) => x * (1 - a) + y * a;
const setPxs = (x, y, w, buffer, val) => {
y = Math.floor(y);
const index0 = (y*w+x)*4;
buffer[index0] = val;
buffer[index0 + 1] = 0;
buffer[index0 + 2] = 0;
buffer[index0 + 3] = 255;
const index1 = ((y+1)*w+x)*4;
buffer[index1] = val;
buffer[index1 + 1] = 0;
buffer[index1 + 2] = 0;
buffer[index1 + 3] = 255;
}
function getMeanHeight(arr) {
let possiblePixels = [];
for (let i = 0; i < arr.length; i++) {
const px = arr[i];
if (possiblePixels.length === 0) possiblePixels.push(px);
else if (possiblePixels[possiblePixels.length - 1] - px === 1) possiblePixels.push(px);
else if (possiblePixels.length >= 4) break;
else possiblePixels = [];
}
return possiblePixels.length >= 4 ? possiblePixels.reduce((acc, cur) => acc + cur, 0)/possiblePixels.length : 0;
}
function getHeight() {
const outputCanvas = document.querySelector("#output-canvas");
const oCtx = outputCanvas.getContext("2d");
var image = oCtx.getImageData(0, 0, outputCanvas.width, outputCanvas.height);
var l = image.data.length / 4;
const rows = image.height;
const cols = image.width;
// let test = new Uint8ClampedArray(cols*rows*4);
const whitecounts = [];
for (let i = 0; i < cols; i++) {
let whitecount = [];
for (let j = rows-1; j >= 0; j--) {
const index = (i+j*cols)*4
const px = image.data[index+1];
if (px === 255) whitecount.push(j);
}
whitecounts.push(getMeanHeight(whitecount));
}
const points = [];
oCtx.fillStyle = "red";
for (let i = 0; i < whitecounts.length; i++) {
if (whitecounts[i] === 0) continue;
oCtx.beginPath();
oCtx.arc(i, whitecounts[i], 2, 0, 2 * Math.PI);
oCtx.fill();
const x = i - (image.width-1)/2;
const y = whitecounts[i] - (image.height-1)/2;
const norm = Math.sqrt(x**2+y**2+state.camera.focalLength**2);
points.push([
[ x/norm, y/norm, state.camera.focalLength/norm ],
[i, whitecounts[i]]
]);
}
const projectorPt = [ 0, state.y - state.projector.height/2, state.projector.focalLength ];
const projectorPtNorm = Math.sqrt(projectorPt[0]**2+projectorPt[1]**2+projectorPt[2]**2);
const projectorPtNormal = [
0/projectorPtNorm,
(state.y - state.projector.height/2)/projectorPtNorm,
state.projector.focalLength/projectorPtNorm
];
// const cameraPos = [7.14586212, -81.71286646, 76.40578868];
// const cameraPos = [
// 4.43436471,
// -80.2131717,
// -43.49579285
// ]
const cameraPos = state.cameraPos;
// const cameraPos = [0, -81.71286646, 0];
// what about camera rotation?
const realSpacePts = points
.map(x => x[0])
.map((u, i) => isect_line_plane_v3(cameraPos, u, [0, 0, 0], projectorPtNormal, [1, 0, 0]));
return [ realSpacePts, points.map(x => x[1]) ];
}
function crossProduct(v0, v1) {
return [
v0[1]*v1[2] - v0[2]*v1[1],
-(v0[0]*v1[2] - v0[2]*v1[0]),
v0[0]*v1[1] - v0[1]*v1[0],
]
}
function isect_line_plane_v3(cameraPos, cameraNormal, planePos, planeNormal0, planeNormal1, epsilon=1e-6) {
const crossNormals = crossProduct(planeNormal0, planeNormal1);
const numerator = dot(crossNormals, sub(cameraPos, planePos))
const denominator = dot(neg(cameraNormal), crossNormals);
const t = numerator/denominator;
return add(cameraPos, mul(cameraNormal, t));
}
function add(v0, v1) {
return [
v0[0] + v1[0],
v0[1] + v1[1],
v0[2] + v1[2],
]
}
function neg(v) {
return v.map(x => -x);
}
function sub(v0, v1) {
return [
v0[0] - v1[0],
v0[1] - v1[1],
v0[2] - v1[2],
]
}
function dot(v0, v1) {
return (
(v0[0] * v1[0]) +
(v0[1] * v1[1]) +
(v0[2] * v1[2])
)
}
function len_squared(v0) {
return dot_v3v3(v0, v0)
}
function mul(v0, f) {
return [
v0[0] * f,
v0[1] * f,
v0[2] * f,
]
}
// all in pixel units
const makeMatrix = ({ focalLength, width, height }) => [
[focalLength, 0, (width-1)/2],
[0, focalLength, (height-1)/2],
[0, 0, 1]
]
const cameraMatrix = makeMatrix(state.camera);
const projectorMatrix = makeMatrix(state.projector);