0

I am writing code in python 2.7.12 using opencv '2.4.9.1'.
I have a 2d numpy array containing values in range [0,255].
My aim is to find largest region containing value in range[x,y]
I found How to use python OpenCV to find largest connected component in a single channel image that matches a specific value? as pretty well-explained . Only, the catch is - it is meant for opencv 3 .

I can try to write a function of this type
[pseudo code]

def get_component(x,y,list):
  append x,y to list
  visited[x][y]=1
 if(x+1<m && visited[x+1][y]==0)
  get_component(x+1,y,list)
 if(y+1<n && visited[x][y+1]==0)
  get_component(x,y+1,list)
 if(x+1<m)&&(y+1<n)&&visited[x+1][y+1]==0
  get_component(x+1,y+1,list)
 return

MAIN
biggest_component = NULL
biggest_component_size = 0
low = lowest_value_in_user_input_range
high = highest_value_in_user_input_range
matrix a = gray image of size mxn
matrix visited = all value '0' of size mxn
for x in range(m):
 for y in range(n):
  list=NULL
  if(a[x][y]>=low) && (a[x][y]<=high) && visited[x][y]==1:
   get_component(x,y,list)
   if (list.size>biggest_component_size)
    biggest_component = list
Get maximum x , maximum y , min x and min y from above list containing coordinates of every point of largest component to make rectangle R .
Mission accomplished !

[/pseudo code]

Such an approach will not be efficient, I think.
Can you suggest functions for doing the same with my setup ?
Thanks.

user1371666
  • 445
  • 1
  • 5
  • 18

1 Answers1

2

Happy to see my answer linked! Indeed, connectedComponentsWithStats() and even connectedComponents() are OpenCV 3+ functions, so you can't use them. Instead, the easy thing to do is just use findContours().

You can calculate moments() of each contour, and included in the moments is the area of the contour.

Important note: The OpenCV function findContours() uses 8-way connectivity, not 4-way (i.e. it also checks diagonal connectivity, not just up, down, left, right). If you need 4-way, you'd need to use a different approach. Let me know if that's the case and I can update..

In the spirit of the other post, here's the general approach:

  1. Binarize your image with the thresholds you're interested in.
  2. Run cv2.findContours() to get the contour of each distinct component in the image.
  3. For each contour, calculate the cv2.moments() of the contour and keep the maximum area contour (m00 in the dict returned from moments() is the area of the contour).
  4. Either keep the contour as a list of points if that's what you need, otherwise draw them on a new blank image if you want it as a mask.

I lack creativity today, so you get the cameraman as our example image as you didn't provide one.

import cv2
import numpy as np

img = cv2.imread('cameraman.png', cv2.IMREAD_GRAYSCALE)

Cameraman

Now, let's binarize to get some separated blobs:

bin_img = cv2.inRange(img, 50, 80)

Binary Cameraman

Now let's find the contours.

contours = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0]

# For OpenCV 3+ use: 
# contours = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[1]

Now for the main bit; looping through the contours and finding the largest one:

max_area = 0
max_contour_index = 0
for i, contour in enumerate(contours):
    contour_area = cv2.moments(contour)['m00']
    if contour_area > max_area:
        max_area = contour_area
        max_contour_index = i

So now we have an index max_contour_index of the largest contour by area, so you can access the largest contour directly just by doing contours[max_contour_index]. You could of course just sort the contours list by the contour area and grab the first (or last, depending on sort order). If you want to make a mask of the one component, you can use

cv2.drawContours(new_blank_image, contours, max_contour_index, color=255, thickness=-1)

Note the -1 will fill the contour as opposed to outlining it. Here's an example drawing the contour over the original image:

Labeled Cameraman

Looks about right.

All in one function:

def largest_component_mask(bin_img):
    """Finds the largest component in a binary image and returns the component as a mask."""

    contours = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0]
    # should be [1] if OpenCV 3+

    max_area = 0
    max_contour_index = 0
    for i, contour in enumerate(contours):
        contour_area = cv2.moments(contour)['m00']
        if contour_area > max_area:
            max_area = contour_area
            max_contour_index = i

    labeled_img = np.zeros(bin_img.shape, dtype=np.uint8)
    cv2.drawContours(labeled_img, contours, max_contour_index, color=255, thickness=-1)

    return labeled_img
alkasm
  • 22,094
  • 5
  • 78
  • 94
  • i will run this and report back . BTW , really lucid . – user1371666 Apr 03 '18 at 14:51
  • can i draw this contour over a color image (actually, gray scale image was derived from it) ? Also, i did print contours[max_contour_index].shape and got (924,1,2) . It is actually x,y coordinates of 924 points. How can I extract xmin,xmax,ymin,ymax as it will denote rectangular mask whose top left and bottom right coordinate are of my interest ? – user1371666 Apr 03 '18 at 17:14
  • 1
    Sure, but you'll need a 3 channel color image to draw over. I read in the image as grayscale in the first lines of code above, so you'd want to read in a color image instead so you can use `drawContours()` with a `color=(b, g, r)` tuple for what color you'd like to draw. – alkasm Apr 03 '18 at 17:16
  • got it . Now , i have to find top left and bottom right coordinates from the mask. – user1371666 Apr 03 '18 at 17:48
  • that is turning out to be a bit complicated. i tried with following example - >>a = [[[239, 128]], [[240, 129]], [[239, 130]], [[240, 129]]] >>np.asarray(a).shape (4,1,2) >>np.asarray(a).argmax(axis=2) this seems to give 0 or 1 for each pixel depending on comparison between its x and y coordinate. – user1371666 Apr 03 '18 at 18:07
  • I don't understand what you're trying to do. Could you edit the OP with an example image and exactly what you need to achieve? – alkasm Apr 03 '18 at 18:10
  • okay . the aim is to represent contour as rectangular region . so its top left and bottom right coordinate will be passed to another function which will draw rectangle on the image. I figured out way to get minimum and maximum x and y coordinate among all pixels of largest component. it requires calculating - xmax,ymax=contours[max_contour_index].reshape(,2).argmax(axis=0). so =contours[max_contour_index].shape[0] . I will try it out . – user1371666 Apr 03 '18 at 18:19
  • 1
    @user1371666 you can also simply use `boundingRect()` with each contour to find the bounding rectangle of the contour. See more here: https://docs.opencv.org/3.1.0/dd/d49/tutorial_py_contour_features.html – alkasm Apr 03 '18 at 18:45
  • sir , in above code is it possible that if I get a mask having just 1 pixel (say my input image has just 1 white and i give that 255 to in range) and then when i will try to find largest component , i get error number_of_points_in_contour=contours[max_contour_index].shape[0] IndexError: list index out of range – user1371666 Apr 05 '18 at 00:08
  • 1
    @user1371666 I can't reproduce your error, possibly because I'm using OpenCV 3. When I run this on an image with a single white point, it works as expected. Can you edit your original post to show, in the single point case, what your `contours` list looks like? Also please print out all the other variables -- what `contour_area` is when you go through and what `max_contour_index` is and print the full output of `contours`. – alkasm Apr 05 '18 at 17:37
  • okay, sir. I had this in my code h_bin_img = cv2.inRange(converted, lower, upper) if not np.any(h_bin_img): print("this image has no face") else: contours = cv2.findContours(h_bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0] I will print those cases where inrange gave result but contour being any empty list gave exception . Currently I simply excluded such cases using if not contours: print 'empty contour' break – user1371666 Apr 06 '18 at 04:53