Skip to content

Instantly share code, notes, and snippets.

@rplacd
Created June 25, 2023 06:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rplacd/1ed41216a177c61bff3f6d83cc07126c to your computer and use it in GitHub Desktop.
Save rplacd/1ed41216a177c61bff3f6d83cc07126c to your computer and use it in GitHub Desktop.
Real time integer dithering test
<!-- We Timothy Robards' webcam filter codepen as a starting
point for this webcam B or W conversion test.
https://codepen.io/trobes/pen/bxropm
-->
<!--
PURPOSE: this is a Canvas-based prototype of a RGB-to-grayscale-to-dithered BandW
conversion scheme that
– can be done, per pixel, immediately after generating a source pixel colour;
– uses storage at most equal to one graphics buffer, plus a comparatively small
amount of working memory;
– can rely only on integer operations;
– and, for the same input, have some level of temporal consistency;
These constraints should, in practice, allow an FPU-less 68k
Mac to do live RGB-to-B/W conversion (say, for 3D rendering.)
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Display Webcam Stream</title>
<style>
#player {
width: 512px;
height: 342px;
background-color: #666;
}
#photo {
width: 512px;
height: 342px;
background-color: #666;
}
</style>
<body>
<div class="photobooth">
<canvas class="photo"></canvas>
<video class="player"></video>
</div>
<script>
"use strict"
const video = document.querySelector('.player');
const canvas = document.querySelector('.photo');
const ctx = canvas.getContext('2d');
let interval;
// When the webcam loads, set up a callback that
// runs our B and W conversion scheme on every frame,
// writing it back on the canvas.
function onLoadWebcam() {
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
clearInterval(interval);
return interval = setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
let frame = ctx.getImageData(0, 0, width, height);
rgbToBAndW(frame.data, frame.width, frame.height);
ctx.putImageData(frame, 0, 0);
},16);
}
// The overall algorithm is the following.
// Given a source image 'srcImage', an array of RGBA values tagged with
// width W and height H, we will update the srcImage array in place
// to be a B/W image.
function rgbToBAndW(srcImage, imageWidth, imageHeight) {
console.assert(srcImage.length % 4 == 0)
// From now on, all of our operations will be
// integer operations.
// Loop over all columns, and then rows:
for(let y = 0; y < imageWidth; y += 1) {
let residual = 0;
for(let x = 0; x < imageWidth; x += 1) {
// Get the RGB values for this pixel:
let i = (y * imageWidth + x) * 4;
let r = srcImage[i];
let g = srcImage[i+1];
let b = srcImage[i+2];
// First, convert from RGB to grayscale.
// In order to keep ourselves in the integer domain,
// we'll use a lookup table.
let grayscale = rgbToGrayscale(r, g, b);
// Add the residual from the last pixel:
grayscale += residual;
// Compute the B/W value for this pixel:
let bw = grayscale > 127 ? 255 : 0;
// Compute the residual for the next pixel:
residual = grayscale - bw;
// Write the B/W value back to the image:
srcImage[i] = bw;
srcImage[i+1] = bw;
srcImage[i+2] = bw;
}
}
}
// The following RGB to grayscale conversion function
// implments the perceptually-weighted average
// (or "luma"):
// Gray = (Red * 0.2126 + Green * 0.7152 + Blue * 0.0722)
// However, we do so entirely with fixed point arithmetic.
// -- First, shift our weights up by 16 bits, so that we can do integer
// multiplication in 32 bits without losing precision
const iR_WEIGHT = 13933
// = 0.2126 * 2^16
const iG_WEIGHT = 46871
// = 0.7152 * 2^16
const iB_WEIGHT = 4732
// = 0.0722 * 2^16
function rgbToGrayscale(r, g, b) {
// The following operations happen in left-shift-by-16-space:
// Multiply each component by its weight:
let iR = r * iR_WEIGHT;
let iG = g * iG_WEIGHT;
let iB = b * iB_WEIGHT;
// Add them together:
let iGray = iR + iG + iB;
// Return to normal space, shifting back down by 16 bits:
let toReturn = iGray >> 16;
console.assert(
Number.isInteger(toReturn)
&& (toReturn >= 0 && toReturn <= 255))
return toReturn;
}
// On load, obtain a 512x342 webcam stream,
// and run 'doFrame' on every frame;
function onLoad() {
video.addEventListener('canplay', onLoadWebcam);
navigator.mediaDevices.getUserMedia({
video: {
width: 512,
height: 342,
}
})
.then(localMediaStream => {
video.srcObject = localMediaStream;
video.play();
})
};
onLoad();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment