Finding the Yellow Line

This was a project I did a while back, but it’s cool, and I haven’t written about it yet. Recently, several car manufacturers have started including a warning system to yell at you if you start drifting out of your lane.

I implemented a version of this that takes in video from the front of the car and continually tracks the geometric center of the lane marker. It doesn’t use OpenCV or any other vision libraries, just simple chromatic and density filtering and blob detection. Some of the code I’ll present here refer to data structures and memory allocations that are generated in other files. I think that stuff is kind of boring and it’s not too important for what I’m trying to show, that is, the general approach to this problem. The comments in the code should help clear up confusion, and I’ll highlight the important parts.

Chromatic Filtering

Chromatic filtering was performed in an integer HSI representation. It models RGB pixels with hue, saturation, and intensity. People often get confused between hue, saturation, and intensity, so here’s a good diagram that explains the differences:

Filtering an image based on chromaticity involves the hue and saturation values at every point. Depending on what you’re filtering for, thresholds must be empirically determined and set. It’s also a good idea to set limits on those parameters because low illumination in the image can cause erroneous hue and saturation values.

Here’s an example function that will replace an RGB value with an HSI value. It limits all fields to an 8 bit unsigned integer and replaces the vectored RGB pixel with the resulting HSI value. It returns that value in frame.

void RGB2HSIin(Pixel *P) {

unsigned char Max = P->R, Mid = P->G, Min = P->B, Delta;

if (Mid > Max) {
Mid ^= Max;
Max ^= Mid;
Mid ^= Max;
  }
if (Min > Mid) {
Min ^= Mid;
Mid ^= Min;
Min ^= Mid;
if (Mid > Max) {
Mid ^= Max;
Max ^= Mid;
Mid ^= Max;
  }
 }
Delta = Max - Min;
if (Max == 0) // black pixel
P->R = P->G = P->B = 0;
else if (Delta == 0) { // gray pixel
P->R = P->G = 0;
P->B = (Max + Mid + Min) / 3;
} else {
if (P->R == Max)
P->R = ((P->G - P->B) * 42 / Delta) + 42; // range from 0 - 84
else if (P->G == Max)
P->R = ((P->B - P->R) * 42 / Delta) + 127; // range from 85 - 169
else
P->R = ((P->R - P->G) * 42 / Delta) + 212; // range from 170 - 254
P->G = Delta * 255 / Max;
P->B = (Max + Mid + Min) / 3;
  }
}

You can do something similar to build a hue filter, but since it’s basically searching the image space for thresholds, I won’t reproduce the code here.

Here’s what that filter looks like when applied to an actual image. This is a single frame pulled from video footage taken from the dashboard of a car on the highway in Atlanta:

And here’s that same frame with a chromatic filter applied to look for the shade of yellow that the left lane marker is:

It’s clear that there are several parts of the image that are showing false positives for that shade of yellow. We’ll need to deal with that by running this through another filter.

Density Filtering

Density filtering essentially finds the areas of the image that have clusters of pixels we think are interesting. In this case, after chromatic filtering, only non-black pixels are interesting. We want to find the area image density. The following  function will two dimensionally scan an input frame containing salient regions with blackened pixels everywhere else. It returns a preallocated map of contiguous salient pixels. You’ll see mentions of wheels here, that’s a metaphor for rolling the routine across the image.

void Area_Image_Density(FrmBuf *FB, int *DensityMap, int WheelSize) {

   int Wheels[FB->Height];
   int Sums[FB->Height];
   int Vwheel[WheelSize];
   int Vsum, Vptr, HalfWheel, WheelOff, X, Y, I, Edge;
   // initialize one in left edge of window    
   Edge = 1 << (int) (WheelSize - 1);   
   // half wheel size               
   HalfWheel = WheelSize >> 1;
   // window offset = half window size x Width
   WheelOff = HalfWheel * (FB->Width + 1);            
   for (Y = 0; Y < FB->Height; Y++) {  // for all rows
      Wheels[Y] = 0; // clear the wheels
      Sums[Y] = 0;   // clear the sums
   }
   // for each column plus write out rows
   for (X = 0; X < FB->Width + HalfWheel; X++) {      
      // for all positions in vertical wheel
      for (Vptr = 0; Vptr < WheelSize; Vptr++)        
         Vwheel[Vptr] = 0; // clear vertical wheel
      Vsum = Vptr = 0; // clear vertical sum and ptr
      // for each row
      for (Y = 0; Y < FB->Height + HalfWheel; Y++) {  
       I = X + Y * FB->Width; // compute index
       Sums[Y] -= Wheels[Y] & 1; // remove outgoing pixel count
       Wheels[Y] >>= 1;  // shift window for new pixel
      Vsum -= Vwheel[Vptr];//removing outgoing vertical wheel count
         if (Y < FB->Height) { // while in image column
	    if (X < FB->Width ) // while in image row
               // if pixel contains non-blackened value
               if (FB->Frm[3*I] | FB->Frm[3*I+1] | FB->Frm[3*I+2]) { 
	      Sums[Y] += 1; // increment count
	      Wheels[Y] |= Edge; // and insert new edge pixel count
               }
        Vwheel[Vptr] = Sums[Y]; // insert row sum on vertical wheel
            Vsum += Sums[Y]; // and add to vertical sum
         }
         Vptr = (Vptr + 1) % WheelSize; // increment vertical ptr
         // wait until fully into row and column
         if (X >= HalfWheel && Y >= HalfWheel)        
	    // write current vertical sum to density map
            DensityMap[I - WheelOff] = Vsum;          
      }
   }
}

If you wanted to, you could reverse the scan order of this function to take advantage of spacial locality in the cache and eek a little more speed out.

Here’s what running a chromatically filtered image through this density filter looks like:

 

Now that we’ve identified areas of interest, we look for features.

Blob Detection

Blob detection is again mostly a game of thresholds. The worst part of this whole project was playing with thresholds until stuff worked well enough. To find a blob, we compare the density of the image at a certain point to a threshold. If it is touching another blob, we combine those two into a single blob. Here’s a function that returns a list of blobs found in a given density map. It annotates all density map values with pointers to the corresponding blob. After scanning the map and identifying blobs, it flattens the map by forwarding points until a non-forwarded blob is found, and it’s ID is used to replace the blob pointer in the blob map.

Blob *Blob_Finder_Map(int *DensityMap, int Width, int Height, int Bth) {

   Blob *Blobs = NULL, *RowBlob;
   Blob *ColBlobs[Width];
   int  X, Y, I = 0;

   for (X = 0; X < Width; X++) // for all columns
      ColBlobs[X] = NULL; // clear column blobs
   for (Y = 0; Y < Height; Y++) { // for each row
      RowBlob = NULL; // clear row blob
      for (X = 0; X < Width; X++) { // for each row offset   
         // if column blob is a fwd ptr
         if(ColBlobs[X] && ColBlobs[X]->FP) 
          // then reduce to the vectored blob
	  ColBlobs[X] = Reduce_FP(ColBlobs[X]); 
         if (DensityMap[I] >= Bth) { // if blob-worthy
	    if (RowBlob && ColBlobs[X]) // if two blobs touch
                // then merge column blob into rowblob
	       Merge_Blobs(ColBlobs[X], RowBlob, 0); 
            else if (ColBlobs[X]) // if only column blob exists
	       RowBlob = ColBlobs[X]; // copy to row blob
            else if (RowBlob == NULL) { // if no current blobs
	       RowBlob = New_Blob(X,Y, 0); // allocate a new blob
               RowBlob->Next = Blobs;//push new blob onto blob list
               Blobs = RowBlob; // update blob list
            }
	 Add_Position(RowBlob, X, Y);//add position to current blob
            ColBlobs[X] = RowBlob; // update column blob
            // store blob address in blob map
	    DensityMap[I] = (int) RowBlob; 
	 } else { // else leaving blob
	    // clear blob map for unblob-worthy positions
            DensityMap[I] = 0; 
             // clear current blob pointers
	    RowBlob = ColBlobs[X] = NULL; 
	 }
	 I += 1; // adjust map index
      }
   }
   Number_Blobs(Blobs); // set ID on non-forwarded blobs
   // transform blob pointers to base ID
   Mark_Blob_ID_Map(DensityMap, Width * Height); 
   // eliminate residual fwd ptrs from last row
   Blobs = Reap_FP_Blobs(Blobs); 
   // return blob list
   return (Blobs); 
}

If you visualize those blobs by drawing squares around them, here’s what you get:

You can see that we picked up a bunch of blobs. To detect the yellow lane marker, you want to adjust all of those thresholds in each filter so that the largest blob contains the lane marker. When I ran this code, I would occasionally get two large blobs covering the lane marker. There are a lot of ways to try to detect the marker from this point, and most of them involve having a flexible algorithm and taking advantage of preexisting knowledge. For instance, you know that the marker will always be approximately a straight line, and it’s  always going to be roughly diagonal as it moves towards the vanishing point. You also know that the line won’t do some things, like curve around backwards, spiral, get thicker, etc.

Here’s a video showing a bunch of different attempts at this problem. After the lane marker was detected, a line was drawn down the perceived middle of it. Each attempt is a different color. You can see that some are much better than others. Try to guess what parameter was wrong in different attempts.

2 Comments

Leave a Reply to admin Cancel reply