0

I have an image something like the image below (on the left):

enter image description here

I want to extract only the pixels in red on the right: the pixels that belong to a 1px vertical line, but not to any thicker line or other region with more than 1 adjacent black pixel. The image is bitonal.

I have so far tried a morphology OPEN with a vertical (10px, which is find for my purposes) and horizontal kernel and taken the difference, but this needs an awkward shift and leaves some "speckles":


    vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 10))
    vertical_mask1 = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel,
                                      iterations=1)

    horz_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 1))
    horz_mask = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horz_kernel,
                                      iterations=1)

    M = np.float32([[1,0,-1],[0,1,1]])
    rows, cols = horz_mask.shape
    vertical_mask = cv2.warpAffine(horz_mask, M, (cols, rows))

    result = cv2.bitwise_and(thresh, cv2.bitwise_not(horz_mask))

What is the correct way to isolate the 1px lines (and only the 1px lines)?

In the general case, for other kernels, this question is: how do I find all pixels in the image that are in regions that the kernel "fits inside" (and then a subtraction to get my desired result)?

Inductiveload
  • 6,094
  • 4
  • 29
  • 55
  • try hit-or-miss morphology. https://docs.opencv.org/master/db/d06/tutorial_hitOrMiss.html – Christoph Rackwitz Jul 06 '21 at 14:16
  • In your sample picture, you have ONLY one pixel red lines. For this picture you could brute force go pixel by pixel and (1) change all red pixels adjacent to more than one black pixel to white then (2) change all black pixels to white. For more complicated pictures you'd have to add a third step that removes all wide lines. – bfris Jul 10 '21 at 00:57
  • @bfris the red pixels are the desired output. – Inductiveload Jul 13 '21 at 08:52

1 Answers1

1

That's basically (binary) template matching. You need to derive proper templates from your "kernels". For larger "kernels", that might involve using masks for these templates, too, cf. cv2.matchTemplate.

What's the most important feature for a single pixel vertical line? The left and right neighbour of the current pixel must be 0. So, the template to match is [0, 1, 0]. By using the TemplateMatchMode cv2.TM_SQDIFF_NORMED, perfect matches will lead to close to 0 values in the result array.

You can mask those locations, and dilate according to the size of your template. Then, you use bitwise_and to extract the actual pixels that belong to your template.

Here's some code with a few template ("kernels"):

import cv2
import numpy as np

img = cv2.imread('AapJk.png', cv2.IMREAD_GRAYSCALE)[:, :50]

vert_line = np.array([[0, 1, 0]], np.uint8)
cross = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], np.uint8)
corner = np.array([[0, 0, 1], [0, 0, 1], [1, 1, 1]], np.uint8)

for i_k, k in enumerate([vert_line, cross, corner]):
    m, n = k.shape
    img_tmp = 1 - img // 255
    mask = cv2.matchTemplate(img_tmp, k, cv2.TM_SQDIFF_NORMED) < 10e-6
    mask = cv2.dilate(mask.astype(np.uint8), np.ones((m, n)), anchor=(n-1, m-1))
    m, n = mask.shape
    mask = cv2.bitwise_and(img_tmp[:m, :n], mask)
    out = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
    roi = out[:m, :n]
    roi[mask.astype(bool), :] = [0, 0, 255]
    cv2.imwrite('{}.png'.format(i_k), out)

Vertical line:

Vertical line

Cross:

Cross

Bottom right corner 3 x 3:

Bottom right corner

Larger templates ("kernels") most likely will require additional masks, depending on how many or which neighbouring pixels should be considered or not.

----------------------------------------
System information
----------------------------------------
Platform:      Windows-10-10.0.19041-SP0
Python:        3.9.1
PyCharm:       2021.1.3
NumPy:         1.20.3
OpenCV:        4.5.2
----------------------------------------
HansHirse
  • 18,010
  • 10
  • 38
  • 67