Computer Vision is Hard

So cameras, computer vision and cool stuffs! Yay! For this week, I messed around with the Video and OpenCV libraries. this resulted in two programs: 1) a program that taces the webcam capture and runs an edge-identifying algorithm to re-draw the capture in cool, computer-y ways, and 2) an attempt at a function that is able to identify the inside of a shape drawn with a black marker. Fun times.

Program number one was fairly straightforward. I started by opening the OpenCV example that dealt with edge detection to get acquainted with the methods and how to use them. From there, I just hd to slightly modify the sketch: add the Video library and replace the image to be analyzed with the current frame of the camera’s capture. I also added a green tint because that was the first color that came to mind. 😀

 

Look at dat retro green aesthetic
Look at dat retro green aesthetic

Here’s the code (most of the credit goes to the library’s creator):

import processing.video.*;

import gab.opencv.*;

Capture capture;
OpenCV opencv;
PImage src, canny, scharr, sobel;

void setup() {
  size(640, 480);
  capture = new Capture(this, 640, 480);

  capture.start();
}

void draw() {
  src = capture;
  opencv = new OpenCV(this, src);
  opencv.findCannyEdges(30, 85);
  canny = opencv.getSnapshot();

  opencv.loadImage(src);
  opencv.findScharrEdges(OpenCV.HORIZONTAL);
  scharr = opencv.getSnapshot();

  opencv.loadImage(src);
  opencv.findSobelEdges(1, 0);
  sobel = opencv.getSnapshot();
  pushMatrix();
  scale(0.5);
  image(src, 0, 0);
  tint(0, 255, 0);
  image(canny, src.width, 0);
  image(scharr, 0, src.height);
  image(sobel, src.width, src.height);
  popMatrix();

  text("Source", 10, 25); 
  text("Canny", src.width/2 + 10, 25); 
  text("Scharr", 10, src.height/2 + 25); 
  text("Sobel", src.width/2 + 10, src.height/2 + 25);
}

void captureEvent(Capture c) {
  c.read();
}

And now the painful fun part!

OpenCV comes with a contour-identifying function, which works great, but I couldn’t think of a way to use it to identify only shapes within drawn borders. Instead, it would find many contours that were irrelevant for my purposes.

Thus, I took inspiration from the FindContours example. There, the gray() and threshold() methods are used to turn an image into a grayscale version of itself and then setting all pixels over a certain value of brightness to white, and all others to black. That way, I can work with a simplified version of the image I’m analyzing. (This, however, also introduced some noise, as any shadow dark enough would be turned pitch black, and parts of the outline would be too light to be identified at times).

Then, I wrote I function to look for shapes. It looks at the image pixel by pixel. First, it tries to find an upper left corner. Then it defines that spot as the inside of the shape, and continues marking pixels as inside until it hits an edge, after which it marks the area as outside of the shape. Thus, finding edges while moving through horizontal rows of pixels would trigger the inside/outside of the shape. I also made the program “finish” the shape once it finds a lower right corner.

My algorithm, however, is quite problematic. It can’t deal with (even slightly) concave shapes or shapes whose bottom is thinner than its top. I’ve thought of a couple of ways to address this (noise reduction functions, functions to “smooth” out the analyzed image, making the shape finder function analyze the shape vertically as well as horizontally, etc.), but regardless of the outcome, this was an interesting exercise. 😀

[I’ll insert image here eventually]

And the code. It includes several attempts and upgrades and downgrades (?) and everything. Yay.

import gab.opencv.*;

import processing.video.*;

Capture capture;
OpenCV opencv;

PImage snapshot, analyzed, render;

ArrayList<Contour> contours;
ArrayList<Contour> polygons;

int threshold = 70;

void setup() {
  //fullScreen();
  size(640, 480);
  capture = new Capture(this, 640, 480);
  capture.start();
  snapshot = capture.get();
  opencv = new OpenCV(this, snapshot);
  opencv.gray();
  opencv.threshold(threshold);
  analyzed = opencv.getOutput();
  contours = opencv.findContours();
  println("found " + contours.size() + " contours");
  noFill();
  for (Contour contour : contours) {
    stroke(0, 255, 0);
    contour.draw();
  }
  render = analyzed;
}

void draw() {
  scale(0.5);
  image(capture, 0, 0);
  //image(snapshot, 640, 0);
  image(analyzed, 640, 0);
  //image(analyzed, 0, 480);

  //for (Contour contour : contours) {
  // strokeWeight(3);
  // stroke(0, 255, 0);
  // pushMatrix();
  // translate(640, 0);
  // contour.draw();
  // stroke(255, 0, 0);
  // beginShape();
  // for (PVector point : contour.getPolygonApproximation().getPoints()) {
  //   vertex(point.x, point.y);
  // }
  // endShape();  
  // popMatrix();
  //}
}

void keyPressed() {
  switch (key) {
  case ' ':
    snapshot = capture.get();
    opencv = new OpenCV(this, snapshot);
    opencv.gray();
    analyzed = opencv.getOutput();
    int minVal = 255;
    int maxVal = 0;
    for (int q = 0; q < analyzed.pixels.length; q ++) {
      if (int(red(analyzed.pixels[q])) > minVal) {
        minVal = int(red(analyzed.pixels[q]));
      }
      if (int(red(analyzed.pixels[q])) < maxVal) {
        maxVal = int(red(analyzed.pixels[q]));
      }
    }
    threshold = int((minVal + maxVal)*0.3);
    opencv.threshold(threshold);
    analyzed = opencv.getOutput();
    contours = opencv.findContours();
    println("found " + contours.size() + " contours");
    noFill();
    for (Contour contour : contours) {
      strokeWeight(3);
      stroke(0, 255, 0);
      contour.draw();
    }
    PImage smoothAnalyzed = smoothEdge(analyzed, 5);
    detectShape2(smoothAnalyzed, render);
    image(smoothAnalyzed, 0, 480);
    image(render, 640, 480);
    break;
  }
}

void captureEvent(Capture c) {
  c.read();
}
void detectShape(PImage check, PImage destination) {
  boolean shapeFound = false;
  boolean inShape = false;  
  boolean onEdge = false;
  boolean checkingCorner = false;
  for (int q = 0; q < check.pixels.length; q ++) {
    if (red(check.pixels[q]) == 0) { //If black
      if (shapeFound == false) { //If you haven't found a shape
        checkingCorner = true; //Start checking for corners
      } else { //If you have found a shape already
        onEdge = true; //That means you are on the edge
      }
      destination.loadPixels();
      destination.pixels[q] = color(255, 255, 255); //All black edges will be white, I only care about inside
      destination.updatePixels();
    } else { //If white
      if (inShape == false && onEdge == true) { //If you were on an edge right before and were outside the shape
        onEdge = false; //Then you are not on the edge anymore
        inShape = true; //But rather within the shape
      } else if (inShape == true && onEdge == true) { //If you were on an edge before and within the shape
        onEdge = false; //Then you are ount of the edge now
        inShape = false; //And outside of the shape as well
      }
      if (shapeFound == true && inShape == true) { //If you've found a shape and you are within it
        destination.loadPixels();
        destination.pixels[q] = color(255, 0, 0); //Mark the pixel
        destination.updatePixels();

        int corner = 0; //Check if adyacent pixels are black
        if (red(check.pixels[q+1]) == 0) { //Check right
          corner ++;
        }
        //if (red(check.pixels[q+1]) == 0) {
        //  corner ++;
        //}
        if (q < check.pixels.length - check.width) { //Check down
          if (red(check.pixels[q+check.width]) == 0) {
            corner ++;
          }
        }
        if (corner >= 2) { //If at least two are black
          shapeFound = false; //Then it was a corner and you've found the end of the shape
          inShape = false; //therefore you are now outside the shape
        }
      } else { //Any other white bit that's not in the shape
        destination.loadPixels();
        destination.pixels[q] = color(255, 255, 255); //Remains white
        destination.updatePixels();
      }
      if (shapeFound == false && checkingCorner == true) { //If you haven't found a shape but are checking for corners
        int corner = 0; //Check if adyacent pixels are black
        if (red(check.pixels[q-1]) == 0) { //Check left
          corner ++;
        }
        //if (red(check.pixels[q+1]) == 0) {
        //  corner ++;
        //}
        if (q >= check.width) {
          if (red(check.pixels[q-check.width]) == 0) { //Check up
            corner ++;
          }
        }
        boolean lowerEdge = false;
        for (int w = floor(q/check.width); w < check.pixels.length/check.width; w ++) {
          if (red(check.pixels[q + ((w - floor(q/check.width))*check.width)]) == 0) {
            lowerEdge = true;
          }
        }
        if (corner >= 2 && lowerEdge == true) { //If at least two are black
          shapeFound = true; //Then it was a corner and you've found a shape
          inShape = true; //therefore you are inside the shape
          destination.loadPixels();
          destination.pixels[q] = color(255, 0, 0); //Mark the pixel!
          destination.updatePixels();
        } else { //If not, then you still haven't found the inside of the shape
          checkingCorner = false; //Stop checking this as a corner
        }
      }
    }
  }
}
void detectShape2(PImage check, PImage destination) {
  boolean shapeFound = false;
  boolean inShape = false;  
  boolean onEdge = false;
  boolean checkingCorner = false;
  IntList edgePixels = new IntList();
  IntList shapePixels = new IntList();
  for (int q = 0; q < check.pixels.length; q ++) {
    if (red(check.pixels[q]) == 0) { //If black
      if (shapeFound == false) { //If you haven't found a shape
        checkingCorner = true; //Start checking for corners
      } else { //If you have found a shape already
        onEdge = true; //That means you are on the edge
      }
      edgePixels.append(q); //Add edge pixel to list
    } else { //If white
      if (inShape == false && onEdge == true) { //If you were on an edge right before and were outside the shape
        onEdge = false; //Then you are not on the edge anymore
        inShape = true; //But rather within the shape
      } else if (inShape == true && onEdge == true) { //If you were on an edge before and within the shape
        onEdge = false; //Then you are ount of the edge now
        inShape = false; //And outside of the shape as well
      }
      if (shapeFound == true && inShape == true) { //If you've found a shape and you are within it
        shapePixels.append(q); //Add shape pixel to list

        int corner = 0; //Check if adyacent pixels are black
        if (red(check.pixels[q+1]) == 0) { //Check right
          corner ++;
        }
        //if (red(check.pixels[q+1]) == 0) {
        //  corner ++;
        //}
        if (q < check.pixels.length - check.width) { //Check down
          if (red(check.pixels[q+check.width]) == 0) {
            corner ++;
          }
        }
        if (corner >= 2) { //If at least two are black
          shapeFound = false; //Then it was a corner and you've found the end of the shape
          inShape = false; //therefore you are now outside the shape
        }
      } else { //Any other white bit that's not in the shape
        destination.loadPixels();
        destination.pixels[q] = color(255, 255, 255); //Remains white
        destination.updatePixels();
      }
      if (shapeFound == false && checkingCorner == true) { //If you haven't found a shape but are checking for corners
        int corner = 0; //Check if adyacent pixels are black
        if (red(check.pixels[q-1]) == 0) { //Check left
          corner ++;
        }
        //if (red(check.pixels[q+1]) == 0) {
        //  corner ++;
        //}
        if (q >= check.width) {
          if (red(check.pixels[q-check.width]) == 0) { //Check up
            corner ++;
          }
        }
        boolean lowerEdge = false;
        for (int w = floor(q/check.width); w < check.pixels.length/check.width; w ++) {
          if (red(check.pixels[q + ((w - floor(q/check.width))*check.width)]) == 0) {
            lowerEdge = true;
          }
        }
        if (corner >= 2 && lowerEdge == true) { //If at least two are black
          shapeFound = true; //Then it was a corner and you've found a shape
          inShape = true; //therefore you are inside the shape
          shapePixels.append(q); //Add shape pixel to the list!
        } else { //If not, then you still haven't found the inside of the shape
          checkingCorner = false; //Stop checking this as a corner
        }
      }
    }
  }
  noiseReduction(check, shapePixels, edgePixels);
  for (int q = 0; q < destination.pixels.length; q ++) {
    destination.loadPixels();
    destination.pixels[q] = color(255, 255, 255);
    destination.updatePixels();
  }
  for (int q = 0; q < shapePixels.size(); q ++) {
    destination.loadPixels();
    destination.pixels[shapePixels.get(q)] = color(255, 0, 0); //Mark the pixel!
    destination.updatePixels();
  }
  for (int q = 0; q < edgePixels.size(); q ++) {
    destination.loadPixels();
    destination.pixels[edgePixels.get(q)] = color(0, 0, 255);
    destination.updatePixels();
  }
}

void noiseReduction(PImage check, IntList shape, IntList edge) { //Kill "shapes" that aren't enclosed
  boolean edit = false;
  int counter = 0;
  int initialCount = shape.size();
  for (int q = shape.size() - 1; q >= 0; q --) { //For every "shape" pixel...
    int analyzedPixel = shape.get(q); //We look at one pixel at a time
    if ((analyzedPixel >= check.pixels.length - check.width) //If the pixel belongs to top or bottom row
      || (analyzedPixel <= check.width)) { //By default kill it
      shape.remove(q); //BYE
      counter ++;
      edit = true;
    } else { //If it's not on the borders, then the thing gets trickier...
      int stay = 0; //Let's count how many "valid" pixels are ther around our buddy
      for (int w = 0; w < shape.size(); w ++) { //Valid pixels include those who are also part of the shape
        //Check pixel left (get(q)-1), right (get(q)+1), up (get(q)-width), and down (get(q)+width)
        //And compare those to all pixels in the shape (get(w))
        if (shape.get(q)-1 == shape.get(w) || shape.get(q)+1 == shape.get(w) || 
          shape.get(q)-check.width == shape.get(w) || shape.get(q)+check.width == shape.get(w)) {
          stay ++; //A match means an adyacent valid pixel
        }
      }
      for (int w = 0; w < edge.size(); w ++) { //As well as those who are part of the edge
        //Ditto but now we check if any edge pixel is adyacent
        if (shape.get(q)-1 == edge.get(w) || shape.get(q)+1 == edge.get(w) || 
          shape.get(q)-check.width == edge.get(w) || shape.get(q)+check.width == edge.get(w)) {
          stay ++;
        }
      }
      if (stay < 3) { //Finally, unless the pixel has 4 valid pixels around it...
        shape.remove(q); //KILL IT
        counter ++;
        edit = true;
      }
    }
  }
  println(counter + " pixels ignored out of " + initialCount);
  if (edit == true) {
    //noiseReduction(check, shape, edge);
  } else {
    return;
  }
}
PImage smoothEdge(PImage check) { //To avoid random floating "edge" pixels
  IntList edge = new IntList(); //List to store edge pixels
  int counter = 0;
  for (int w = 0; w < check.pixels.length; w ++) { //Check black pixels for preliminary list
    if (red(check.pixels[w]) == 0) {
      edge.append(w);
    }
  }
  for (int q = edge.size() - 1; q >= 0; q --) { //For each pixel...
    //if ((edge.get(q) >= check.pixels.length - check.width) || 
    //  (edge.get(q) <= check.width) || (edge.get(q) % check.width == 0) || 
    //  (edge.get(q) % check.width == check.width - 1)) {
    //    edge.remove(q);
    //}
    int nextTo = 0; //Check how many edge pixels are there next to it
    for (int e = 0; e < edge.size(); e ++) {
      if ((edge.get(e) == edge.get(q) - 1) || (edge.get(e) == edge.get(q) + 1) || 
        (edge.get(e) == edge.get(q) - check.width) || 
        (edge.get(e) == edge.get(q) + check.width)) {
        nextTo ++;
      }
    }
    if (nextTo < 2) { //If there is less than two adyacent edge pixels...
      edge.remove(q); //Kill da pixel!
      counter ++;
    }
  }
  for (int t = 0; t < check.pixels.length; t ++) { //Now, we blank out the b/w image we had...
    check.loadPixels();
    check.pixels[t] = color(255, 255, 255);
    check.updatePixels();
  }
  for (int r = 0; r < edge.size(); r ++) { //And write the new black edges on top!
    check.loadPixels();
    check.pixels[edge.get(r)] = color(0, 0, 0);
    check.updatePixels();
  } 
  //for (int q = 0; q < check.pixels.length; q ++) {
  //  if (q < 20*check.width || q >= check.pixels.length
  //}
  println(counter + " pixels killed");
  return check; //And return the result!
}

PImage smoothEdge(PImage check, int iterations) {
  for (int q = 0; q < iterations; q ++) {
    check = smoothEdge(check);
  }
  return check;
}

 

 

Leave a Reply