1

I've been researching and trying a couple functions to get what I want and I feel like I might be overthinking it. One version of my code is below. The sample image is here.

My end goal is to find the angle (yellow) of the approximated line with respect to the frame (green line) Final I haven't even got to the angle portion of the program yet.

The results I was obtaining from the below code were as follows. Canny Closed Small Removed

Anybody have a better way of creating the difference and establishing the estimated line? Any help is appreciated.

import cv2
import numpy as np

pX = int(512)
pY = int(768)

img = cv2.imread('IMAGE LOCATION', cv2.IMREAD_COLOR)
imgS = cv2.resize(img, (pX, pY))
aimg = cv2.imread('IMAGE LOCATION', cv2.IMREAD_GRAYSCALE)

# Blur image to reduce noise and resize for viewing
blur = cv2.medianBlur(aimg, 5)
rblur = cv2.resize(blur, (384, 512))

canny = cv2.Canny(rblur, 120, 255, 1)
cv2.imshow('canny', canny)
kernel = np.ones((2, 2), np.uint8)
#fringeMesh = cv2.dilate(canny, kernel, iterations=2)
#fringeMesh2 = cv2.dilate(fringeMesh, None, iterations=1)
#cv2.imshow('fringeMesh', fringeMesh2)
closing = cv2.morphologyEx(canny, cv2.MORPH_CLOSE, kernel)
cv2.imshow('Closed', closing)

nb_components, output, stats, centroids = cv2.connectedComponentsWithStats(closing, connectivity=8)
#connectedComponentswithStats yields every separated component with information on each of them, such as size
sizes = stats[1:, -1]; nb_components = nb_components - 1

min_size = 200  #num_pixels

fringeMesh3 = np.zeros((output.shape))
for i in range(0, nb_components):
    if sizes[i] >= min_size:
        fringeMesh3[output == i + 1] = 255


#contours, _ = cv2.findContours(fringeMesh3, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
#cv2.drawContours(fringeMesh3, contours, -1, (0, 255, 0), 1)


cv2.imshow('final', fringeMesh3)

#cv2.imshow("Natural", imgS)
#cv2.imshow("img", img)
cv2.imshow("aimg", aimg)
cv2.imshow("Blur", rblur)
cv2.waitKey()
cv2.destroyAllWindows()
  • In your "small removed" image, get the most bottom white pixel of the first and last column. That approximates the desired line (not ideal in that example, though), and you can calculate the desired angle from both points. – HansHirse Mar 16 '21 at 09:23

2 Answers2

2

You can fit a straight line to the first white pixel you encounter in each column, starting from the bottom.

I had to trim your image because you shared a screen grab of it with a window decoration, title and frame rather than your actual image:

enter image description here

import cv2
import math
import numpy as np

# Load image as greyscale
im = cv2.imread('trimmed.jpg', cv2.IMREAD_GRAYSCALE)

# Get index of first white pixel in each column, starting at the bottom
yvals = (im[::-1,:]>200).argmax(axis=0)

# Make the x values 0, 1, 2, 3...
xvals = np.arange(0,im.shape[1])

# Fit a line of the form y = mx + c
z = np.polyfit(xvals, yvals, 1)

# Convert the slope to an angle
angle = np.arctan(z[0]) * 180/math.pi

Note 1: The value of z (the result of fitting) is:

array([ -0.74002694, 428.01463745])

which means the equation of the line you are looking for is:

y = -0.74002694 * x + 428.01463745

i.e. the y-intercept is at row 428 from the bottom of the image.

Note 2: Try to avoid JPEG format as an intermediate format in image processing - it is lossy and changes your pixel values - so where you have thresholded and done your morphology you are expecting values of 255 and 0, JPEG will lossily alter those values and you end up testing for a range or thresholding again.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
1

Your 'Closed' image seems to quite clearly segment the two regions, so I'd suggest you focus on turning that boundary into a line that you can do something with. Connected components analysis and contour detection don't really provide any useful information here, so aren't necessary.

One quite simple approach to finding the line angle is to find the first white pixel in each row. To get only the rows that are part of your diagonal, don't include rows where that pixel is too close to either side (e.g. within 5%). That gives you a set of points (pixel locations) on the boundary of your two types of grass.

From there you can either do a linear regression to get an equation for the straight line, or you can get two points by averaging the x values for the top and bottom half of the rows, and then calculate the gradient angle from that.

An alternative approach would be doing another morphological close with a very large kernel, to end up with just a solid white region and a solid black region, which you could turn into a line with canny or findContours. From there you could either get some points by averaging, use the endpoints, or given a smooth enough result from a large enough kernel you could detect the line with hough lines.

ES-Alexander
  • 111
  • 3
  • I'm not sure how to do any of those things yet but thanks for the input! I did try the large kernel morphological close and it doesn't really end up with just one white side and one black side. There were just large white specks on the whole left side as well. – PlasticBlaze Mar 15 '21 at 23:49
  • Large white specs can only be there if there are small white specs to start with. Try doing another open before you do the large close. – ES-Alexander Mar 16 '21 at 01:53
  • Okay, I’ve got it to where it’s white on the top right and black in the bottom left. Although due to the large kernel size it’s almost like a stair case. – PlasticBlaze Mar 16 '21 at 02:16
  • Just realised I meant a dilate not a close, forgot my morphology. You'll also likely get better results by using a circular (elliptical) kernel, instead of a square. – ES-Alexander Mar 16 '21 at 03:23