1

I have an image showed below (this image is a result of pre-processing of original image I had):

enter image description here

What I am trying to do is to detect the contour for the Black region bounded by 2 white regions (please note that white regions are not always solid and there could be 2, 3,... disconnected white region on top - for most of pictures the white regions are continuous; however, there are some examples where top white region is not continuous - Please see update section of my question to see example of non-continuous region)

Do you have any suggestion as how I can detect the boundary of black region and separate the black region?

========= UPDATE Requested by Micka ========

Example of non-continuous white region (this is happening only on top layer) - bottom layer is always continues solid white region

enter image description here

In this case like the picture showed above, I am okay with selecting a region like the one I showed below

enter image description here

ideal selection

enter image description here

Original PHOTO I also included the original image for the above pre-processed image below in a case you are interested to work on pre-processing it. The pre-processing code I used and ended up with the image above is showed below:

img = cv2.imread('....tif')
# convert to grayscale
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

# threshold
thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)[1]

# apply morphology
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,5))
morph = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (29,1))
morph = cv2.morphologyEx(morph, cv2.MORPH_CLOSE, kernel)

enter image description here

Ross_you
  • 881
  • 5
  • 22
  • context: (1) https://stackoverflow.com/questions/73807919/how-to-measure-average-thickness-of-segmented-image-using-diplib (2) https://stackoverflow.com/questions/73805144/how-to-change-back-the-labeled-image-to-a-form-that-cv2-can-read (3) https://stackoverflow.com/questions/73792621/how-to-measure-average-thickness-of-labeled-segmented-image (referencing those for *context*, so others don't have to waste time suggesting solutions that have already been discussed) – Christoph Rackwitz Oct 12 '22 at 19:21
  • @ChristophRackwitz Thanks for adding those. One of those questions is irrelevant to this question, the question which is relevant to this question is https://stackoverflow.com/questions/73792621/how-to-measure-average-thickness-of-labeled-segmented-image which I already included it in the body of this question – Ross_you Oct 12 '22 at 19:52
  • 1
    Invert your image color, so the region is white surrounded by black – fmw42 Oct 12 '22 at 23:32
  • Can you show a sample image for "please note that white regions are not always solid and the could be 2 disconnected white region on top"? – Micka Oct 13 '22 at 04:38
  • This problem is easy when the white regions are connected and touching both sides. Please show typical samples such that this does not hold. –  Oct 13 '22 at 09:51
  • @Micka I added a sample picture of image with non-continues white region – Ross_you Oct 13 '22 at 20:15
  • @YvesDaoust for most pictures I have, both top and bottom white regions are continuous. However, there are some cases that only top white region is not continuous. I added a sample picture in the question – Ross_you Oct 13 '22 at 20:16
  • @fmw42 I tried using `cv2.bitwise_not(img2)` and then applying contour detection algorithm you suggested; however, it will give me an strange result for almost all pictures. The result is worst if the original image has 2 separated white top region (I showed an example of this type of photo above) – Ross_you Oct 13 '22 at 20:26
  • @Micka I added a sample picture of image with non-continues white region – Ross_you Oct 14 '22 at 07:29
  • 1
    if you are able to detect the region coarsely, you can use distanceTransform which's maximum value will be the thickness (as a radius, not as a diameter). Another way, if you can segment the region finer, is to put a minAreaRect around the black region that is surrounded by the white region in 2 directions. If the region is mostly horizontal, the approach of @YvesDaoust might be sufficient to find the fine-segmented region. The width (or height) of the minAreaRect is then the thickness of the region (as a diameter, not as a radius). – Micka Oct 14 '22 at 13:15
  • 1
    Show your original image before you processed it. You may need to adjust the morphology to get a more continuous top line. – fmw42 Oct 14 '22 at 15:41
  • @fmw42 I add original photo to the question; however, I believe that changing image pre-processing is not good solution. I have more than 1000 photos and each can have different geometry and may need different image processing. What I am trying to find is a solution that can help me detect black region (and potentially thickness of it) for all images with same image pre-processing. In other words, I want to fix my image pre-processing and then have universal solution to detect black region – Ross_you Oct 17 '22 at 19:15
  • 1
    This seems to be a duplicate of your question from before. Why? I thought I answered it. – fmw42 Oct 27 '22 at 22:22
  • @fmw42 sorry for the confusion, no. This question is different. In the question you answered before, I tried to find the thickness of white layers; while this question is to try and detect and separate the black region bounded by 2 white regions. You suggested inversing the photo and I tried that; however, I didn't get a good result for it specially for the picture where the narrow white region on top is separate – Ross_you Oct 27 '22 at 23:02

3 Answers3

4

Here is how:

import cv2
import numpy as np

img = cv2.imread(r"D:\OpenCV Projects\Black Contour\image.jpg")

img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower = np.array([0, 0, 75])
upper = np.array([179, 179, 115])
mask = cv2.inRange(img_hsv, lower, upper)

mask_blur = cv2.GaussianBlur(mask, (3, 3), 1)
_, thresh = cv2.threshold(mask_blur, 200, 255, cv2.THRESH_BINARY)

contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

cv2.drawContours(img, [max(contours, key=cv2.contourArea)], -1, (255, 0, 0), 3)
cv2.imshow("Image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Output:

enter image description here

Here is all the processing the image went through during the execution of the above code:

BGR to HSV:

enter image description here

HSV to mask:

enter image description here

Mask to blurred mask:

enter image description here

Blurred mask to thresholded mask:

enter image description here

From here, simply detect the contour from the image above and draw it onto the image.

Red
  • 26,798
  • 7
  • 36
  • 58
  • Thanks so much for your solution. I have a couple of questions. Can I ask why you changed the photo to HSV format? what's the benefit of doing this? my original photo is TIF format, can I still change it to HSV type? Also, I can see that you defined `lower` and `upper` bands for the photo. can I ask how you defined these values? is it some sort of general bandwidth that I can apply to all my photos? or it's specific to this photo I posted? – Ross_you Oct 27 '22 at 21:30
  • I same comment for thresholding looks like you chose 200 and 255 for the boundary, but I am not sure how you chose these numbers. Can you explain, please? – Ross_you Oct 27 '22 at 21:31
  • @Ross_you Hi! Didn't notice your comments at first. I used [OpenCV Trackbars](https://docs.opencv.org/3.4/da/d6a/tutorial_trackbar.html). Here is another answer of mine where I post the code for using trackbars to fine-tune the HSV values: [Digital image processing of corn kernels](https://stackoverflow.com/a/71606031/13552470) – Red Nov 02 '22 at 02:51
3

Here is one approach in Python/OpenCV that I tested on your short lines image. You may have to adjust for other images.

The idea is to apply vertical morphology to close up the spacing. Then get the absdiff between he threshold version and the closed version. The clean up any small spots with a morphology open.

Input:

enter image description here

import cv2
import numpy as np

# read input
image = cv2.imread("short_lines.jpg")

# convert to grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# create a binary thresholded image
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]

# apply morphology close to connect space between lines
kernel = np.ones((45,1), np.uint8)
morph = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)

# get absdiff of morph and thresh
diff = cv2.absdiff(thresh, morph)

# apply morphology open to clean up small spots
kernel = np.ones((5,5), np.uint8)
result = cv2.morphologyEx(diff, cv2.MORPH_OPEN, kernel)

# save results
cv2.imwrite("short_lines_close.jpg",morph)
cv2.imwrite("short_lines_diff.jpg",diff)
cv2.imwrite("short_lines_space.jpg",result)

# display result, though it won't show transparency
cv2.imshow("morph", morph)
cv2.imshow("diff", diff)
cv2.imshow("result", result)
cv2.waitKey(0)

Morphology Close image:

enter image description here

Absdiff Image:

enter image description here

Final Result after Morphology Open:

enter image description here

fmw42
  • 46,825
  • 10
  • 62
  • 80
2

Quick & dirty method:

Scan every image column and count the transitions from black to white and conversely. Every time you find four transitions, the two middle ones give you the thickness. Keep the median value.

  • Thanks for the idea, does angle of layer affect this approach? should I rotate image first and then count? Also, how can I detect transition from black to white? can I say when pixel value changes from 0 to 255, that's a transition? do we have function to do this in `cv2` or I need to implement this in a `for loop`? – Ross_you Oct 17 '22 at 19:18
  • @Ross_you: depends if you are after the vertical thickness or normal thickness. –  Oct 17 '22 at 19:30
  • I define thickness as the normal thickness. FYI, I also added my original photo to the question, if that helps. I am doing following image pre-processing: `kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,5)) morph = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (29,1)) morph = cv2.morphologyEx(morph, cv2.MORPH_CLOSE, kernel)` however, as mentioned in my comment to to @fmw42, I don't want to change pre-processing and am looking for solution to detect edge and thickness with same pre-processing for all of my photos – Ross_you Oct 17 '22 at 19:36
  • 1
    @Ross_you: the ratio of the two thicknesses is the cosine of the angle. Close to 1 for small angles. You can find the angle by considering two distant columns. –  Oct 17 '22 at 19:39