myPicture (1).png

link:https://editor.p5js.org/StonesGate604/sketches/kCL5lFSm3

image.png

I want to use p5js to recreate the effect of the classic HOPE rendering from Obama's campaign.

However, achieving such a graphic effect completely is almost impossible for me at present, because the shadows in this image must have been drawn manually. My approach is to calculate the brightness of the pixels distributed across the chessboard on the canvas and then fill them with the corresponding colors.

let video;
let block = 3;
let t1 = 60,
  t2 = 100;
t3 = 120;
t4 = 160;

const NAVY = [0, 48, 73];
const RED = [198, 12, 48];
const CREAM = [240, 235, 210];
const PINK = [228, 117, 117];
const BLUE = [68, 134, 163];

function setup() {
  createCanvas(800, 600);
  pixelDensity(1);
  noSmooth();
  video = createCapture(VIDEO);
  video.size(400, 300);
  // video.hide();
}

function draw() {
  background(235);
  video.loadPixels();
  if (video.pixels.length === 0) return;

  const grid = min(width / video.width, height / video.height);

  for (let y = 0; y < video.height; y += block) {
    for (let x = 0; x < video.width; x += block) {
      const i = 4 * (y * video.width + x);
      const r = video.pixels[i];
      const g = video.pixels[i + 1];
      const b = video.pixels[i + 2];

      const br = 0.2126 * r + 0.7152 * g + 0.0722 * b; //Brightness is too demanding on performance.

      let color;
      if (br < t1) color = NAVY;
      else if (br < t2) color = BLUE;
      else if (br < t3) color = RED;
      else if (br < t4) color = PINK;
      else color = CREAM;

      noStroke();
      fill(color[0], color[1], color[2]);
      rect(x * grid, y * grid, block * grid,block * grid);
    }
  }
}

I set several thresholds for the brightness value; if it is less than a threshold, the corresponding color will be filled in that area.

let t1 = 60,
  t2 = 100;
t3 = 120;
t4 = 160;

One thing I should mention here is that I originally planned to use the brightness function to directly calculate the brightness values of the pixels in the chessboard distribution (let c = brightness(video.get(x,y))), which would have made my code much cleaner and allowed me to be lazy. However, for some reason, using brightness resulted in a very low frame rate and high computer usage. Therefore, I had to manually calculate the brightness values. The three correction coefficients here are brightness calculation coefficients that I found online that conform to human visual perception.

 for (let y = 0; y < video.height; y += block) {
    for (let x = 0; x < video.width; x += block) {
      const i = 4 * (y * video.width + x);
      const r = video.pixels[i];
      const g = video.pixels[i + 1];
      const b = video.pixels[i + 2];

      const br = 0.2126 * r + 0.7152 * g + 0.0722 * b; //Brightness is too demanding on performance.

      let color;
      if (br < t1) color = NAVY;
      else if (br < t2) color = BLUE;
      else if (br < t3) color = RED;
      else if (br < t4) color = PINK;
      else color = CREAM;

The final result is as follows.

image.png

I wanted to add outlines to the people and main objects in the image, but I didn't know how to do it, so I copied my code into chatGPT to have it modified. This is the modified code.

Link:https://editor.p5js.org/StonesGate604/sketches/kCL5lFSm3

// Week 8 Assessment
let video;

const BLOCK = 3;                
const T1 = 60, T2 = 100, T3 = 120, T4 = 160; 
const EDGE_THRESH = 150;       
const EDGE_MIX = 0.90;         

const NAVY  = [0, 48, 73];
const RED   = [198, 12, 48];
const CREAM = [240, 235, 210];
const PINK  = [228, 117, 117];
const BLUE  = [68, 134, 163];

function setup() {
  createCanvas(800, 600);
  pixelDensity(1);
  noSmooth();

  video = createCapture(VIDEO);
  video.size(400, 300); 
  video.hide();         
}

function draw() {
  background(235);
  video.loadPixels();
  if (video.pixels.length === 0) return;

// ---------------------- Sobel Edge detection(generated by chatGPT) ----------------------
  const vw = video.width, vh = video.height;

  const grid = min(width / vw, height / vh);
  const outW = vw * grid, outH = vh * grid;
  const ox = (width - outW) / 2, oy = (height - outH) / 2;

  const gray = new Uint8ClampedArray(vw * vh);
  for (let y = 0; y < vh; y++) {
    for (let x = 0; x < vw; x++) {
      const i = 4 * (y * vw + x);
      const r = video.pixels[i], g = video.pixels[i+1], b = video.pixels[i+2];
      gray[y * vw + x] = 0.2126*r + 0.7152*g + 0.0722*b;
    }
  }

  const edges = new Uint16Array(vw * vh);
  for (let y = 1; y < vh - 1; y++) {
    for (let x = 1; x < vw - 1; x++) {
      const idx = y * vw + x;
      const tl = gray[idx - vw - 1], tc = gray[idx - vw], tr = gray[idx - vw + 1];
      const ml = gray[idx - 1],      mr = gray[idx + 1];
      const bl = gray[idx + vw - 1], bc = gray[idx + vw], br = gray[idx + vw + 1];
      const gx = -tl - 2*ml - bl + tr + 2*mr + br;
      const gy =  tl + 2*tc + tr - bl - 2*bc - br;
      edges[idx] = Math.hypot(gx, gy);
    }
  }
  
  //-------------------------------------------------------------------------------------------

  for (let y = 0; y < vh; y += BLOCK) {
    for (let x = 0; x < vw; x += BLOCK) {
      const idx = y * vw + x;
      const br  = gray[idx];

     
      let c = CREAM;
      if (br < T1)      c = NAVY;
      else if (br < T2) c = BLUE;
      else if (br < T3) c = RED;
      else if (br < T4) c = PINK;

   // ---------------------- Sobel Edge detection(generated by chatGPT) ----------------------
      if (edges[idx] > EDGE_THRESH) {
        c = [
          lerp(c[0], NAVY[0], EDGE_MIX),
          lerp(c[1], NAVY[1], EDGE_MIX),
          lerp(c[2], NAVY[2], EDGE_MIX)
        ];
      }
  //--------------------------------------------------------------------------------------------

      fill(c[0], c[1], c[2]);
      rect(ox + x * grid, oy + y * grid, BLOCK * grid, BLOCK * grid);
    }
  }
}

function keyPressed() {
  if (key === 's')  saveCanvas('myPicture', 'png');
}

I've commented out the modified parts of chat. However, I didn't quite understand how chat implemented the outline function, so I asked it to explain. It says it uses an algorithm called Sobel.

The principle is to see if each pixel has a significant change in grayscale value relative to the surrounding pixels. For example, if a person wearing white clothes stands in front of a black wall, the transition between the white clothes and the black will change drastically from the camera's perspective. If there is a way to capture this change, then it is possible to draw an outline.

The first step is to calculate the grayscale(brightness) value.

 const gray = new Uint8ClampedArray(vw * vh);
  for (let y = 0; y < vh; y++) {
    for (let x = 0; x < vw; x++) {
      const i = 4 * (y * vw + x);
      const r = video.pixels[i], g = video.pixels[i+1], b = video.pixels[i+2];
      gray[y * vw + x] = 0.2126*r + 0.7152*g + 0.0722*b;
    }

I feel like this code is calculating the rate of change, but I don't actually understand what it's calculating or why it can produce the rate of change this way. In short, it calculates a value to detect the rate of change. If this value exceeds a set threshold, it draws an outline and puts this value into an unsigned int array.