Created
June 25, 2023 06:39
-
-
Save rplacd/1ed41216a177c61bff3f6d83cc07126c to your computer and use it in GitHub Desktop.
Real time integer dithering test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- 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