2

I have segmented and binary image of biological cells and using openCV I have extracted the areas and perimeters of the contours. I am trying to label and color with a colormap each cell according to a parameter q=perimeter/Sqrt(area) but have no idea where to even start. Essentially each cell will have a unique color according to this value.

Any help would be greatly appreciated! Here is what I have so far:

> #specify folders
filelocat = '/Users/kate/Desktop/SegmenterTest3/SegmentedCells/'

#process image
img = cv2.imread(str(filelocat) + 'Seg3.png')
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)[1]
kernel = np.ones((20,20), np.uint8)
closing = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
#inverts image so that the objects are white (for analysis)
imagem = cv2.bitwise_not(closing)

#Find contours
cnts = cv2.findContours(imagem.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)

#calculate moments and extract cell shape info
moment_dict = {}
for index, cnt in enumerate(cnts):
    moment_dict[index] = cv2.moments(cnt)
    
obj_properties = {}
for index, (key, obj_moments) in enumerate(moment_dict.items()):
    if obj_moments['m00'] > 1000 and obj_moments['m00'] < 20000:
        area = obj_moments['m00']
        cx = obj_moments['m10'] / obj_moments['m00']
        cy = obj_moments['m01'] / obj_moments['m00']
        peri = cv2.arcLength(cnts[index], True)
        q = (peri/(math.sqrt(area)))
        props = {}
        props['q']=q
        props['peri']=peri
        props['area']=area
        props['cx']=cx
        props['cy']=cy
        obj_properties[key] = props

Thank you for your help!!

helpkate1991
  • 35
  • 1
  • 7
  • Can the color be random or must it be a unique list of colors? – fmw42 Jul 24 '20 at 01:44
  • I would ideally like to use a color map so that it represents the gradients of values, but any color map would really do! – helpkate1991 Jul 24 '20 at 02:08
  • To use a specific color map, you would have to have as many defined colors as you have different areas, which has to be known when you draw your contours. – fmw42 Jul 24 '20 at 02:09
  • Search Google for `python opencv draw random colors`. You will find answers such as https://stackoverflow.com/questions/28999287/generate-random-colors-rgb and https://stackoverflow.com/questions/19400376/how-to-draw-circles-with-random-colors-in-opencv/19401384 for drawing with random colors. – fmw42 Jul 24 '20 at 02:12

1 Answers1

3

To solve this problem, you need to collect all the q's so that you can scale them according to the observed range of q's. You can do that with a list comprehension like so:

all_the_q = [v['q'] for k, v in obj_properties.items()]

You also need to pick some colormap. I leave that as an exercise for the reader based on suggestions in the previous comments. For a quick idea, you can see a preliminary result just by scaling your q's to 8 bits of RGB.

See the complete code below. Note that index in your moment_dict is the key in your obj_properties dictionary, so the whole enumerate construct is unnecessary. I took the liberty of dropping enumerate completely. Your filtering loop picks up the correct contour index anyway. After you select your contours based on your criteria, collect all the q's and calculate their min/max/range. Then use those to scale individual q's to whatever scale you need. In my example below, I scale it to 8-bit values of the green component. You can follow that pattern for the red and blue as you wish.

Note that in this image most of the q's are in the 4.0 - 4.25 range, with a few outliers at 5.50 (plot a histogram to see that distribution). That skews the color map, so most cells will be colored with a very similar-looking color. However, I hope this helps to get you started. I suggest applying a logarithmic function to the q's in order to "spread out" the lower end of their distribution visually.

import matplotlib.pyplot as plt
import math
import os
import cv2
import imutils
import numpy as np
# specify folders
filelocat = '/Users/kate/Desktop/SegmenterTest3/SegmentedCells/'

# process image
img = cv2.imread(os.path.join(filelocat, 'Seg3.png'))
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)[1]
kernel = np.ones((20, 20), np.uint8)
closing = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
# inverts image so that the objects are white (for analysis)
imagem = cv2.bitwise_not(closing)

# Find contours
cnts = cv2.findContours(imagem.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)

# calculate moments and extract cell shape info
moment_dict = {}
for index, cnt in enumerate(cnts):
    moment_dict[index] = cv2.moments(cnt)

obj_properties = {}
for index, obj_moments in moment_dict.items():
    if obj_moments['m00'] > 1000 and obj_moments['m00'] < 20000:
        area = obj_moments['m00']
        cx = obj_moments['m10'] / obj_moments['m00']
        cy = obj_moments['m01'] / obj_moments['m00']
        peri = cv2.arcLength(cnts[index], True)
        q = (peri/(math.sqrt(area)))
        props = {}
        props['q'] = q
        props['peri'] = peri
        props['area'] = area
        props['cx'] = cx
        props['cy'] = cy
        obj_properties[index] = props

all_the_q = [v['q'] for k, v in obj_properties.items()]
min_q = min(all_the_q)
max_q = max(all_the_q)
range_q = max_q - min_q

# colormapping of q scalars to BGR values
cmap = plt.cm.get_cmap('terrain')
for index, prop in obj_properties.items():
    v = (prop['q'] - min_q) / range_q
    r, g, b, a = [int(x) for x in cmap(v, bytes=True)]
    cv2.drawContours(img, cnts, index, (b, g, r), -1)

cv2.imwrite('colored.png', img)
cv2.imshow('Biocells', img)
cv2.waitKey(10000)
Basil
  • 659
  • 4
  • 11
  • Wow this is great! Thank you! I've managed to apply this to the cells and it labels them really nicely. Now I'm trying to figure out how to remove the outer cells that get cut off in the image. Wish me luck! – helpkate1991 Jul 24 '20 at 15:30
  • [This](https://stackoverflow.com/questions/25408393/getting-individual-colors-from-a-color-map-in-matplotlib) will also be helpful if you have matplotlib installed. You will just need to do a little more work to scale the RGB values, unpack them, and rearrange them in BGR order for the call to `cv2.drawContours`. – Basil Jul 24 '20 at 17:29
  • Hi Again, this definitely works. I'm still working on how to adjust the color scheme/q values with a logarithmic function. But I've encountered a new problem. To remove the boundary, I've added a border and filled in the holes to get rid of the contours that were previously cut off by the edge of the image. Now, when I add the contour colors, the image is shifted from the original, or when I analyze the new image with the border, I lose all the colors. – helpkate1991 Jul 24 '20 at 20:54
  • Part 1 of new code: `#add a border to image bordersize = 10 new_img = cv2.copyMakeBorder(image, top=bordersize, bottom=bordersize, left=bordersize, right=bordersize, borderType=cv2.BORDER_CONSTANT, value=[0, 0, 0]) thresh = cv2.adaptiveThreshold(new_img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY,11,2) #find contours of the image cnts = cv2.findContours(thresh.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts)` – helpkate1991 Jul 24 '20 at 20:58
  • Part 2 of new code: `moment_dict = {} for index, cnt in enumerate(cnts): moment_dict[index] = cv2.moments(cnt) obj_properties = {}` – helpkate1991 Jul 24 '20 at 21:00
  • Part 3: `for key, obj_moments in moment_dict.items(): if obj_moments['m00'] >= 2000 and obj_moments['m00'] <= 25000: area = obj_moments['m00'] cx = obj_moments['m10'] / obj_moments['m00'] cy = obj_moments['m01'] / obj_moments['m00'] peri = cv2.arcLength(cnts[key], True) q = (peri/(math.sqrt(area))) props = {} props['q']=q props['peri']=peri props['area']=area props['cx']=cx props['cy']=cy obj_properties[key] = props` – helpkate1991 Jul 24 '20 at 21:00
  • `q = [] for index, (key, properties) in enumerate(obj_properties.items()): if (properties['area']) <= 2500000 and (properties['area']) >= 1000: if (properties['q']) <= 5.6: q.append(properties['q']) min_q = min(q) max_q = max(q) range_q = max_q - min_q plt.hist(q, bins=20, alpha=0.5) for index, prop in obj_properties.items(): q = prop['q'] if area >= 3000 and area <=25000: cv2.drawContours(img, cnts, index, (150, int((q - min_q) / range_q * 255), 100), -1)` – helpkate1991 Jul 24 '20 at 21:00
  • You're placing a 10-pixel border around the whole image. You may want to place it around the graphed window of interest only. – Basil Jul 24 '20 at 22:18
  • I updated the answer with a better-looking colormap. – Basil Jul 24 '20 at 22:26
  • If it's the right answer to your posted question, please mark it as such. – Basil Jul 25 '20 at 14:30