diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@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", @@ -32,6 +33,7 @@ "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: '@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 @@ -74,6 +77,9 @@ dependencies: 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 @@ -935,6 +941,10 @@ packages: 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 diff --git a/web/calibrate/FindSafeExposure.ts b/web/calibrate/FindSafeExposure.ts new file mode 100644 --- /dev/null +++ b/web/calibrate/FindSafeExposure.ts @@ -0,0 +1,82 @@ +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; + } +} diff --git a/web/calibrate/Light9Calibrate.ts b/web/calibrate/Light9Calibrate.ts --- a/web/calibrate/Light9Calibrate.ts +++ b/web/calibrate/Light9Calibrate.ts @@ -4,15 +4,17 @@ import { customElement, query, state } f 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)); } @@ -78,37 +80,8 @@ export class Light9Calibrate extends Lit 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) { diff --git a/web/calibrate/Light9Camera.ts b/web/calibrate/Light9Camera.ts --- a/web/calibrate/Light9Camera.ts +++ b/web/calibrate/Light9Camera.ts @@ -179,7 +179,7 @@ export class Light9CameraSettingsTable e 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; diff --git a/web/calibrate/XyPlot.ts b/web/calibrate/XyPlot.ts --- a/web/calibrate/XyPlot.ts +++ b/web/calibrate/XyPlot.ts @@ -18,7 +18,7 @@ export class XyPlot extends LitElement { chart!: echarts.ECharts; @property() label: string = ""; @property() data: number[][] = []; - + render() { return html`
@@ -52,10 +52,25 @@ 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), + }, + }, + ], + } // + ); + } }