Mercurial > code > home > repos > light9
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 } |