Search code examples
opencvcomputer-vision

Detecting lines on test cassettes opencv


real test cassette picture

(There is a solid line at C and a faint line at T)

I want to detect the line at T. Currently I am using opencv to locate the qr code and rotate the image until the qr code is upright. Then I calculate the approximate location of the C and T mark by using the coordinates of the qr code. Then my code will scan along the y axis down and detect there are difference in the Green and Blue values.

My problem is, even if the T line is as faint as shown, it should be regarded as positive. How could I make a better detection?


Solution

  • I cropped out just the white strip since I assume you have a way of finding it already. Since we're looking for red, I changed to the LAB colorspace and looked on the "a" channel.

    Note: all images of the strip have been transposed (np.transpose) for viewing convenience, it's not that way in the code.

    the A channel

    enter image description here

    I did a linear reframe to improve the contrast

    enter image description here

    The image is super noisy. Again, I'm not sure if this is from the camera or the jpg compression. I averaged each row to smooth out some of the nonsense.

    enter image description here

    I graphed the intensities (x-vals were the row index)

    enter image description here

    Use a mean filter to smooth out the graph

    enter image description here

    I ran a mountain climber algorithm to look for peaks and valleys

    enter image description here

    And then I filtered for peaks with a climb greater than 10 (the second highest peak has a climb of 25.5, the third highest is 4.4).

    enter image description here

    Using these peaks we can determine that there are two lines and they are (about) here:

    enter image description here

    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    # returns direction of gradient
    # 1 if positive, -1 if negative, 0 if flat
    def getDirection(one, two):
        dx = two - one;
        if dx == 0:
            return 0;
        if dx > 0:
            return 1;
        return -1;
    
    # detects and returns peaks and valleys
    def mountainClimber(vals, minClimb):
        # init trackers
        last_valley = vals[0];
        last_peak = vals[0];
        last_val = vals[0];
        last_dir = getDirection(vals[0], vals[1]);
    
        # get climbing
        peak_valley = []; # index, height, climb (positive for peaks, negative for valleys)
        for a in range(1, len(vals)):
            # get current direction
            sign = getDirection(last_val, vals[a]);
            last_val = vals[a];
    
            # if not equal, check gradient
            if sign != 0:
                if sign != last_dir:
                    # change in gradient, record peak or valley
                    # peak
                    if last_dir > 0:
                        last_peak = vals[a];
                        climb = last_peak - last_valley;
                        climb = round(climb, 2);
                        peak_valley.append([a, vals[a], climb]);
                    else:
                        # valley
                        last_valley = vals[a];
                        climb = last_valley - last_peak;
                        climb = round(climb, 2);
                        peak_valley.append([a, vals[a], climb]);
    
                    # change direction
                    last_dir = sign;
    
        # filter out very small climbs
        filtered_pv = [];
        for dot in peak_valley:
            if abs(dot[2]) > minClimb:
                filtered_pv.append(dot);
        return filtered_pv;
    
    # run an mean filter over the graph values
    def meanFilter(vals, size):
        fil = [];
        filtered_vals = [];
        for val in vals:
            fil.append(val);
    
            # check if full
            if len(fil) >= size:
                # pop front
                fil = fil[1:];
                filtered_vals.append(sum(fil) / size);
        return filtered_vals;
    
    # averages each row (also gets graph values while we're here)
    def smushRows(img):
        vals = [];
        h,w = img.shape[:2];
        for y in range(h):
            ave = np.average(img[y, :]);
            img[y, :] = ave;
            vals.append(ave);
        return vals;
    
    # linear reframe [min1, max1] -> [min2, max2]
    def reframe(img, min1, max1, min2, max2):
        copy = img.astype(np.float32);
        copy -= min1;
        copy /= (max1 - min1);
        copy *= (max2 - min2);
        copy += min2;
        return copy.astype(np.uint8);
    
    # load image
    img = cv2.imread("strip.png");
    
    # resize
    scale = 2;
    h,w = img.shape[:2];
    h = int(h*scale);
    w = int(w*scale);
    img = cv2.resize(img, (w,h));
    
    # lab colorspace
    lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB);
    l,a,b = cv2.split(lab);
    
    # stretch contrast
    low = np.min(a);
    high = np.max(a);
    a = reframe(a, low, high, 0, 255);
    
    # smush and get graph values
    vals = smushRows(a);
    
    # filter and round values
    mean_filter_size = 20;
    filtered_vals = meanFilter(vals, mean_filter_size);
    for ind in range(len(filtered_vals)):
        filtered_vals[ind] = round(filtered_vals[ind], 2);
    
    # get peaks and valleys
    pv = mountainClimber(filtered_vals, 1);
    
    # pull x and y values
    pv_x = [ind[0] for ind in pv];
    pv_y = [ind[1] for ind in pv];
    
    # find big peaks
    big_peaks = [];
    for dot in pv:
        if dot[2] > 10: # climb filter size
            big_peaks.append(dot);
    print(big_peaks);
    
    # make plot points for the two best
    tops_x = [dot[0] for dot in big_peaks];
    tops_y = [dot[1] for dot in big_peaks];
    
    # plot
    x = [index for index in range(len(filtered_vals))];
    fig, ax = plt.subplots()
    ax.plot(x, filtered_vals);
    ax.plot(pv_x, pv_y, 'og');
    ax.plot(tops_x, tops_y, 'vr');
    plt.show();
    
    # draw on original image
    h,w = img.shape[:2];
    for dot in big_peaks:
        y = int(dot[0] + mean_filter_size / 2.0); # adjust for mean filter cutting
        cv2.line(img, (0, y), (w,y), (100,200,0), 2);
    
    # show
    cv2.imshow("a", a);
    cv2.imshow("strip", img);
    cv2.waitKey(0);
    

    Edit:

    I was wondering why the lines seemed so off, then I realized that I forgot to account for the fact that the meanFilter reduces the size of the list (it cuts from the front and back). I've updated to take that into account.