OpenCV Computer Vision Application Programming Cookbook Second Edition
上QQ阅读APP看书,第一时间看更新

Scanning an image with neighbor access

In image processing, it is common to have a processing function that computes a value at each pixel location based on the value of the neighboring pixels. When this neighborhood includes pixels of the previous and next lines, you then need to simultaneously scan several lines of the image. This recipe shows you how to do it.

Getting ready

To illustrate this recipe, we will apply a processing function that sharpens an image. It is based on the Laplacian operator (which will be discussed in Chapter 6, Filtering the Images). It is indeed a well-known result in image processing that if you subtract the Laplacian from an image, the image edges are amplified, thereby giving a sharper image.

This sharpened value is computed as follows:

sharpened_pixel= 5*current-left-right-up-down;

Here, left is the pixel that is immediately on the left-hand side of the current one, up is the corresponding one on the previous line, and so on.

How to do it...

This time, the processing cannot be accomplished in-place. Users need to provide an output image. The image scanning is done using three pointers, one for the current line, one for the line above, and another one for the line below. Also, since each pixel computation requires access to the neighbors, it is not possible to compute a value for the pixels of the first and last row of the image as well as the pixels of the first and last column. The loop can then be written as follows:

void sharpen(const cv::Mat &image, cv::Mat &result) {

   // allocate if necessary
  result.create(image.size(), image.type()); 
  int nchannels= image.channels(); // get number of channels

   // for all rows (except first and last)
  for (int j= 1; j<image.rows-1; j++) { 

    const uchar* previous= 
        image.ptr<const uchar>(j-1);     // previous row
    const uchar* current= 
        image.ptr<const uchar>(j);       // current row
    const uchar* next= 
        image.ptr<const uchar>(j+1);     // next row

    uchar* output= result.ptr<uchar>(j); // output row

    for (int i=nchannels; i<(image.cols-1)*nchannels; i++) {

       *output++= cv::saturate_cast<uchar>(
                  5*current[i]-current[i-nchannels]-
                  current[i+nchannels]-previous[i]-next[i]); 
    }
  }

  // Set the unprocessed pixels to 0
  result.row(0).setTo(cv::Scalar(0));
  result.row(result.rows-1).setTo(cv::Scalar(0));
  result.col(0).setTo(cv::Scalar(0));
  result.col(result.cols-1).setTo(cv::Scalar(0));
}

Note how we wrote the function such that it would work on both gray-level and color images. If we apply this function on a gray-level version of our test image, the following result is obtained:

How to do it...

How it works...

In order to access the neighboring pixels of the previous and next row, you must simply define additional pointers that are jointly incremented. You then access the pixels of these lines inside the scanning loop.

In the computation of the output pixel value, the cv::saturate_cast template function is called on the result of the operation. This is because it often happens that a mathematical expression applied on pixels leads to a result that goes outside the range of the permitted pixel values (that is, below 0 or over 255). The solution is then to bring the values back inside this 8-bit range. This is done by changing negative values to 0 and values over 255 to 255. This is exactly what the cv::saturate_cast<uchar> function is doing. In addition, if the input argument is a floating point number, then the result is rounded to the nearest integer. You can obviously use this function with other types in order to guarantee that the result will remain within the limits defined by this type.

Border pixels that cannot be processed because their neighborhood is not completely defined need to be handled separately. Here, we simply set them to 0. In other cases, it could be possible to perform a special computation for these pixels, but most of the time, there is no point in spending time to process these very few pixels. In our function, these border pixels are set to 0 using two special methods. The first one is row and its dual is col. They return a special cv::Mat instance composed of a single-line ROI (or a single-column ROI) as specified in a parameter (remember, we discussed region of interest in the previous chapter). No copy is made here because if the elements of this 1D matrix are modified, they will also be modified in the original image. This is what we do when the setTo method is called. This method assigns a value to all elements of a matrix. Take a look at the following statement:

   result.row(0).setTo(cv::Scalar(0));

The preceding statement assigns the value of 0 to all pixels of the first line of the result image. In the case of a 3-channel color image, you would use cv::Scalar(a,b,c) to specify the three values to be assigned to each channel of the pixel.

There's more...

When a computation is done over a pixel neighborhood, it is common to represent this with a kernel matrix. This kernel describes how the pixels involved in the computation are combined in order to obtain the desired result. For the sharpening filter used in this recipe, the kernel would be as follows:

Unless stated otherwise, the current pixel corresponds to the center of the kernel. The value in each cell of the kernels represents a factor that multiplies the corresponding pixel. The result of the application of the kernel on a pixel is then given by the sum of all these multiplications. The size of the kernel corresponds to the size of the neighborhood (here, 3 x 3). Using this representation, it can be seen that, as required by the sharpening filter, the four horizontal and vertical neighbors of the current pixel are multiplied by -1, while the current one is multiplied by 5. Applying a kernel to an image is more than a convenient representation; it is the basis for the concept of convolution in signal processing. The kernel defines a filter that is applied to the image.

Since filtering is a common operation in image processing, OpenCV has defined a special function that performs this task: the cv::filter2D function. To use this, you just need to define a kernel (in the form of a matrix). The function is then called with the image and the kernel, and it returns the filtered image. Using this function, it is therefore, easy to redefine our sharpening function as follows:

void sharpen2D(const cv::Mat &image, cv::Mat &result) {

   // Construct kernel (all entries initialized to 0)
   cv::Mat kernel(3,3,CV_32F,cv::Scalar(0));
   // assigns kernel values
   kernel.at<float>(1,1)= 5.0;
   kernel.at<float>(0,1)= -1.0;
   kernel.at<float>(2,1)= -1.0;
   kernel.at<float>(1,0)= -1.0;
   kernel.at<float>(1,2)= -1.0;

   //filter the image
   cv::filter2D(image,result,image.depth(),kernel);
}

This implementation produces exactly the same result as the previous one (and with the same efficiency). If you input a color image, then the same kernel will be applied to all three channels. Note that it is particularly advantageous to use the filter2D function with a large kernel, as it uses, in this case, a more efficient algorithm.

See also

  • Chapter 6, Filtering the Images, provides more explanations on the concept of image filtering