# HG changeset patch
# User drewp@bigasterisk.com
# Date 1716193692 25200
# Node ID ae4b90efb55a54f40a194674d7c70165a82a0f08
# Parent 61dc5bc8ce2ebb0d3d5a06d662331c6a1c4542ad
start calibration tool
diff -r 61dc5bc8ce2e -r ae4b90efb55a package.json
--- a/package.json Mon May 20 01:27:09 2024 -0700
+++ b/package.json Mon May 20 01:28:12 2024 -0700
@@ -26,6 +26,7 @@
"avro-js": "^1.11.3",
"d3": "^7.9.0",
"debug": "^4.3.4",
+ "echarts": "^5.5.0",
"flexlayout-react": "^0.7.15",
"fpsmeter": "^0.3.1",
"immutable": "^4.3.5",
diff -r 61dc5bc8ce2e -r ae4b90efb55a pnpm-lock.yaml
--- a/pnpm-lock.yaml Mon May 20 01:27:09 2024 -0700
+++ b/pnpm-lock.yaml Mon May 20 01:28:12 2024 -0700
@@ -50,15 +50,15 @@
avro-js:
specifier: ^1.11.3
version: 1.11.3
- avsc:
- specifier: ^5.7.7
- version: 5.7.7
d3:
specifier: ^7.9.0
version: 7.9.0
debug:
specifier: ^4.3.4
version: 4.3.4
+ echarts:
+ specifier: ^5.5.0
+ version: 5.5.0
flexlayout-react:
specifier: ^0.7.15
version: 0.7.15(react-dom@18.3.1)(react@18.3.1)
@@ -1086,11 +1086,6 @@
underscore: 1.13.6
dev: false
- /avsc@5.7.7:
- resolution: {integrity: sha512-9cYNccliXZDByFsFliVwk5GvTq058Fj513CiR4E60ndDwmuXzTJEp/Bp8FyuRmGyYupLjHLs+JA9/CBoVS4/NQ==}
- engines: {node: '>=0.11'}
- dev: false
-
/base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -1651,6 +1646,13 @@
engines: {node: '>=10'}
dev: true
+ /echarts@5.5.0:
+ resolution: {integrity: sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==}
+ dependencies:
+ tslib: 2.3.0
+ zrender: 5.5.0
+ dev: false
+
/elliptic@6.5.5:
resolution: {integrity: sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==}
dependencies:
@@ -2550,6 +2552,10 @@
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: false
+ /tslib@2.3.0:
+ resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+ dev: false
+
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false
@@ -2794,3 +2800,9 @@
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
dev: false
+
+ /zrender@5.5.0:
+ resolution: {integrity: sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==}
+ dependencies:
+ tslib: 2.3.0
+ dev: false
diff -r 61dc5bc8ce2e -r ae4b90efb55a web/calibrate/Light9Calibrate.ts
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/calibrate/Light9Calibrate.ts Mon May 20 01:28:12 2024 -0700
@@ -0,0 +1,128 @@
+import debug from "debug";
+import { css, html, LitElement } from "lit";
+import { customElement, query, state } from "lit/decorators.js";
+import { CollectorClient } from "../collector/CollectorClient";
+import { getTopGraph } from "../RdfdbSyncedGraph";
+import { SyncedGraph } from "../SyncedGraph";
+import { Light9Camera } from "./Light9Camera";
+import { XyPlot } from "./XyPlot";
+export { RdfdbSyncedGraph } from "../RdfdbSyncedGraph";
+export { Light9Camera } from "./Light9Camera";
+export { XyPlot } from "./XyPlot";
+debug.enable("*");
+const log = debug("calibrate");
+
+async function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+@customElement("light9-calibrate")
+export class Light9Calibrate extends LitElement {
+ graph!: SyncedGraph;
+ static styles = [
+ css`
+ button {
+ min-height: 3em;
+ min-width: 10em;
+ }
+ `,
+ ];
+ collector: CollectorClient = new CollectorClient("calibrate");
+ @query("light9-camera", true) cam?: Light9Camera;
+ @query("xy-plot", true) plot?: XyPlot;
+ @state() device: string;
+ constructor() {
+ super();
+ this.device = "http://light9.bigasterisk.com/theater/vet/device/parR3";
+ getTopGraph().then((g) => {
+ this.graph = g;
+ });
+ }
+
+ render() {
+ return html`
+
Calibrate
+
+
+ Device to calibrate: [ ${this.device} ]
+
+
+
+
+
+
+
+
+
+ r/g/b/r*g*b lines ,x=send y=seen `;
+ }
+
+ async withButtonSpinner(ev: MouseEvent, fn: () => Promise) {
+ const btn = ev.target as HTMLButtonElement;
+ try {
+ btn.disabled = true;
+ await fn();
+ } finally {
+ btn.disabled = false;
+ }
+ }
+ async setToFull(ev: MouseEvent) {
+ await this.withButtonSpinner(ev, async () => {
+ this.collector.updateSettings([
+ /// device,attr,value
+ [this.device, "http://light9.bigasterisk.com/color", "#ffffff"],
+ [this.device, "http://light9.bigasterisk.com/white", 1],
+ ]);
+ });
+ }
+
+ async findSafeExposure(ev: MouseEvent) {
+ await this.withButtonSpinner(ev, async () => {
+
+ const gatherSample = async (expo: number) => {
+ await this.cam?.set("exposureTime", expo);
+ const settleUntil = Date.now() + 1000;
+ let miny = this.cam?.saturatedPixelCount!;
+ while (Date.now() < settleUntil) {
+ await sleep(50);
+ miny = Math.min(miny, this.cam?.saturatedPixelCount!);
+ }
+ this.plot!.insertPoint(expo, miny);
+ };
+
+ // todo: drive around without big skips, gradually slower, looking for the max workable expo
+ let fixedSteps = 8;
+ const expoMin = 1;
+ const expoMax = 200;
+ let expo = 0;
+ const data=this.plot!.data;
+ while (data.length < 20) {
+ if (data.length < fixedSteps + 1) {
+ expo = expoMin + ((expoMax - expoMin) / fixedSteps) * data.length;
+ } else {
+ let x2 = data.findIndex(([_, y]) => y > 2);
+ if (x2 < 1) x2 = 1;
+ const x1 = x2 - 1;
+ log(JSON.stringify([x1, data[x1], x2, data[x2]]));
+ expo = (data[x1][0] + data[x2][0]) / 2;
+ log(data);
+ }
+ await gatherSample(expo);
+ }
+ });
+ }
+ async setToZero(ev: MouseEvent) {
+ await this.withButtonSpinner(ev, async () => {
+ this.collector.updateSettings([
+ [this.device, "http://light9.bigasterisk.com/color", "#000000"],
+ [this.device, "http://light9.bigasterisk.com/white", 0],
+ ]);
+ });
+ }
+ async markTare(ev: MouseEvent) {
+ await this.withButtonSpinner(ev, async () => {});
+ }
+ async calibrateLoop(ev: MouseEvent) {
+ await this.withButtonSpinner(ev, async () => {});
+ }
+}
diff -r 61dc5bc8ce2e -r ae4b90efb55a web/calibrate/Light9Camera.ts
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/calibrate/Light9Camera.ts Mon May 20 01:28:12 2024 -0700
@@ -0,0 +1,227 @@
+import debug from "debug";
+import { css, html, LitElement, PropertyValueMap, TemplateResult } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+
+@customElement("light9-camera")
+export class Light9Camera extends LitElement {
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ }
+ #video {
+ display: none;
+ }
+ #stack {
+ position: relative;
+ width: 640px;
+ height: 480px;
+ }
+ #stack > * {
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+ #stack > :first-child {
+ position: static;
+ }
+ #stack > img {
+ opacity: 0;
+ animation: fadeIn 1s 1s ease-in-out forwards;
+ }
+ @keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+ `,
+ ];
+ videoEl!: HTMLVideoElement;
+ canvas!: HTMLCanvasElement;
+ ctx!: CanvasRenderingContext2D;
+ @state()
+ vtrack: MediaStreamTrack | undefined;
+
+ @property() saturatedPixelCount = 0;
+ @property() saturatedPixelFraction = 0;
+
+ @property() videoSettings: MediaTrackSettings & any = {};
+
+ render() {
+ const saturatedCountDisplay = `${this.saturatedPixelCount} (${(this.saturatedPixelFraction * 100).toFixed(2)}%)`;
+
+ return html`
+
+
+
+

+
+
+
+
saturated pixels: ${saturatedCountDisplay}
+
+
+ `;
+ }
+
+ protected async firstUpdated(_changedProperties: PropertyValueMap | Map) {
+ this.videoEl = this.shadowRoot!.getElementById("video") as HTMLVideoElement;
+ this.canvas = this.shadowRoot!.getElementById("canvas") as HTMLCanvasElement;
+ this.ctx = this.canvas.getContext("2d", { willReadFrequently: true })!;
+
+ const constraints: MediaStreamConstraints = {
+ video: {
+ facingMode: { ideal: "environment" },
+ frameRate: { max: 10 },
+ },
+ };
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ const t = stream.getVideoTracks()[0];
+ await t.applyConstraints({
+ brightness: 0,
+ contrast: 32,
+ colorTemperature: 6600,
+ exposureMode: "manual",
+ exposureTime: 250,
+ whiteBalanceMode: "manual",
+ focusMode: "manual",
+ focusDistance: 235,
+ } as MediaTrackConstraints);
+
+ this.vtrack = t;
+ this.videoEl.srcObject = stream;
+ this.videoEl.play();
+ this.videoSettings = this.vtrack.getSettings();
+
+ this.redrawLoop();
+ }
+
+ redrawLoop() {
+ if (this.videoEl.videoWidth !== 0 && this.videoEl.videoHeight !== 0) {
+ this.redraw();
+ }
+ // todo: video frames come slower than raf is waiting
+ requestAnimationFrame(this.redrawLoop.bind(this));
+ }
+
+ public async set(k: string, v: any) {
+ if (!this.vtrack) {
+ throw new Error("vtrack");
+ }
+ await this.vtrack.applyConstraints({ [k]: v });
+ this.videoSettings = this.vtrack.getSettings();
+ }
+
+ private redraw() {
+ this.canvas.width = this.videoEl.videoWidth;
+ this.canvas.height = this.videoEl.videoHeight;
+ this.ctx.drawImage(this.videoEl, 0, 0);
+ this.makeSaturatedPixelsTransparent();
+ }
+
+ private makeSaturatedPixelsTransparent() {
+ const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ const data = imageData.data;
+ this.saturatedPixelCount = 0;
+ for (let i = 0; i < data.length; i += 4) {
+ if (data[i] === 255 || data[i + 1] === 255 || data[i + 2] === 255) {
+ this.saturatedPixelCount += 1;
+
+ data[i + 3] = 0;
+ }
+ }
+ this.saturatedPixelFraction = this.saturatedPixelCount / (data.length / 4);
+ this.ctx.putImageData(imageData, 0, 0);
+ }
+}
+
+@customElement("light9-camera-settings-table")
+export class Light9CameraSettingsTable extends LitElement {
+ static styles = [
+ css`
+ table {
+ border-collapse: collapse;
+ }
+ td {
+ border: 1px solid gray;
+ padding: 1px 6px;
+ }
+ `,
+ ];
+
+ boring = [
+ "aspectRatio",
+ "backgroundBlur",
+ "channelCount",
+ "deviceId",
+ "displaySurface",
+ "echoCancellation",
+ "eyeGazeCorrection",
+ "faceFraming",
+ "groupId",
+ "latency",
+ "noiseSuppression",
+ "pointsOfInterest",
+ "resizeMode",
+ "sampleRate",
+ "sampleSize",
+ "suppressLocalAudioPlayback",
+ "torch",
+ "voiceIsolation",
+ ];
+
+ adjustable: Record = {
+ focusDistance: { min: "0", max: "1023" },
+ brightness: { min: "0", max: "64" },
+ colorTemperature: { min: "2800", max: "6500" },
+ exposureTime: { min: "0", max: "400" },
+ };
+
+ @property() cam!: Light9Camera;
+ @property() videoSettings: MediaTrackSettings & any = {};
+ supportedByBrowser: MediaTrackSupportedConstraints;
+ constructor() {
+ super();
+ this.supportedByBrowser = navigator.mediaDevices.getSupportedConstraints();
+ }
+ render() {
+ const rows: TemplateResult<1>[] = [];
+ for (const key of Object.keys(this.supportedByBrowser)) {
+ if (!this.boring.includes(key)) {
+ this.renderRow(key, rows);
+ }
+ }
+ return html``;
+ }
+
+ private renderRow(key: string, rows: any[]) {
+ let valueDisplay = "";
+ if (this.videoSettings[key] !== undefined) {
+ valueDisplay = JSON.stringify(this.videoSettings[key]);
+ }
+ let adjuster = html``;
+ let conf = this.adjustable[key];
+ if (conf !== undefined) {
+ adjuster = html`
+
+ `;
+ }
+ rows.push(
+ html`
+ ${key} |
+ ${valueDisplay} |
+ ${adjuster} |
+
`
+ );
+ }
+
+ async setFromSlider(ev: InputEvent) {
+ const el = ev.target as HTMLInputElement;
+ await this.cam.set(el.dataset.param as string, parseFloat(el.value));
+ }
+}
diff -r 61dc5bc8ce2e -r ae4b90efb55a web/calibrate/XyPlot.ts
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/calibrate/XyPlot.ts Mon May 20 01:28:12 2024 -0700
@@ -0,0 +1,61 @@
+import debug from "debug";
+import * as echarts from "echarts";
+import { css, html, LitElement, PropertyValueMap } from "lit";
+import { customElement, property } from "lit/decorators.js";
+debug.enable("*");
+const log = debug("calibrate");
+
+@customElement("xy-plot")
+export class XyPlot extends LitElement {
+ static styles = [
+ css`
+ #chart {
+ width: 800px;
+ height: 300px;
+ }
+ `,
+ ];
+ chart!: echarts.ECharts;
+ @property() label: string = "";
+ @property() data: number[][] = [];
+
+ render() {
+ return html`
+
+ `;
+ }
+
+ protected async firstUpdated(_changedProperties: PropertyValueMap | Map) {
+ var chartDom = this.shadowRoot!.getElementById("chart")!;
+ this.chart = echarts.init(chartDom);
+ this.chart.setOption({
+ animation: false,
+ xAxis: {
+ type: "value",
+ },
+ yAxis: {
+ type: "value",
+ },
+ series: [
+ {
+ name: "d",
+ data: [],
+ type: "line",
+ },
+ ],
+ });
+ }
+
+ clear() {
+ this.data.length = 0;
+ }
+
+ insertPoint(x: number, y: number) {
+ this.data.push([x, y]);
+ this.data.sort((a, b) => a[0] - b[0]);
+ this.chart.setOption({ series: [{ name: "d", data: this.data }] });
+ }
+}
diff -r 61dc5bc8ce2e -r ae4b90efb55a web/calibrate/index.html
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/calibrate/index.html Mon May 20 01:28:12 2024 -0700
@@ -0,0 +1,12 @@
+
+
+
+ calibrate
+
+
+
+
+
+
+
+
diff -r 61dc5bc8ce2e -r ae4b90efb55a web/calibrate/zebra.png
Binary file web/calibrate/zebra.png has changed
diff -r 61dc5bc8ce2e -r ae4b90efb55a web/collector/CollectorClient.ts
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/collector/CollectorClient.ts Mon May 20 01:28:12 2024 -0700
@@ -0,0 +1,30 @@
+type Settings = Array<[string,string,string|number]>;
+
+export class CollectorClient {
+ private settings: Settings;
+ constructor(public clientName:string) {
+ this.settings = [];
+ this.putLoop();
+ }
+ private async putLoop() {
+ await this.put();
+ setTimeout(() => {
+ this.putLoop();
+ }, 1000);
+ }
+ private async put() {
+ await fetch("/service/collector/attrs", {
+ method: "PUT",
+ body: JSON.stringify({
+ client: this.clientName,
+ clientSession: "unused",
+ sendTime: Date.now() / 1000,
+ settings: this.settings,
+ }),
+ });
+ }
+ public async updateSettings(settings: Settings) {
+ this.settings = settings;
+ await this.put()
+ }
+}
diff -r 61dc5bc8ce2e -r ae4b90efb55a web/panels.ts
--- a/web/panels.ts Mon May 20 01:27:09 2024 -0700
+++ b/web/panels.ts Mon May 20 01:28:12 2024 -0700
@@ -3,6 +3,7 @@
export { Light9EffectListing } from "./effects/Light9EffectListing";
export { Light9FadeUi } from "./fade/Light9FadeUi";
export { Light9DeviceSettings } from "./live/Light9DeviceSettings";
+export { Light9Calibrate } from "./calibrate/Light9Calibrate";
const panels: Map> = new Map([
["light9-ascoltami-ui", ["ascoltami", "ascoltami"]],
@@ -10,6 +11,7 @@
["light9-device-settings", ["device-settings", "live"]],
["light9-effect-listing", ["effect-listing" , "effectListing"]],
["light9-fade-ui", ["fade", "fade"]],
+ ["light9-calibrate", ["calibrate", "calibrate"]],
]);
export function panelElementNames(): string[] {