Changeset - e3af0ac507c8
[Not reviewed]
default
0 5 1
drewp@bigasterisk.com - 8 months ago 2024-05-21 21:08:17
drewp@bigasterisk.com
new exposure-finder algorithm
6 files changed with 115 insertions and 33 deletions:
0 comments (0 inline, 0 general)
package.json
Show inline comments
 
@@ -9,38 +9,40 @@
 
    "test": "test"
 
  },
 
  "dependencies": {
 
    "@material/mwc-button": "^0.27.0",
 
    "@material/mwc-dialog": "^0.27.0",
 
    "@material/mwc-slider": "^0.27.0",
 
    "@material/mwc-textfield": "^0.27.0",
 
    "@microsoft/fast-components": "^2.30.6",
 
    "@types/async": "^3.2.24",
 
    "@types/d3": "^7.4.3",
 
    "@types/debug": "^4.1.12",
 
    "@types/fpsmeter": "^0.3.34",
 
    "@types/lodash": "^4.17.4",
 
    "@types/n3": "^1.16.4",
 
    "@types/node": "^20.12.11",
 
    "@types/reconnectingwebsocket": "^1.0.10",
 
    "@types/underscore": "^1.11.15",
 
    "async": "^3.2.5",
 
    "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",
 
    "knockout": "^3.5.1",
 
    "lit": "^2.8.0",
 
    "lodash": "^4.17.21",
 
    "n3": "^1.17.3",
 
    "onecolor": "^4.1.0",
 
    "parse-prometheus-text-format": "^1.1.1",
 
    "react": "^18.3.1",
 
    "react-dom": "^18.3.1",
 
    "reconnectingwebsocket": "^1.0.0",
 
    "sub-events": "^1.9.0",
 
    "sylvester": "^0.0.21",
 
    "typescript": "^5.4.5",
 
    "underscore": "^1.13.6",
 
    "vite": "^4.5.3",
 
    "vite-plugin-rewrite-all": "^1.0.2",
pnpm-lock.yaml
Show inline comments
 
@@ -23,24 +23,27 @@ dependencies:
 
  '@types/async':
 
    specifier: ^3.2.24
 
    version: 3.2.24
 
  '@types/d3':
 
    specifier: ^7.4.3
 
    version: 7.4.3
 
  '@types/debug':
 
    specifier: ^4.1.12
 
    version: 4.1.12
 
  '@types/fpsmeter':
 
    specifier: ^0.3.34
 
    version: 0.3.34
 
  '@types/lodash':
 
    specifier: ^4.17.4
 
    version: 4.17.4
 
  '@types/n3':
 
    specifier: ^1.16.4
 
    version: 1.16.4
 
  '@types/node':
 
    specifier: ^20.12.11
 
    version: 20.12.11
 
  '@types/reconnectingwebsocket':
 
    specifier: ^1.0.10
 
    version: 1.0.10
 
  '@types/underscore':
 
    specifier: ^1.11.15
 
    version: 1.11.15
 
@@ -65,24 +68,27 @@ dependencies:
 
  fpsmeter:
 
    specifier: ^0.3.1
 
    version: 0.3.1
 
  immutable:
 
    specifier: ^4.3.5
 
    version: 4.3.5
 
  knockout:
 
    specifier: ^3.5.1
 
    version: 3.5.1
 
  lit:
 
    specifier: ^2.8.0
 
    version: 2.8.0
 
  lodash:
 
    specifier: ^4.17.21
 
    version: 4.17.21
 
  n3:
 
    specifier: ^1.17.3
 
    version: 1.17.3
 
  onecolor:
 
    specifier: ^4.1.0
 
    version: 4.1.0
 
  parse-prometheus-text-format:
 
    specifier: ^1.1.1
 
    version: 1.1.1
 
  react:
 
    specifier: ^18.3.1
 
    version: 18.3.1
 
@@ -926,24 +932,28 @@ packages:
 
  /@types/estree@1.0.5:
 
    resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
 
    dev: true
 

	
 
  /@types/fpsmeter@0.3.34:
 
    resolution: {integrity: sha512-ja3G1z7zkL0SUIH8fkhrSuOrayqiaocbLd4uZ8rTTbLLZy8wCdLx4E1ygOtJy/K5133VyXaU4r0Tr1js7edshA==}
 
    dev: false
 

	
 
  /@types/geojson@7946.0.14:
 
    resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==}
 
    dev: false
 

	
 
  /@types/lodash@4.17.4:
 
    resolution: {integrity: sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==}
 
    dev: false
 

	
 
  /@types/ms@0.7.34:
 
    resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
 
    dev: false
 

	
 
  /@types/n3@1.16.4:
 
    resolution: {integrity: sha512-6PmHRYCCdjbbBV2UVC/HjtL6/5Orx9ku2CQjuojucuHvNvPmnm6+02B18YGhHfvU25qmX2jPXyYPHsMNkn+w2w==}
 
    dependencies:
 
      '@rdfjs/types': 1.1.0
 
      '@types/node': 20.12.11
 
    dev: false
 

	
 
  /@types/node@20.12.11:
web/calibrate/FindSafeExposure.ts
Show inline comments
 
new file 100644
 
import { clamp } from "lodash";
 
import { sleep } from "./Light9Calibrate";
 
import { Light9Camera } from "./Light9Camera";
 
import { XyPlot } from "./XyPlot";
 

	
 
export class FindSafeExposure {
 
  expoMin: number;
 
  expoMax: number;
 
  expo: number;
 
  expoStep: number;
 
  sameStateSteps: number;
 

	
 
  constructor(public cam: Light9Camera, public plot: XyPlot) {
 
    const fixedSteps = 5;
 

	
 
    this.plot.clear();
 
    this.expoMin = 1;
 
    this.expoMax = 800;
 
    this.expo = this.expoMin;
 
    this.expoStep = (this.expoMax - this.expoMin) / fixedSteps;
 
    this.sameStateSteps = 0;
 
  }
 

	
 
  async run() {
 
    while (true) {
 
      const currentOverexposed = await this.gatherSample();
 
      this.step(currentOverexposed);
 
      if (this.plot.data.length > 50 || Math.abs(this.expoStep) < 1) {
 
        this.expo = this.expoMin;
 
        await this.gatherSample();
 
        break;
 
      }
 
    }
 
  }
 
  
 
  step(currentOverexposed: number) {
 
    const maxAllowedOverexposedPixels = 5;
 
    const turnaroundScale = 0.6;
 
    const stepsPerSide = 3;
 

	
 
    const overexposed = currentOverexposed > maxAllowedOverexposedPixels;
 
    if (this.expoStep > 0) {
 
      if (overexposed) {
 
        this.sameStateSteps += 1;
 
      }
 
    } else {
 
      if (!overexposed) {
 
        this.sameStateSteps += 1;
 
      }
 
    }
 
    this.plot.setXMarklines([
 
      { txt: "min", x: this.expoMin },
 
      { txt: "max", x: this.expoMax },
 
      { txt: "expo", x: this.expo },
 
    ]);
 
    const nextExpo = clamp(this.expo + this.expoStep, this.expoMin, this.expoMax);
 
    if (this.sameStateSteps > stepsPerSide || nextExpo <= this.expoMin || nextExpo >= this.expoMax) {
 
      if (this.expoStep > 0) {
 
        this.expoMax = this.expo;
 
      } else {
 
        this.expoMin = this.expo;
 
      }
 
      this.expoStep = this.expoStep * -1 * turnaroundScale;
 
      this.sameStateSteps = 0;
 
    }
 
    this.expo = clamp(this.expo + this.expoStep, this.expoMin, this.expoMax);
 
  }
 
  
 
  async gatherSample() {
 
    const settleMs = 200;
 

	
 
    await this.cam.set("exposureTime", this.expo);
 
    const settleUntil = Date.now() + settleMs;
 
    let miny = this.cam.saturatedPixelCount!;
 
    while (Date.now() < settleUntil) {
 
      await sleep(1000 / this.cam.videoSettings.frameRate);
 
      miny = Math.min(miny, this.cam.saturatedPixelCount!);
 
    }
 
    this.plot!.insertPoint(this.expo, clamp(miny, 0, 100));
 
    return miny;
 
  }
 
}
web/calibrate/Light9Calibrate.ts
Show inline comments
 
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 { FindSafeExposure } from "./FindSafeExposure";
 
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) {
 
export 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;
 
      }
 
@@ -69,55 +71,26 @@ export class Light9Calibrate extends Lit
 
  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);
 
      }
 
      const algo = new FindSafeExposure(this.cam!, this.plot!);
 
      await algo.run();
 
    });
 
  }
 
  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 () => {});
web/calibrate/Light9Camera.ts
Show inline comments
 
@@ -170,25 +170,25 @@ export class Light9CameraSettingsTable e
 
    "resizeMode",
 
    "sampleRate",
 
    "sampleSize",
 
    "suppressLocalAudioPlayback",
 
    "torch",
 
    "voiceIsolation",
 
  ];
 

	
 
  adjustable: Record<string, { min: string; max: string }> = {
 
    focusDistance: { min: "0", max: "1023" },
 
    brightness: { min: "0", max: "64" },
 
    colorTemperature: { min: "2800", max: "6500" },
 
    exposureTime: { min: "0", max: "400" },
 
    exposureTime: { min: "0", max: "800" },
 
  };
 

	
 
  @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)) {
web/calibrate/XyPlot.ts
Show inline comments
 
@@ -49,13 +49,28 @@ export class XyPlot extends LitElement {
 
    });
 
  }
 

	
 
  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 }] });
 
  }
 
  setXMarklines(lines: { txt: string; x: number }[]) {
 
    const markLineData = (row: { txt: string; x: number }, i: number) => ({ name: row.txt, label: { distance: 10*i, formatter: "{b} {c}", color: "#fff", textBorderWidth: 0 }, xAxis: row.x });
 
    this.chart.setOption(
 
      {
 
        series: [
 
          {
 
            name: "d",
 
            markLine: {
 
              data: lines.map(markLineData),
 
            },
 
          },
 
        ],
 
      } //
 
    );
 
}
 
}
0 comments (0 inline, 0 general)