Skip to content
Snippets Groups Projects
Select Git revision
  • f8d412973dbabd6b6616621bd4b386c657bdd228
  • main default protected
2 results

index.js

Blame
  • 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);