Files @ af83aeef8b0a
Branch filter:

Location: light9/web/calibrate/Light9Camera.ts - annotation

drewp@bigasterisk.com
fancier spectrograms
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
9bb0eb587d5b
9bb0eb587d5b
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
d5750b2aaa9e
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
d5750b2aaa9e
d5750b2aaa9e
d5750b2aaa9e
d5750b2aaa9e
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
d5750b2aaa9e
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
e3af0ac507c8
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
d5750b2aaa9e
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
d5750b2aaa9e
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
ae4b90efb55a
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`
      <video id="video"></video>

      <div id="stack">
        <img src="zebra.png" />
        <canvas id="canvas"></canvas>
      </div>
      <div id="controls">
        <p>saturated pixels: ${saturatedCountDisplay}</p>
        <light9-camera-settings-table .cam=${this} .videoSettings=${this.videoSettings}></light9-camera-settings-table>
      </div>
    `;
  }

  protected async firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
    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 },
        width: 640,
        height: 480,
      },
    };
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    const t = stream.getVideoTracks()[0];
    await t.applyConstraints({
      brightness: 0,
      contrast: 32,
      colorTemperature: 4600,
      exposureMode: "manual",
      exposureTime: 250,
      whiteBalanceMode: "manual",
      // this could stop focus from moving around, but it also makes my cam
      // click on every page reload
      //   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 = [
    "autoGainControl",
    "aspectRatio",
    "backgroundBlur",
    "channelCount",
    "deviceId",
    "displaySurface",
    "echoCancellation",
    "eyeGazeCorrection",
    "faceFraming",
    "groupId",
    "latency",
    "noiseSuppression",
    "pointsOfInterest",
    "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: "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)) {
      if (!this.boring.includes(key)) {
        this.renderRow(key, rows);
      }
    }
    return html`<table>
      ${rows}
    </table>`;
  }

  private renderRow(key: string, out: TemplateResult<1>[]) {
    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`
        <input type="range" min="${conf.min}" max="${conf.max}" value="${this.videoSettings[key]}" data-param="${key}" @input=${this.setFromSlider} />
      `;
    }
    out.push(
      html`<tr>
        <td>${key}</td>
        <td>${valueDisplay}</td>
        <td>${adjuster}</td>
      </tr>`
    );
  }

  async setFromSlider(ev: InputEvent) {
    const el = ev.target as HTMLInputElement;
    await this.cam.set(el.dataset.param as string, parseFloat(el.value));
  }
}