user8307720
user8307720

Reputation:

Using Houghlines on a binary image, to identify the horizontal and vertical components and then "removing" them by drawing them in black

On this original image I am attempting to create a binary image with a black background and white points so I can fit the curve around them. here is the image after thresholding, dilating, corroding and blurring
I Intend to do this by using Houghlines on a binary image, to identify the horizontal and vertical components and then "removing" them by drawing them in black, however my code merely returns the original image in grayscale as opposed to a bunch of white points on a black background ready to be used as co-ordinates to fit a curve around them

    erosion = cv2.erode(img,kernel,iterations = 500)
    edges = cv2.Canny(img,0,255)
    lines = cv2.HoughLines(edges, 1, np.pi/180, 0, 0, 0)
    for rho,theta in lines[0]:
        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a*rho
        y0 = b*rho
        x1 = int(x0 + 1000*(-b))
        y1 = int(y0 + 1000*(a))   
        x2 = int(x0 - 1000*(-b))
        y2 = int(y0 - 1000*(a))

        line = cv2.line(img,(x1,y1),(x2,y2),(0,0,255),2)

    cv2.imshow("blackwave.PNG", line)
    cv2.imwrite("blackwave.PNG", line)
    cv2.waitKey(0) 
else:
    print 'Image could not be read'

Upvotes: 1

Views: 2414

Answers (1)

As a learning exercise for myself I've spent a while trying to solve the image analysis part of this problem. In some ways I feel a bit reluctant to gift you a solution because I think you are already showing the effect of having this happen to you - you haven't learned how to use cv, so you have to ask more questions looking for a solution rather than figuring out how to adapt the code for yourself. OTOH it feels churlish to not share what I've done.

DON'T ask me to 'please change/improve/get this working' - this code does what it does, if you want it to do something different then get coding: it's over to you now.

I saved your raw image in a file sineraw.png.

The code goes through the following steps:

1. read raw image, already grayscale

enter image description here

2. equalize the image in the first step to getting a binary (black/white) image

enter image description here

3. do an adaptive threshold to get a black/white image, still got lots of noise

enter image description here

4. perform an erosion to remove any very small dots of noise from the thresholded image

enter image description here

5. perform a connected component analysis on the thresholded image, then store only the "large" blobs into mask

enter image description here

6. skeletonize mask into skel

enter image description here

7. Now look for and overwrite near-horizontal and near-vertical lines with black

enter image description here

The final image should then be suitable for using curve fitting as only the curve is shown in white pixels. That's another exercise for you.

BTW you should really get a better source image.

I suppose there are other and possibly much better ways of achieving the same effect as shown in the final image, but this works for your source image. If it doesn't work for other images, well, you have the source code, get editing.

While doing this I explored a few options like different adaptive thresholding, the gaussian seems better at not putting white on the edges of the picture. I also explored drawing black lines around the picture to get rid of the edge noise, and also using the labelling to remove all white that is on the edge of the picture but that removes the main curve which goes up to the edge. I also tried more erosion/dilate/open/close but gave up and used the skeletonize because it preserves the shape and also happily leaves a centreline for the curve.

CODE

import copy
import cv2 
import numpy as np
from skimage import measure

# seq is used to give the saved files a sequence so it is easier to understand the sequence
seq = 0

# utility to save/show an image and optionally pause
def show(name,im, pause=False, save=False):
    global seq
    seq += 1
    if save:
        cv2.imwrite(str(seq)+"-"+name+".PNG", im)
    cv2.imshow(str(seq)+"-"+name+".PNG",im)
    if pause:
        cv2.waitKey(0) 

# utility to return True if theta is approximately horizontal        
def near_horizontal(theta):
    a = np.sin(theta)
    if a > -0.1 and a < 0.1:
        return True
    return False

# utility to return True if theta is approximately vertical
def near_vertical(theta):
    return near_horizontal(theta-np.pi/2.0)

################################################    
# 1. read raw image, already grayscale
src = cv2.imread('sineraw.PNG',0)
show("src",src, save=True)

################################################    
# 2. equalize the image in the first step to getting a binary (black/white) image
gray = cv2.equalizeHist(src)
show("gray",gray, save=True)

################################################    
# 3. do an adaptive threshold to get a black/white image, still got lots of noise
# I tried a range of parameters for the 41,10 - may vary by image, not sure
dst = cv2.adaptiveThreshold(gray, 255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY_INV,41,10)  
show("dst",dst, save=True)

################################################    
# 4. perform an erosion to remove any very small dots of noise from the thresholded image
erode1 = cv2.erode(dst, None, iterations=1)
show( "erode1",erode1, save=True)

################################################    
# 5. perform a connected component analysis on the thresholded image, then store only the "large" blobs into mask
labels = measure.label(erode1, neighbors=8, background=0)
# mask is initially all black
mask = np.zeros(erode1.shape, dtype="uint8")
# loop over the unique components
for label in np.unique(labels):
    # if this is the background label, ignore it
    if label == 0:
        continue

    # otherwise, construct the mask for this label and count the
    # number of pixels
    labelMask = np.zeros(erode1.shape, dtype="uint8")
    labelMask[labels == label] = 255
    numPixels = cv2.countNonZero(labelMask)

    # if the number of pixels in the component is sufficiently
    # large, then add it to our mask of "large blobs"
    if numPixels > 50:
        # add the blob into mask
        mask = cv2.add(mask, labelMask)
show( "mask", mask, save=True )

################################################    
# 6. skeletonize mask into skel
img = copy.copy(mask)
element = cv2.getStructuringElement(cv2.MORPH_CROSS,(3,3))
done = False
size = np.size(img)
# the skeleton is initially all black
skel = np.zeros(img.shape,np.uint8)     
while( not done):
    eroded = cv2.erode(img,element)
    temp = cv2.dilate(eroded,element)
    temp = cv2.subtract(img,temp)
    skel = cv2.bitwise_or(skel,temp)
    img = eroded.copy()
#        show( "tempimg",img)
    zeros = size - cv2.countNonZero(img)
    if zeros==size:
        done = True
show( "skel",skel, save=True )

################################################    
# 7. Now look for and overwrite near-horizontal and near-vertical lines with black
lines = cv2.HoughLines(skel, 1, np.pi/180, 100)
for val in lines:
    (rho,theta)=val[0]    
    a = np.cos(theta)
    b = np.sin(theta)
    if not near_horizontal(theta) and not near_vertical(theta):
        print "ignored line",rho,theta
        continue
    print "line",rho, theta, 180.0*theta/np.pi
    x0 = a*rho
    y0 = b*rho
    # this is pretty kulgey, should be able to use actual image dimensions, but this works as long as image isn't too big
    x1 = int(x0 + 1000*(-b))
    y1 = int(y0 + 1000*(a))   
    x2 = int(x0 - 1000*(-b))
    y2 = int(y0 - 1000*(a))
    print "line",rho, theta, 180.0*theta/np.pi,x0,y0,x1,y1,x2,y2
    cv2.line(skel,(x1,y1),(x2,y2),0,3)
################################################
# the final image is now in skel    
show("final",skel, pause=True,save=True)

Upvotes: 6

Related Questions