How to find certain line pattern on image

Hi.

I have a images like included but without red line.

My goal is to get the equation or list of points of a line (red on the image) which looks like in the below image:
threshold_close_line

There are at least patterns that can be detected:

  1. The longest vertical white line
  2. “Place” where horizontal line is split into two parts (left one is the lower and right one is upper on the image)

Does anyone have an idea how can I get the list of points (equation) of points selected by red color on the second image?

welcome.

how did that break happen? what’s the picture before thresholding?

The brake is in a place where laser light splits on wooden brick. In other words, the first part of the line is at a different height from the second part of the line.

Here is the image before the threshold:
base

What do you think about using “Hough Line Transform” algorithm?

that’s the context I needed to understand the situation. it looked like some kind of broken nail but now I know it’s not.

here’s a step towards a solution: a profile line, for every column it gives the y coordinate of mean brightness. it’s not perfect but neither is the picture :wink: you can run this on the thresholded picture as well. might even give you cleaner results.

you can do a derivative (np.gradient) on that profile and find extrema in its slope. those will be where the break/step is.

import cv2 as cv
import matplotlib.pyplot as plt

im = cv.imread("de4d1c40c99620fbc9c813f111cb8767c87fcca3.jpeg", cv.IMREAD_GRAYSCALE)
h, w = im.shape
gy, gx = np.mgrid[0:h, 0:w]

# weighted sum
profile = np.sum(im * gy, axis=0) / np.sum(im, axis=0)

# negative to make it look like the picture
# I can't be bothered to mess with the axes so positive y goes down instead of up
plt.plot(-profile)

plt.show()

gradient:

Thank you @crackwitz. Your solution works very well.
By the way, I got better results on the thresholded image.

How can I get the value of x for the smallest y? I tried np.min(gradient, axis=0), but I got nan. When I print gradient all values are nan.

In the above example, we assume that the line is horizontal. I understand that if the line will be vertical I need to calculate the x coordinate of mean brightness.
How can I deal with a line which diagonal (or has an unknown angle)?

How to deal with multiple lines in different directions?

you can guess what I’ll ask of you next: to present precisely what you did. there’s no way to help you without that information. you should anticipate this.

I’d also recommend argmin, which gives the index of the smallest element.

do you actually have to handle directions other than horizontal? instead you could tell the user to take the picture in the right orientation. there are ways to estimate orientation but there’s insufficient information to pick one yet.

you should present the entire problem thoroughly. playing 20 questions and guessing what you need is counter-productive.

My whole code looks like this:

import cv2
import matplotlib.pyplot as plt
import numpy as np

orginal_img = cv2.imread("base_file.bmp", cv2.IMREAD_GRAYSCALE)

ret, threshold_img = cv2.threshold(orginal_img, 41, 255, cv2.THRESH_BINARY)
print("threshold: ", threshold_img)

h, w = threshold_img.shape
gy, _ = np.mgrid[0:h, 0:w]

profile = np.sum(threshold_img * gy, axis=0) / np.sum(threshold_img, axis=0)
print("profile: ", profile)

gradient = np.gradient(profile)
print("gradient: ", gradient)

min_value = np.min(gradient, axis=0)
print("min_value: ", min_value)

plt.plot(gradient)
plt.show()

In console one can see:

threshold:  [[0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 ...
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]]
main.py:17: RuntimeWarning: invalid value encountered in true_divide
  profile = np.sum(threshold_img * gy, axis=0) / np.sum(threshold_img, axis=0)
profile:  [nan nan nan ... nan nan nan]
gradient:  [nan nan nan ... nan nan nan]
min_value:  nan

I guess that I divide by 0 that’s why I get “nan”.
How to get the value of y where x is minimum:

For example from the above image, I want to get a value near 2150.

You are right. I need to prepare more materials (images) to present the entire problem. I will do it shortly.

#!/usr/bin/env python3
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

original_img = cv.imread("base_file.bmp", cv.IMREAD_GRAYSCALE)
cv.namedWindow("original_img", cv.WINDOW_NORMAL)
cv.imshow("original_img", original_img)

reit, threshold_img = cv.threshold(original_img, 41, 255, cv.THRESH_BINARY)
print("threshold: ", threshold_img)
cv.namedWindow("threshold_img", cv.WINDOW_NORMAL)
cv.imshow("threshold_img", threshold_img)
cv.waitKey(1)

h, w = threshold_img.shape
gy, _ = np.mgrid[0:h, 0:w]

profilesum = np.sum(threshold_img, axis=0)

# weighted sum, calculates the "center of mass" for every column
profile = np.sum(threshold_img * gy, axis=0) / profilesum

# alternative: find first and last white pixel of every column, calculate midpoint

# let's only consider columns that have at least 10 pixels set
# less causes some noise
# OpenCV uses 0 and 255 for thresholded/binary pictures
# I need 0/1 for this step:
boolean_img = (threshold_img > 0)
profilevalid = (np.sum(boolean_img, axis=0) >= 10)

# nans aren't plotted
# invalidate columns that have less than 10 pixels set
profile[~profilevalid] = np.nan

gradient = np.gradient(profile)
#print("gradient: ", gradient)

extremum_value = np.nanmin(gradient, axis=0) # nanmin: min but ignore nans
print("extremum_value: ", extremum_value)

extremum_x = np.nanargmin(gradient)
print("extremum_x: ", extremum_x)

fig, axs = plt.subplots(2, sharex=True)
fig.set_size_inches(12, 6)
axs[0].axvline(x=extremum_x, color='r', dashes=(1,2))
axs[0].plot(profile)
axs[1].plot([extremum_x], [extremum_value], 'ro')
axs[1].plot(gradient)
fig.tight_layout()
plt.show()
cv.destroyAllWindows()

Thank you for your detailed answer.
Before getting materials for an explanation of the entire problem I would like to ask one thing.

What exactly does this line?

profile = np.sum(threshold_img * gy, axis=0) / profilesum

Can one interpret it as the code below?

for i in range(0, len(threshold_img)):
for j in range(0, len(threshold_img[i])):
profile[i][j] = sum(threshold_img[i][j] * gy[i][j] / profilesum[j])

I make a simple image like this one
example

I examine what does profile variable returns, I “feel” that this is what I need but I don’t understand how it is created and why we use gx (which is [[0, 0, 0, …], [1, 1, 1, …] …]). Could you please elaborate on this topic or send me a link to material from which I can get these pieces of information?

no. profile is a 1-dimensional array. your interpretation accesses it like a 2-dimensional array.

I’ll explain my code:

profilesum = np.sum(threshold_img, axis=0)

# weighted sum, calculates the "center of mass" for every column
profile = np.sum(threshold_img * gy, axis=0) / profilesum

we are calculating a weighted sum or weighted average. the goal is to figure out the “center of mass” of the pixels for every column.

threshold_img is the image.

np.sum along axis 0 means every column is collapsed into the sum of its values. you get a row vector that contains something proportional to the number of pixels in every column of the image. it would be exactly the number of pixels if the pixel values were 0 or 1, but they’re 0 and 255, so you get a weight of 255 in almost everything. I’m trying to explain this as simply as possible, so I will only explain this for binary images.

gy merely gives us the y-coordinate for every pixel. gx is not used. I only gave it a name because it is part of the data returned from np.mgrid

threshold_img * gy leaves in every pixel either that coordinate or 0.

if we want the average y coordinate of those pixels, we have to divide the sum by the number of pixels. that’s what the whole expression does.

you have already proposed a small test picture to investigate. now look at the values inside of all the arrays.

Now everything is clear for me, thank you!

Going back to the problem (which isn’t very well defined as I play with OpenCV rather when solving the real problem).

Let assume that on every image I have only one line, the line can be only vertical or horizontal, the crack on the line can be either perpendicular or at an angle (different than 90 degrees which I understand as perpendicular). Resuming we can have 4 situations:

  1. horizontal line, perpendicular crack (as in the image that we already discussed)
  2. horizontal line, crack at angle 0-180
  3. vertical line, perpendicular crack (as in the image that we already discussed)
  4. vertical line, crack at angle 0-180

In the case of the vertical and horizontal lines, one should create a “profile” base on rows instead of columns. We can reduce the problem to two cases: perpendicular crack and crack at angle 0-180, the first one is well examined.

How to get the angle of crack and “point of crack” if it isn’t perpendicular to the line?

I attached the image horizontal line with crack at angle about 45 degrees.

too little data to estimate the angle of the break. I see you have full resolution data. do post that instead of low resolution versions.

next you might tell me that the laser line can move across the scene? I hate to guess.

why is the angle of the break even interesting? don’t say that someone just wants to know. that’s no motivation.

I expected an explanation that includes perhaps a picture of the scene that is not in darkness (and a picture that is not from the camera that does the “measuring”, i.e. a view from the outside), or a claim that the camera is fixed (relative to the scene) and the laser line moves. I’m looking through a keyhole here. it’s frustrating.

you need more data. and I can’t speculate on how to get it because I’m literally tapping in the dark.

What I send is just a crop from the whole image, there is no more data. Rest of the image almost black.

Theoretically, it can move but during taking the picture it stays in one place. Why it is important?

I try to know how much information about the position of a flat object (plywood rectangle) I can get only from image analysis. Depends on the result I will use it in my project in which I use laser light to mark positions on a stage or not.

The camera is fixed relative to the scene, the laser lines are also fixed (but can be moved, but we can assume that there are horizontal and vertical lines). I send a picture on Wednesday.

based on the latest picture, I guess an angle could be estimated somehow, very approximately. the laser line is very narrow. there isn’t much to work with. I’m gonna leave it at that. I’ll be more reserved from now on.

Hi @crackwitz. I don’t understand what I did wrong that you will be “more reserved from now”. I had no bad intentions.

there is very little data in that picture and I think it’s not enough to answer questions like “what’s the orientation of the break”. I think it’s somewhat doable but the result will be guesswork and I suspect the task is made needlessly difficult by the poor picture quality and your other stated constraints, for which there was no justification given.

if you want to test the limits of feasibility, that’s a challenge you give yourself.

Hi @crackwitz,

I have one more question related to this topic. In some situations “split line” looks like this:

example

I want to find the beginning and the end of the “split line”.

The profile of of this line (with selected searched points) looks like this:

profile_selected_points

I tried to make this line smoother (by rolling average) and then make a gradient from it, but after smoothing the line the point in which the function’s value increase changes its position on the x value.

Do you have any idea how to get those points?

second derivatives.

signal looks like

           _____
          /
         /
        /
       /
      /
_____/

first derivative looks like

     _____
____/     \_____

second derivative looks like

____/\____  ____
          \/

if you do the moving average wrong, you introduce a phase shift. you just add/subtract that then.

I’d recommend a gaussian filter. spares you from understanding the details.

https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.gaussian_filter.html

Unfortunately, I can’t get a satisfying result.

When I don’t smooth the function I can’t get any interesting data from derivatives. The plot looks like this:

gradient

I tried to use botch convolved and gausian filters and I got result as below (gradient from profile of lined on which filters were used):
gradient_convolved
gradient_gaussian

Theoretically, I can try to add/subtract the phase shift, but I don’t know how to calculate the shift.

Could you please provide me more hints?

vary the smoothing (try less), and you still need a second derivative