3

I am brand new to Image Processing and have gotten stuck on a problem. I have this image:

enter image description here

My goal is to turn it into a matrix with a 1 if there is red inside the cell and a 0 if there is not.

So this would be

10000000000
10001000000
10001000000
10001000000
11111111111
10000000101
10111111101
etc...

So I have my code where it can extract contours and uses approxPolyDP to determine the (x,y) coordinates of the 4 corner edges.

Contours shown in white

Now I need to figure out how to determine if a certain color (red) is within each contour.

Here is some of my code: hopefully someone can help!

def extract_cells(grid):
    #convert to gray
    image_gray = cv2.cvtColor(grid, cv2.COLOR_BGR2GRAY)
    #creates a binary image from the gray scale image to use as input for findContours()
    #thresh = cv2.adaptiveThreshold(image_gray,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY_INV,11,15)

    #Find countors
    tempimg, contours, hierarchy = cv2.findContours(image_gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

    #draw all countours
    count = 0
    max_size = 0
    matrix = [] 
    new_contours = []
    grid_contour = 0
    grid_contour_row = None
    grid_contour_column = None
    for each in enumerate(contours):

        #used to find the midpoint of each cell
        M = cv2.moments(contours[count])
        row = int(M['m10']/M['m00'])
        column = int(M['m01']/M['m00'])

        #find biggest box (this is the grid itself, so needs to be removed since it is not a cell)
        size = cv2.contourArea(contours[count])
        if (size > max_size):
            new_contours.append(contours[grid_contour])
            #put a marker in each cell for testing
            #if (grid_contour_row != None and grid_contour_column != None):
                #cv2.putText(grid, "0", (grid_contour_row, grid_contour_column), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,255))
            grid_contour = count
            grid_contour_row = row
            grid_contour_column = column
        else:
            new_contours.append(contours[count])
            #put a marker in each cell for testing
            #cv2.putText(grid, "0", (row, column), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,255))

        #matrix = create_matrix(matrix,count)
        count += 1

    #draw white lines showing contours
    cv2.drawContours(grid, new_contours, -1, (255,255,255))

    #approx contains x,y coordinates for the 4 corners of the cell
    approx = cv2.approxPolyDP(contours[0],0.01*cv2.arcLength(contours[0],True),True)

    cv2.imshow("test", grid)
    cv2.waitKey(0)
    return new_contours, approx


def identify_colors(image, *colors):
    colorlist = []
    #Add RGB values for each color specified when the function was called
    #to the list colorlist

    if "blue" in colors:
        colorlist.append(([115,0,0], [255,100,100]))
    if "white" in colors:
        colorlist.append(([215, 215, 215], [255, 255, 255]))
    if "red" in colors:
        colorlist.append(([0,0,100], [100,100,255]))
    if "green" in colors:
        colorlist.append(([0,115,0], [100,255,100]))

    #loop over the colorlist
    for (lower, upper) in colorlist:
        # create NumPy arrays from the colorlist
        lower = np.array(lower, dtype = "uint8")
        upper = np.array(upper, dtype = "uint8")

        #econverts image to b/w with white being anything in the BGR value range
        mask = cv2.inRange(image, lower, upper)
        #converts that specified range back to its orginal color
        output = cv2.bitwise_and(image, image, mask = mask)

        #show the photos side by side
        #cv2.imshow("images", np.hstack([image, output]))
        #cv2.waitKey(0)

    return output
3
  • So, in my understanding, you have each "white box" edge coordinates, right? You could loop into each white box you found and, for each box, you look into the original image (using the coordinates of the box) for a red pixel. If you find one, mark it as 1. You might even use the white boxes coordinates to crop (copy) the original image, by creating a new Mat - then you could just look into the cropped Mat for a red pixel... Commented Apr 8, 2016 at 19:50
  • This is what I was thinking too. However I'm not sure how to loop through the image using the coordinates I have. Do you think you could give an example? Sorry, I'm brand new to this :) Commented Apr 8, 2016 at 19:57
  • Using the edge coordinats of a small box, you have to extract a subimage /region of interes /ROI, simply by adressing this in the image matrix. In that subimage, you can look for a colour (using cv2.inrange). Have a look at the tutorial at docs.opencv.org/3.1.0/d6/d00/tutorial_py_root.html#gsc.tab=0 Commented Apr 8, 2016 at 20:18

3 Answers 3

3

It's much simpler if you use scipy.ndimage.label():

from scipy import ndimage
import cv2
import numpy as np
import pandas as pd

img = cv2.imread("image.png")

blue = np.array([200, 70, 60])
red = np.array([30, 20, 220])

isblue = cv2.inRange(img, blue, blue+20)
isred = cv2.inRange(img, red, red+20) > 0

labels, count = ndimage.label(~isblue)

loc = np.where(labels >= 2) #label 1 is the border

# to get the location, we need to sort the block along yaxis and xaxis
df = pd.DataFrame({"y":loc[0], "x":loc[1], "label":labels[loc], "isred":isred[loc]})

grid = df.groupby("label").mean().sort_values("y")

def f(df):
    return df.sort_values("x").reset_index(drop=True)
res = grid.groupby((grid.y.diff().fillna(0) > 10).cumsum()).apply(f)

print((res.isred.unstack(1) > 0).astype(np.uint8))

the output:

    0   1   2   3   4   5   6   7   8   9   10
y                                             
0    1   0   0   0   0   0   0   0   0   0   0
1    1   0   0   0   1   0   0   0   0   0   0
2    1   0   0   0   1   0   0   0   0   0   0
3    1   0   0   0   1   0   0   0   0   0   0
4    1   1   1   1   1   1   1   1   1   1   1
5    1   0   0   0   0   0   0   1   1   1   1
6    1   0   1   1   1   1   1   1   1   0   1
7    0   0   0   0   0   0   0   0   0   0   1
8    0   0   0   0   0   0   0   0   0   0   1
9    0   0   0   0   0   0   0   0   0   0   1
10   0   0   0   0   0   0   0   0   0   0   1
Sign up to request clarification or add additional context in comments.

Comments

0

You can proceed as follows:

  • create masks for the red and blue colour
  • from blue mask, create contours four each single small square
  • from red mask, look wether red pixels are in a small square

Result looks like this:

enter image description here

Code with comments attached.

I have just seen that you need to generate the matrix from the list which gets printed, for each square the list contains 0 or 1 and the x/y coordinates of the centroid of that square. Unfortunately, the contours do not sort in the "normal" order due to small shape irregularities, so you still have to do some sorting of coordinates on your own, sorry for this.

If you want to avoid this you could also generate mask2 by doing a flood fill with a seed which is generated from some regular grid, and loop through that grid.

import cv2
import numpy as np
img = cv2.imread('image.png')


# Define range of blue color 
lower_limit = np.array([204,72,63])
upper_limit = np.array([204,72,63])

# Generate mask for the blue pixels 
blue_colour_mask = cv2.inRange(img, lower_limit, upper_limit)

# Define range of red color 
lower_limit = np.array([36,28,237])
upper_limit = np.array([36,28,237])

# Generate mask for the red  pixels 
red_colour_mask = cv2.inRange(img, lower_limit, upper_limit)


# Remove outer black area
flooded = img.copy()
x = 5
y = 5
flooded = blue_colour_mask.copy()
h, w = blue_colour_mask.shape[:2]
mask = np.zeros((h+2, w+2), np.uint8)
mask[:] = 0
cv2.floodFill(flooded,mask,(x,y),(255,)*3, (40,)*3, (40,)*3, 4 )


# Loop through each single small rectange (contour # from 1 to 121, 0 ist image border)
_, contours, hierarchy = cv2.findContours( flooded.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
h, w = img.shape[:2]

result= np.zeros((h, w, 3), np.uint8)
result[:] = 0

mask2 = np.zeros((h, w), np.uint8)
list =[]

for i in range(1,122):
    mask2[:] = 0
    cv2.drawContours( mask2, contours, i, (255,255,255), cv2.FILLED)
    mask3= cv2.bitwise_and(mask2, red_colour_mask)
    pixnumber= cv2.countNonZero(mask3)

    if pixnumber == 0:
        cv2.drawContours( result, contours, i, (255,255,255), cv2.FILLED)
        moments = cv2.moments(contours[i])
        cx = int(moments['m10']/moments['m00'])
        cy = int(moments['m01']/moments['m00'])
        print 0, i, cx, cy
        list.append([0,cx, cy])
    else:
        cv2.drawContours( result, contours, i, (0,0,255), cv2.FILLED)
        moments = cv2.moments(contours[i])
        cx = int(moments['m10']/moments['m00'])
        cy = int(moments['m01']/moments['m00'])     
        print 1, i, cx, cy
        list.append([0,cx, cy])

cv2.imshow('Result',result)

cv2.waitKey(0)

print list


cv2.imshow('image',img)
cv2.imshow('Blue pixel mask',blue_colour_mask)
cv2.imshow('Red pixel mask',red_colour_mask)
cv2.imshow('Result',result)

cv2.waitKey(0)

Comments

0
import cv2
import numpy as np


def centroid(contour):
    x,y,w,h = cv2.boundingRect(contour)
    return (y+h/2.0, x+w/2.0)

def contains_red(red_mask, tile):
    tile_area = np.zeros_like(red_mask)
    cv2.drawContours(tile_area, [tile[1]], 0, 255, -1)
    red_tile_area = cv2.bitwise_and(tile_area, red_mask)
    return (cv2.countNonZero(red_tile_area) > 0)

def get_transform(grid_size, grid_contour):
    x,y,w,h = cv2.boundingRect(grid_contour)
    tile_w = float(w) / (grid_size[0])
    tile_h = float(h)/ (grid_size[1])
    return ((-y - tile_h/2, -x - tile_w/2), (1/tile_h, 1/tile_w))


img = cv2.imread("input.png")

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)

cv2.imwrite("out_1.png", np.hstack([h, s, v]))

# Saturation mask to get rid of black
s_mask = cv2.threshold(s, 10, 255, cv2.THRESH_BINARY)[1]

# Pick out blue area
blue_range = [110, 130]
blue_mask = cv2.inRange(h, blue_range[0], blue_range[1])
blue_mask = cv2.bitwise_and(blue_mask, s_mask)


# Pick out blue area
red_range = [[170, 180], [0,10]]
red_mask = cv2.bitwise_or(
    cv2.inRange(h, red_range[0][0], red_range[0][1])
    , cv2.inRange(h, red_range[1][0], red_range[1][1]))
red_mask = cv2.bitwise_and(red_mask, s_mask)

cv2.imwrite("out_2.png", np.hstack([s_mask, blue_mask, red_mask]))


kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# Remove noise
blue_mask = cv2.morphologyEx(blue_mask, cv2.MORPH_OPEN, kernel)
# Fill any small holes
blue_mask = cv2.morphologyEx(blue_mask, cv2.MORPH_CLOSE, kernel)


# Find outer contour, and fill area outside
cnt_grid = cv2.findContours(blue_mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0]
assert(len(cnt_grid) == 1)
grid_area = np.zeros_like(blue_mask)
cv2.drawContours(grid_area, cnt_grid, 0, 255, -1)
grid_tiles = cv2.bitwise_and(cv2.bitwise_not(blue_mask), grid_area)

cv2.imwrite("out_3.png", np.hstack([blue_mask, grid_area, grid_tiles]))

# Find contours of our tiles
cnt_tiles = cv2.findContours(grid_tiles.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0]

# Find scaling parameters
offset, scale = get_transform((11, 11), cnt_grid[0])

tiles = [[centroid(contour), contour, False] for contour in cnt_tiles]
for tile in tiles:
    # Rescale centroid
    tile[0] = (
        int(round((tile[0][0] + offset[0]) * scale[0]))
        , int(round((tile[0][1] + offset[1]) * scale[1]))
    )
    tile[2] = contains_red(red_mask, tile)

# Sort the tiles
tiles = sorted(tiles, key=lambda x: x[0], reverse=False)

# Extract the results
result = np.array([int(t[2]) for t in tiles])

print result.reshape(11,11)

We first split the image into HSV colour space.

Hue, Saturation, Value channels:

enter image description here

Since black can end up any hue, we should ignore unsaturated pixels.

Then we can extract blue and red masks using in range, and bitwise and with saturation mask.

Saturation mask, blue and red mask:

enter image description here

We apply morphological opening and closing to get rid of noise.

We identify the area of the grid itself by getting an outer contour.

Then we identify tile areas by getting outer contours on inverted image.

Blue mask, Grid area mask, Tile mask:

enter image description here

Next we calculate centroids of our tiles.

We assume that tile grid size is constant or entered by user. Based on grid bounding box coordinates, we can determine scaling and offset to resample our centroids to range 0..10.

Then we can sort the tiles by centroid coordinates, and reshape them to get the result.

Result:

[[1 0 0 0 0 0 0 0 0 0 0]
 [1 0 0 0 1 0 0 0 0 0 0]
 [1 0 0 0 1 0 0 0 0 0 0]
 [1 0 0 0 1 0 0 0 0 0 0]
 [1 1 1 1 1 1 1 1 1 1 1]
 [1 0 0 0 0 0 0 0 1 1 1]
 [1 0 1 1 1 1 1 1 1 0 1]
 [0 0 0 0 0 0 0 0 0 0 1]
 [0 0 0 0 0 0 0 0 0 0 1]
 [0 0 0 0 0 0 0 0 0 0 1]
 [0 0 0 0 0 0 0 0 0 0 1]]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.