comparison web/calibrate/Light9Camera.ts @ 2417:ae4b90efb55a

start calibration tool
author drewp@bigasterisk.com
date Mon, 20 May 2024 01:28:12 -0700
parents
children 9bb0eb587d5b
comparison
equal deleted inserted replaced
2416:61dc5bc8ce2e 2417:ae4b90efb55a
1 import debug from "debug";
2 import { css, html, LitElement, PropertyValueMap, TemplateResult } from "lit";
3 import { customElement, property, state } from "lit/decorators.js";
4
5 @customElement("light9-camera")
6 export class Light9Camera extends LitElement {
7 static styles = [
8 css`
9 :host {
10 display: flex;
11 }
12 #video {
13 display: none;
14 }
15 #stack {
16 position: relative;
17 width: 640px;
18 height: 480px;
19 }
20 #stack > * {
21 position: absolute;
22 left: 0;
23 top: 0;
24 }
25 #stack > :first-child {
26 position: static;
27 }
28 #stack > img {
29 opacity: 0;
30 animation: fadeIn 1s 1s ease-in-out forwards;
31 }
32 @keyframes fadeIn {
33 from {
34 opacity: 0;
35 }
36 to {
37 opacity: 1;
38 }
39 }
40 `,
41 ];
42 videoEl!: HTMLVideoElement;
43 canvas!: HTMLCanvasElement;
44 ctx!: CanvasRenderingContext2D;
45 @state()
46 vtrack: MediaStreamTrack | undefined;
47
48 @property() saturatedPixelCount = 0;
49 @property() saturatedPixelFraction = 0;
50
51 @property() videoSettings: MediaTrackSettings & any = {};
52
53 render() {
54 const saturatedCountDisplay = `${this.saturatedPixelCount} (${(this.saturatedPixelFraction * 100).toFixed(2)}%)`;
55
56 return html`
57 <video id="video"></video>
58
59 <div id="stack">
60 <img src="zebra.png" />
61 <canvas id="canvas"></canvas>
62 </div>
63 <div id="controls">
64 <p>saturated pixels: ${saturatedCountDisplay}</p>
65 <light9-camera-settings-table .cam=${this} .videoSettings=${this.videoSettings}></light9-camera-settings-table>
66 </div>
67 `;
68 }
69
70 protected async firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
71 this.videoEl = this.shadowRoot!.getElementById("video") as HTMLVideoElement;
72 this.canvas = this.shadowRoot!.getElementById("canvas") as HTMLCanvasElement;
73 this.ctx = this.canvas.getContext("2d", { willReadFrequently: true })!;
74
75 const constraints: MediaStreamConstraints = {
76 video: {
77 facingMode: { ideal: "environment" },
78 frameRate: { max: 10 },
79 },
80 };
81 const stream = await navigator.mediaDevices.getUserMedia(constraints);
82 const t = stream.getVideoTracks()[0];
83 await t.applyConstraints({
84 brightness: 0,
85 contrast: 32,
86 colorTemperature: 6600,
87 exposureMode: "manual",
88 exposureTime: 250,
89 whiteBalanceMode: "manual",
90 focusMode: "manual",
91 focusDistance: 235,
92 } as MediaTrackConstraints);
93
94 this.vtrack = t;
95 this.videoEl.srcObject = stream;
96 this.videoEl.play();
97 this.videoSettings = this.vtrack.getSettings();
98
99 this.redrawLoop();
100 }
101
102 redrawLoop() {
103 if (this.videoEl.videoWidth !== 0 && this.videoEl.videoHeight !== 0) {
104 this.redraw();
105 }
106 // todo: video frames come slower than raf is waiting
107 requestAnimationFrame(this.redrawLoop.bind(this));
108 }
109
110 public async set(k: string, v: any) {
111 if (!this.vtrack) {
112 throw new Error("vtrack");
113 }
114 await this.vtrack.applyConstraints({ [k]: v });
115 this.videoSettings = this.vtrack.getSettings();
116 }
117
118 private redraw() {
119 this.canvas.width = this.videoEl.videoWidth;
120 this.canvas.height = this.videoEl.videoHeight;
121 this.ctx.drawImage(this.videoEl, 0, 0);
122 this.makeSaturatedPixelsTransparent();
123 }
124
125 private makeSaturatedPixelsTransparent() {
126 const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
127 const data = imageData.data;
128 this.saturatedPixelCount = 0;
129 for (let i = 0; i < data.length; i += 4) {
130 if (data[i] === 255 || data[i + 1] === 255 || data[i + 2] === 255) {
131 this.saturatedPixelCount += 1;
132
133 data[i + 3] = 0;
134 }
135 }
136 this.saturatedPixelFraction = this.saturatedPixelCount / (data.length / 4);
137 this.ctx.putImageData(imageData, 0, 0);
138 }
139 }
140
141 @customElement("light9-camera-settings-table")
142 export class Light9CameraSettingsTable extends LitElement {
143 static styles = [
144 css`
145 table {
146 border-collapse: collapse;
147 }
148 td {
149 border: 1px solid gray;
150 padding: 1px 6px;
151 }
152 `,
153 ];
154
155 boring = [
156 "aspectRatio",
157 "backgroundBlur",
158 "channelCount",
159 "deviceId",
160 "displaySurface",
161 "echoCancellation",
162 "eyeGazeCorrection",
163 "faceFraming",
164 "groupId",
165 "latency",
166 "noiseSuppression",
167 "pointsOfInterest",
168 "resizeMode",
169 "sampleRate",
170 "sampleSize",
171 "suppressLocalAudioPlayback",
172 "torch",
173 "voiceIsolation",
174 ];
175
176 adjustable: Record<string, { min: string; max: string }> = {
177 focusDistance: { min: "0", max: "1023" },
178 brightness: { min: "0", max: "64" },
179 colorTemperature: { min: "2800", max: "6500" },
180 exposureTime: { min: "0", max: "400" },
181 };
182
183 @property() cam!: Light9Camera;
184 @property() videoSettings: MediaTrackSettings & any = {};
185 supportedByBrowser: MediaTrackSupportedConstraints;
186 constructor() {
187 super();
188 this.supportedByBrowser = navigator.mediaDevices.getSupportedConstraints();
189 }
190 render() {
191 const rows: TemplateResult<1>[] = [];
192 for (const key of Object.keys(this.supportedByBrowser)) {
193 if (!this.boring.includes(key)) {
194 this.renderRow(key, rows);
195 }
196 }
197 return html`<table>
198 ${rows}
199 </table>`;
200 }
201
202 private renderRow(key: string, rows: any[]) {
203 let valueDisplay = "";
204 if (this.videoSettings[key] !== undefined) {
205 valueDisplay = JSON.stringify(this.videoSettings[key]);
206 }
207 let adjuster = html``;
208 let conf = this.adjustable[key];
209 if (conf !== undefined) {
210 adjuster = html`
211 <input type="range" min="${conf.min}" max="${conf.max}" value="${this.videoSettings[key]}" data-param="${key}" @input=${this.setFromSlider} />
212 `;
213 }
214 rows.push(
215 html`<tr>
216 <td>${key}</td>
217 <td>${valueDisplay}</td>
218 <td>${adjuster}</td>
219 </tr>`
220 );
221 }
222
223 async setFromSlider(ev: InputEvent) {
224 const el = ev.target as HTMLInputElement;
225 await this.cam.set(el.dataset.param as string, parseFloat(el.value));
226 }
227 }