2

I'm trying to calculate the distance between two pixels, but in a specific way. I need to know the thickness of the red line in the image, so my idea was to go through the image by columns, find the coordinates of the two edge points and calculate the distance between them. Do this for the two lines, both top and bottom. Do this for each column and then calculate the average. I should also do a conversion from pixels to real scale.

This is my code for now:

# Make numpy array from image
npimage = np.array(image)

# Describe what a single red pixel looks like
red = np.array([255, 0, 0], dtype=np.uint8)   

firs_point = 0
first_find = False
for i in range(image.width):
    column = npimage[:,i]
    for row in column:
        comparison = row == red
        equal_arrays = comparison.all()
        if equal_arrays == True and first_find == False:
                first_x_coord = i
                first_find = True

I can't get the coordinates. Can someone help me please? Of course, if there are more optimal ways to calculate it, I will be happy to accept proposals. I am very new! Thank you very much!

Georgia
  • 109
  • 1
  • 6
  • You want this to be automatic? You want to know the max thickness of the red line, le min or the average? – Panda50 Feb 24 '21 at 12:30
  • Yes, my idea is this to make this automatic! And I was thinking of the average as I think it's the most representative. – Georgia Feb 24 '21 at 12:36
  • Why not try thresholding on the R channel? – YScharf Feb 24 '21 at 12:42
  • yes, I was thinking of a binarisation with R as white and the rest as black, but I don't know how that can take out the width:/ – Georgia Feb 24 '21 at 12:47

4 Answers4

2

After properly masking all red pixels, you can calculate the cumulative sum per each column in that mask:

Cumulative sum

Below each red line, you have a large area with a constant value: Below the first red line, it's the thickness of that red line. Below the second red line, it's the cumulative thickness of both red lines, and so on (if there would be even more red lines).

So, now, for each column, calculate the histogram from the cumulative sum, and filter out these peaks; leaving out the 0 in the histogram, that'd be the large black area at the top. Per column, you get the above mentioned (cumulative) thickness values for all red lines. The remainder is to extract the actual, single thickness values, and calculate the mean over all those.

Here's my code:

import cv2
import numpy as np

# Read image
img = cv2.imread('Dc4zq.png')

# Mask RGB pure red
mask = (img == [0, 0, 255]).all(axis=2)

# We check for two lines
n = 2

# Cumulative sum for each column
cs = np.cumsum(mask, axis=0)

# Thickness values for each column
tvs = np.zeros((n, img.shape[1]))
for c in range(img.shape[1]):

    # Calculate histogram of cumulative sum for a column
    hist = np.histogram(cs[:, c], bins=np.arange(img.shape[1]+1))

    # Get n highest histogram values
    # These are the single thickness values for a column
    tv = np.sort(np.argsort(hist[0][1:])[::-1][0:n]+1)
    tv[1:] -= tv[:-1]
    tvs[:, c] = tv

# Get mean thickness value
mtv = np.mean(tvs.flatten())
print('Mean thickness value:', mtv)

The final result is:

Mean thickness value: 18.92982456140351
----------------------------------------
System information
----------------------------------------
Platform:      Windows-10-10.0.16299-SP0
Python:        3.9.1
NumPy:         1.20.1
OpenCV:        4.5.1
----------------------------------------

EDIT: I'll provide some more details on the "NumPy magic" involved.

# Calculate the histogram of the cumulative sum for a single column
hist = np.histogram(cs[:, c], bins=np.arange(img.shape[1] + 1))

Here, bins represent the intervals for the histogram, i.e. [0, 1], [1, 2], and so on. To also get the last interval [569, 570], you need to use img.shape[1] + 1 in the np.arange call, because the right limit is not included in np.arange.

# Get the actual histogram starting from bin 1
hist = hist[0][1:]

In general, np.histogram returns a tuple, where the first element is the actual histogram. We extract that, and only look at all bins larger 0 (remember, the large black area).

Now, let's disassemble this code:

tv = np.sort(np.argsort(hist[0][1:])[::-1][0:n]+1)

This line can be rewritten as:

# Get the actual histogram starting from bin 1
hist = hist[0][1:]

# Get indices of sorted histogram; these are the actual bins
hist_idx = np.argsort(hist)

# Reverse the found indices, since we want those bins with the highest counts
hist_idx = hist_idx[::-1]

# From that indices, we only want the first n elements (assuming there are n red lines)
hist_idx = hist_idx[:n]

# Add 1, because we cut the 0 bin
hist_idx = hist_idx + 1

# As a preparation: Sort the (cumulative) thickness values
tv = np.sort(hist_idx)

By now, we have the (cumulative) thickness values for each column. To reconstruct the actual, single thickness values, we need the "inverse" of the cumulative sum. There's this nice Q&A on that topic.

# The "inverse" of the cumulative sum to reconstruct the actual thickness values
tv[1:] -= tv[:-1]

# Save thickness values in "global" array
tvs[:, c] = tv
HansHirse
  • 18,010
  • 10
  • 38
  • 67
  • Thank you so much!! I really appreciate it. I don't get the last part, when you do img.shape[1]+1, why are you adding one? And those lines: tv = np.sort(np.argsort(hist[0][1:])[::-1][0:n]+1) // tv[1:] -= tv[:-1] // tvs[:, c] = tv // for me are a little bit difficult to understand, are you eliminating the peaks? Thanks again. – Georgia Feb 24 '21 at 15:40
  • I've added some more details on the "NumPy magic". Please see the edit(s) in my answer. – HansHirse Feb 24 '21 at 21:01
1

using opencv:

img = cv2.imread(image_path)
average_line_width = np.average(np.count_nonzero((img[:,:,:]==np.array([0,0,255])).all(2),axis=0))/2
print(average_line_width)

using pil

img = np.asarray(Image.open(image_path))
average_line_width = np.average(np.count_nonzero((img[:,:,:]==np.array([255,0,0])).all(2),axis=0))/2
print(average_line_width)

output in both cases:

18.430701754385964
joostblack
  • 2,465
  • 5
  • 14
  • for a conversion to real scale you need to know the true size of something in the image – joostblack Feb 24 '21 at 12:46
  • Crazy! Thank you so much:) Can you explain me how this function works? Why are you using img[:,:,2] and dividing the result by 2? Is this going through the image by columns? – Georgia Feb 24 '21 at 13:06
  • It counts the red pixels per column. Dividing by 2 is because there are two lines. img[:,:,2] gives only the red channel of an image when using opencv. This is because opencv uses bgr by default (blue green red) instead of rgb – joostblack Feb 24 '21 at 13:09
  • Using `img[:, :, 2] == 255` works for this specific example, since the other colours are pure RGB green and blue. If there would be, for example, some RGB pure yellow `0, 255, 255`, then you would mask that part, too, leading to false results. – HansHirse Feb 24 '21 at 14:24
  • You are right @HansHirse. Should now be fixed. – joostblack Feb 24 '21 at 14:53
  • Perfect, thank you so much. And this is supposed to give me the final result right? Becasue the average is for all the columns and not only for one. – Georgia Feb 25 '21 at 12:38
0

I'm not sure I got it but I used the answer of joostblack to calculcate both average thickness in pixel of both lines. Here is my code with comments:

import numpy as np

## Read the image
img = cv2.imread('img.png')

## Create a mask on the red part (I don't use hsv here)
lower_val = np.array([0,0,0])
upper_val = np.array([150,150,255])
mask = cv2.inRange(img, lower_val, upper_val)

## Apply the mask on the image
only_red = cv2.bitwise_and(img,img, mask= mask)


gray = cv2.cvtColor(only_red, cv2.COLOR_BGR2GRAY) 
  
## Find Canny edges 
edged = cv2.Canny(gray, 30, 200) 

## Find contours
img, contours, hier = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) 


## Select contours using a bonding box
coords=[]
for c in contours:
    x,y,w,h = cv2.boundingRect(c)
    if w>10:
        ## Get coordinates of the bounding box is width is sufficient (to avoid noise because you have a huge red line on the left of your image)
        coords.append([x,y,w,h])

## Use the previous coordinates to cut the image and compute the average thickness for one red line using the answer proposed by joostblack
for x,y,w,h in coords:
    average_line_width = np.average(np.count_nonzero(only_red[y:y+h,x:x+w],axis=0))
    print(average_line_width)
    ## Show you the selected result
    cv2.imshow('image',only_red[y:y+h,x:x+w])
    cv2.waitKey(0)

The first one is average 6.34 pixels when the 2nd is 5.94 pixels (in the y axis). If you want something more precise you'll need to change this formula!

Panda50
  • 901
  • 2
  • 8
  • 27
  • Wooooow!! Thank you so much! I'm running the code but I got an error in this line: `code` img, contours, hier = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) ValueError: not enough values to unpack (expected 3, got 2) `code` Do you know what's happening? – Georgia Feb 24 '21 at 13:17
  • You use a opencv version under 3 right? modify this line by using: contours, hier = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) instead – Panda50 Feb 24 '21 at 13:18
  • Thank you, it's working!! So as I understand, 6.34 pixels is for the top one and 5.94 for the bottom one? I wanted to ask you why upper_val is np.array([150,150,255]) and not np.array([0,0,255]) – Georgia Feb 24 '21 at 13:28
0

One way of doing this is to calculate the medial axis (centreline) of the red pixels. And then, as that line is 1px wide, the number of centreline pixels gives the length of the red lines. If you also calculate the number of red pixels, you can easily determine the average line thickness using:

average thickness = number of red pixels / length of red lines

The code looks like this:

#!/usr/bin/env python3

import cv2
import numpy as np
from skimage.morphology import medial_axis

# Load image
im=cv2.imread("Dc4zq.png")

# Make mask of all red pixels and count them
mask = np.alltrue(im==[0,0,255], axis=2)  
nRed = np.count_nonzero(mask)

# Get medial axis of red lines and line length
skeleton = (medial_axis(mask*255)).astype(np.uint8)
lenRed = np.count_nonzero(skeleton)
cv2.imwrite('DEBUG-skeleton.png',(skeleton*255).astype(np.uint8))

# We now know the length of the red lines and the total number of red pixels
aveThickness = nRed/lenRed
print(f'Average thickness: {aveThickness}, red line length={lenRed}, num red pixels={nRed}')

That gives the skeleton as follows:

enter image description here

Sample Output

Average thickness: 16.662172878667725, red line length=1261, num red pixels=21011
Mark Setchell
  • 191,897
  • 31
  • 273
  • 432