In [1]:
"""
Weerstanden herkennen
HU elektrotechniek - beeldherkenning - september 2020

Pieter Baelde  1744256
Ewoud Dronkert 1743524
"""

import numpy as np
import cv2
from matplotlib import pyplot as plt
from math import sqrt, sin, cos, atan2, pi
from colorsys import rgb_to_hsv
from scipy.signal import argrelextrema
from glob import glob

# Names for colour numbers 0-9
colournames = np.array(['zwart', 'bruin', 'rood', 'oranje', 'geel', 'groen', 'blauw', 'paars', 'grijs', 'wit'])
multipliernames = {-1: 'goud', -2: 'zilver', -3: 'roze'}
tolerancevals = {'g': -1, 's': -2, 'p': -3}
colourrgb = {
    -3: "#FF69B4",
    -2: "#C0C0C0",
    -1: "#CFB53B",
    0 : "#000000",
    1 : "#964B00",
    2 : "#FF0000",
    3 : "#FFA500",
    4 : "#FFFF00",
    5 : "#9ACD32",
    6 : "#6495ED",
    7 : "#9400D3",
    8 : "#A0A0A0",
    9 : "#FFFFFF"}

# Region of interest based on resistor position
#   pos: (4x int) coordinates of endpoints of resistor
#   imgshape: (2x int) height and width of image = maximum y and x coordinate + 1
#   margin: (int) how many pixels to add on each side
# returns: (4x int) region of interest, can be used to crop image
def cropregion(pos, imgshape, margin=50):
    # Unpack
    x0, y0, x1, y1 = pos

    # Sort and add margin
    if x0 <= x1:
        x0, x1 = x0 - margin, x1 + margin
    else:
        x0, x1 = x1 - margin, x0 + margin

    if y0 <= y1:
        y0, y1 = y0 - margin, y1 + margin
    else:
        y0, y1 = y1 - margin, y0 + margin

    # Check image boundary
    x0 = max(0, x0)
    y0 = max(0, y0)
    x1 = min(x1, imgshape[1])
    y1 = min(y1, imgshape[0])

    # Region of interest tuple
    return x0, y0, x1, y1
In [2]:
"""
Detect resistor position in image
@author pieter.baelde@student.hu.nl 1744256
"""

def detectposition(filename, blurradius=15, erodesize=17, draw=False):
    # Read from disk
    img = cv2.imread(filename)

    # Resize to lose some fine detail
    shrink = 2   # reduce image size by factor
    img = cv2.resize(img, (img.shape[1] // shrink, img.shape[0] // shrink))
    
    # Need clean copy if position will be shown on image
    if draw:
        imgpos = img.copy()
        font = cv2.FONT_HERSHEY_PLAIN

    # Find colours on white paper
    imghsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)  # convert BGR to HSV
    lo = np.array([0, 65, 0])                      # minimum values of the pixels
    hi = np.array([179, 255, 255])                 # maximum values of the pixels
    mask = cv2.inRange(imghsv, lo, hi)             # check which pixels are within the min and max
    img = cv2.bitwise_and(img, img, mask=mask)     # get original colours back

    # Blur
    img = cv2.GaussianBlur(img, (blurradius, blurradius), 0)

    # Remove resistor legs
    img = cv2.erode(img, np.ones((erodesize, erodesize), np.uint8))

    # Find resistor
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # convert to grayscale
    contours, _ = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    approx = False
    for cnt in contours:
        approx = cv2.approxPolyDP(cnt, 0.3 * cv2.arcLength(cnt, True), True)
        if draw:
            # Draw red line along the resistor
            cv2.drawContours(imgpos, [approx], 0, (0, 0, 255), 3)

            # Print text at contour
            x = approx.ravel()[0]
            y = approx.ravel()[1]
            if len(approx) == 3:
                cv2.putText(imgpos, "Triangle", (x, y), font, 1, (0))
            elif len(approx) == 4:
                cv2.putText(imgpos, "Rectangle", (x, y), font, 1, (0))
            elif len(approx) == 5:
                cv2.putText(imgpos, "Pentagon", (x, y), font, 1, (0))
            elif 6 < len(approx) < 15:
                cv2.putText(imgpos, "Ellipse", (x, y), font, 1, (0))
            else:
                cv2.putText(imgpos, "Circle", (x, y), font, 1, (0))

    # Result for resized image (only use first contour found)
    pos = (approx[0,0,0], approx[0,0,1], approx[1,0,0], approx[1,0,1])

    if draw:
        # Only show the area around the contour
        roi = cropregion(pos, img.shape)
        img = img[roi[1]:roi[3], roi[0]:roi[2]]
        imgpos = imgpos[roi[1]:roi[3], roi[0]:roi[2]]

        # Show processed and detected
        dpi = 96                    # approximately actual size
        w = imgpos.shape[1] * 2.1   # 2 columns
        h = imgpos.shape[0]         # 1 row
        plt.figure(figsize=(w/dpi, h/dpi), dpi=dpi)

        plt.subplot(121)
        plt.title("Bewerkt")
        plt.imshow(img)  # grayscale with standard colour space

        plt.subplot(122)
        plt.title("Positie")
        plt.imshow(cv2.cvtColor(imgpos, cv2.COLOR_BGR2RGB))

        plt.tight_layout()
        plt.show()

    # Result for original image
    return np.array(pos, dtype=int) * shrink
In [3]:
"""
Detect colours in image at specified position
@author ewoud.dronkert@student.hu.nl
"""

# Sample BGR and HSV values along lines perpendicular to the resistor long axis.
#   filename: (string) colour image on disk
#   pos: (4x int) x0,y0,x1,y1 coordinates of endpoints of the resistor
#   draw: (bool) show images yes or no
# returns: (2x 2D Numpy array) mean BGR and HSV values of each sample line
def samplecolours(filename, pos, draw=False):

    img = cv2.imread(filename, 1)  # 1=colour image
    if draw:
        detect = img.copy()

    # Imagine the resistor lying horizontally and a sampling grid on top of it.
    # This would be 6 lines of 3 points each:
    #   +-+-+-+-+-+
    #   | | | | | |
    # --+-+-+-+-+-+--
    #   | | | | | |
    #   +-+-+-+-+-+
    lines = 200   # number of sample lines along the length of the resistor
    points = 50   # number of points per sample line

    # Coordinates of the endpoints of the resistor = position detection line
    x0, y0, x1, y1 = pos

    # Draw thick red line over the length of the resistor
    # if draw:
    #     cv2.line(detect, (x0, y0), (x1, y1), (0,0,255), 10)

    # Resistor dimensions
    rdx = x0 - x1                       # resistor dx
    rdy = y0 - y1                       # resistor dy
    rlen = sqrt(rdx * rdx + rdy * rdy)  # resistor length

    # Sample line dimensions perpendicular to the position detection line
    slen = rlen / 8                     # sample line length (from centre) relative to resistor length
    sang = atan2(rdy, rdx) + pi / 2     # sample line angle perpendicular to the resistor
    sdx = slen * cos(sang)              # sample line dx (from centre)
    sdy = slen * sin(sang)              # sample line dy (from centre)

    # Coordinates along the resistor on the position detection line
    rx = np.linspace(x0, x1, num=lines)
    ry = np.linspace(y0, y1, num=lines)

    # BGR and HSV arrays with average colour values along the sample lines
    bgr = np.zeros((lines, 3), dtype=np.float64)
    hsv = np.zeros((lines, 3), dtype=np.float64)

    # Sampling lines along the length of the resistor
    for i in range(lines):
        # Endpoint tuples of the sample line
        sxy1 = (round(rx[i] + sdx), round(ry[i] + sdy))
        sxy2 = (round(rx[i] - sdx), round(ry[i] - sdy))

        # Draw thin yellow line on the sample line
        if draw:
            cv2.line(detect, sxy1, sxy2, (0,255,255), 1)

        # Coordinates along the sample line
        sx = np.linspace(sxy1[0], sxy2[0], num=points)
        sy = np.linspace(sxy1[1], sxy2[1], num=points)

        # Average of BGR colour values along the sample line
        avg = np.zeros(3, dtype=np.float64)

        # Summing points along the sample line
        # and calculating the average
        for j in range(points):
            avg += img[round(sy[j]), round(sx[j])]
        avg /= points

        # Save BGR and HSV results of the sample line
        # all values must be or are in range 0.0 - 1.0
        bgr[i] = avg / 255
        hsv[i] = rgb_to_hsv(bgr[i, 2], bgr[i, 1], bgr[i, 0])

        # Hue correction modulo 1.0 for useable peaks
        if i > 0 and hsv[i - 1, 0] >= 0.8 and hsv[i, 0] <= 0.2:  # wrapped around?
            hsv[i, 0] += 1                                       # unwrap

    if draw:
        # Only show the area around the resistor
        roi = cropregion(pos, img.shape)
        img = img[roi[1]:roi[3], roi[0]:roi[2]]
        detect = detect[roi[1]:roi[3], roi[0]:roi[2]]
        
        dpi = 96  # make image approximately actual size
        w = img.shape[1] * 2.1
        h = img.shape[0]
        plt.figure(figsize=(w/dpi,h/dpi),dpi=dpi)
        plt.subplot(121)
        plt.title("Origineel")
        plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        plt.subplot(122)
        plt.title("Kleuren")
        plt.imshow(cv2.cvtColor(detect, cv2.COLOR_BGR2RGB))
        plt.show()
    
    return (bgr, hsv)

# Smoothing filter by moving average convolution
#   data: (1D Numpy array) with at least 'window' values
#   window: (int >= 1) kernel size
# returns: new array with length reduced by window-1
def movingaverage(data, window=11):
    return np.convolve(data, np.ones(window), 'valid') / window

# Get index & value of peaks/troughs
#   bgr: (1D Numpy array) with average BGR values of sample lines
#   hsv: (1D Numpy array) with average HSV values of sample lines
#   window: (int >= 1) kernel size for smoothing
# returns: (list)
#   1D Numpy array with indices of peaks
#   2D Numpy array with [b,g,r,h,s,v] values
def detectcolours(bgr, hsv, window=11):
    # Detect the base hue of the resistor
    hist = np.histogram(hsv[:, 0], bins=48, range=(0, 1.2))  # histogram of hue in 0.25 steps
    imax = np.argmax(hist[0])       # bin number with the highest count
    basehuemin = hist[1][imax]      # lower bound
    basehuemax = hist[1][imax + 1]  # higher bound

    # Filter the data
    smooth = movingaverage(hsv[:, 0], window)  # has window-1 fewer values than data!

    # Indices of peaks
    ip = argrelextrema(smooth, np.greater)[0]  # indices of hue peaks in smoothed data
    ip += window // 2                          # correction for smoothing size decrease
    ip = ip[hsv[ip, 0] > basehuemax]           # select only peaks above base hue

    # Indices of troughs
    it = argrelextrema(smooth, np.less)[0]     # indices of hue troughs in smoothed data
    it += window // 2                          # correction for smoothing size decrease
    it = it[hsv[it, 0] < basehuemin]           # select only troughs below base hue

    ix = np.sort(np.concatenate((ip, it)))            # sorted indices of peaks & troughs
    val = np.concatenate((bgr[ix], hsv[ix]), axis=1)  # array of [b,g,r,h,s,v] arrays
    return (ix, val.astype(np.float32))               # float32 for KNN
In [4]:
"""
Helper functions for visualisation
@author ewoud.dronkert@student.hu.nl
"""

def showhisto(bgr, hsv):
    plt.figure(figsize=(16, 8))

    plt.subplot(231)
    plt.title("Rood")
    plt.hist(bgr[:, 2], bins=10, range=(0.0, 1.0))

    plt.subplot(232)
    plt.title("Groen")
    plt.hist(bgr[:, 1], bins=10, range=(0.0, 1.0))

    plt.subplot(233)
    plt.title("Blauw")
    plt.hist(bgr[:, 0], bins=10, range=(0.0, 1.0))

    # Get resistor background hue range from maximum value here!
    plt.subplot(234)
    plt.title("Tint")
    plt.hist(hsv[:,0], bins=24, range=(0.0, 1.2), color='r')

    plt.subplot(235)
    plt.title("Verzadiging")
    plt.hist(hsv[:,1], bins=12, range=(0.0, 1.2))

    plt.subplot(236)
    plt.title("Intensiteit")
    plt.hist(hsv[:,2], bins=12, range=(0.0, 1.2))

    plt.tight_layout()
    plt.show()

def plotsamples(bgr, hsv, peaks=None, window=11, save=False):
    x = np.arange(len(bgr) - window + 1) + window // 2

    plt.figure(figsize=(16, 8))

    plt.subplot(121)
    plt.title('RGB-waardes')
    plt.xlabel('Monsterlijn')
    plt.ylabel('Gemiddelde RGB-waarde')
    if peaks is not None:
        plt.vlines(peaks, 0.0, 1.0, colors='0.33', linestyles='--')
    plt.plot(x, movingaverage(bgr[:, 2], window), color='red')
    plt.plot(x, movingaverage(bgr[:, 1], window), color='green')
    plt.plot(x, movingaverage(bgr[:, 0], window), color='blue')
    plt.legend(('Rood', 'Groen', 'Blauw'))

    plt.subplot(122)
    plt.title('HSV-waardes')
    plt.xlabel('Monsterlijn')
    plt.ylabel('Gemiddelde HSV-waarde')
    if peaks is not None:
        plt.vlines(peaks, 0.0, 1.0, colors='0.33', linestyles='--')
    plt.plot(x, movingaverage(hsv[:, 0], window), linewidth=3)
    plt.plot(x, movingaverage(hsv[:, 1], window), linestyle=':')
    plt.plot(x, movingaverage(hsv[:, 2], window), linestyle=':')
    plt.legend(('Tint', 'Verzadiging', 'Intensiteit'))

    plt.tight_layout()
    if save:
        plt.savefig('bgrhsv.png', dpi=72)
    plt.show()
In [5]:
filename = '20200923200846_51031.jpg'
filename = '20200923200325_47031.jpg'
filename = '20200923194706_682g.jpg'
filename = '20200923201135_3942.jpg'
filename = '20200923195413_56011.jpg'
filename = '20200923171335_10031.jpg'
filename = '20200923193804_6842.jpg'
filename = '20200923200626_475g.jpg'

# Detect the position of the resistor
pos = detectposition(filename, draw=True)

# Sample the BGR and HSV values on the resistor
bgr, hsv = samplecolours(filename, pos, draw=True)

# Get indices and r,g,b,h,s,v values of the colour bands
ix, traindata = detectcolours(bgr, hsv, window=15)

# Plot graphs for validation
showhisto(bgr, hsv)    # histograms of BGR and HSV values
plotsamples(bgr, hsv, peaks=ix, window=15, save=True)  # line graphs of BGR and HSV values

# Colour numbers determined from filename
#   last digit is tolerance
code = filename.split('_')[1].split('.')[0]
colournums = np.array([int(d) if d.isdigit() else tolerancevals[d] for d in code], dtype=int)

# Tolerance should be further apart and come last
if len(ix) >= 3 and ix[1] - ix[0] > ix[-1] - ix[-2]:  # first peaks are further apart?
    colournums = colournums[::-1]                     # reverse the colour numbers

# Validate these detection numbers with the graphs
print("Piekindex   = %s" % ix)
print("Kleurnummer = %s" % colournums)
print("Kleurnaam   = %s" % [colournames[i] if i >= 0 else multipliernames[i] for i in colournums])
Piekindex   = [ 65  98 125 167]
Kleurnummer = [ 4  7  5 -1]
Kleurnaam   = ['geel', 'paars', 'groen', 'goud']
In [6]:
# Response shape is (len(colournums),1) and type is float32 for use in KNN
responses = np.array([colournums], dtype=np.float32).T
knndata = np.concatenate((responses, traindata), axis=1)

# Save KNN data for later!
# Every line in text file = [num,b,g,r,h,s,v]
np.savetxt(filename.split(".")[0] + ".knn.txt", knndata)

print("Kleurnummer  = %s" % responses.reshape(-1).astype(int))
print('BGR+HSV data =')
print(traindata)
Kleurnummer  = [ 4  7  5 -1]
BGR+HSV data =
[[0.5807059  0.6984314  0.7545098  0.11289109 0.23035343 0.7545098 ]
 [0.4229804  0.54964703 0.22996078 0.43396303 0.581621   0.54964703]
 [0.8244706  0.42352942 0.5367059  0.7137128  0.48630136 0.8244706 ]
 [0.33764705 0.7579608  0.8521569  0.13615346 0.6037736  0.8521569 ]]
In [7]:
# Read back all values from separate files and save to one file
knndata = np.zeros((1, 7), dtype=np.float32)  # base for concatenating, len([num,b,g,r,h,s,v]) = 7
for f in glob("*.knn.txt"):                   # read every file ending in .knn.txt
    knndata = np.concatenate((knndata, np.loadtxt(f, dtype=np.float32)))  # add new data to the end
np.savetxt("knndata.txt", knndata)

# Split responses from training data, skip first row with zeros
knnrespo = np.array([knndata[1:, 0]], dtype=np.float32).T  # transpose for use in KNN
knntrain = np.array(knndata[1:, 1:], dtype=np.float32)     # array of [b,g,r,h,s,v] arrays

# Histogram of all response values (= different colour numbers) in the KNN model
hmin = min(dict.keys(colourrgb))
hmax = max(dict.keys(colourrgb)) + 1
plt.title('Aantal kleurnummers in KNN-data')
plt.xlabel('Kleurnummer')
plt.ylabel('Aantal')
plt.bar(range(hmin, hmax),
        np.histogram(knnrespo.reshape(-1).astype(int),
                     bins=hmax - hmin,
                     range=(hmin, hmax))[0],
        color=list(dict.values(colourrgb)),
        edgecolor='k')
plt.xlim(hmin - 0.5, hmax - 0.5)
plt.show()

print("Kleurnummer  = %s" % knnrespo.reshape(-1).astype(int))
print('BGR+HSV data =')
print(knntrain)
Kleurnummer  = [ 1  3  0  1  5  5  6  0  1  1  1  0  0  3  1  2  4  8  6 -1  2  8  6  4
  7  0  3  1  4  7  5 -1  2  4  9  3]
BGR+HSV data =
[[0.29631373 0.29160786 0.45858824 0.995303   0.36411834 0.45858824]
 [0.39333335 0.4381961  0.76235294 1.0202621  0.4840535  0.76235294]
 [0.30062744 0.23294118 0.23152941 0.6632615  0.22984608 0.30062744]
 [0.37905884 0.28486276 0.39537254 0.8579371  0.27950802 0.39537254]
 [0.35803923 0.5130196  0.14533333 0.42974973 0.71671    0.5130196 ]
 [0.376      0.55662745 0.11207843 0.43228063 0.79864734 0.55662745]
 [0.78290194 0.43772548 0.04188235 0.57763547 0.9465037  0.78290194]
 [0.31317648 0.22086275 0.22       0.66512346 0.29752067 0.31317648]
 [0.40384313 0.3596863  0.45380393 0.92180556 0.20739716 0.45380393]
 [0.3087843  0.26760784 0.424549   0.9562719  0.36966562 0.424549  ]
 [0.29717648 0.30007842 0.4802353  1.0026422  0.38118568 0.4802353 ]
 [0.24196078 0.19764706 0.2195294  0.7489675  0.18314424 0.24196078]
 [0.24941176 0.19521569 0.21254902 0.71997106 0.2172956  0.24941176]
 [0.30235294 0.4221961  0.76823527 1.0428731  0.60643184 0.76823527]
 [0.26462746 0.24517646 0.4282353  0.98229074 0.42747253 0.4282353 ]
 [0.31654903 0.21490195 0.65223527 0.9612625  0.67051464 0.65223527]
 [0.2809412  0.7283922  0.84062743 0.13324457 0.66579586 0.84062743]
 [0.6293333  0.47113726 0.4458039  0.64366096 0.2916251  0.6293333 ]
 [0.6763137  0.23882353 0.10086274 0.6267094  0.850864   0.6763137 ]
 [0.6294902  0.7539608  0.81529415 0.11165048 0.22789803 0.81529415]
 [0.43960783 0.31592157 0.6861961  0.94432676 0.53960454 0.6861961 ]
 [0.68988234 0.55286276 0.51239216 0.628664   0.25727603 0.68988234]
 [0.84886277 0.40556863 0.0427451  0.5916521  0.94964427 0.84886277]
 [0.3974902  0.66847056 0.43286276 0.3115774  0.4053737  0.66847056]
 [0.5660392  0.2352157  0.3730196  0.7360914  0.58445334 0.5660392 ]
 [0.2322353  0.1827451  0.19780391 0.7173798  0.21310368 0.2322353 ]
 [0.3182745  0.38737255 0.69341177 1.030699   0.54100215 0.69341177]
 [0.24047059 0.22352941 0.3899608  0.98303485 0.42679003 0.3899608 ]
 [0.5807059  0.6984314  0.7545098  0.11289109 0.23035343 0.7545098 ]
 [0.4229804  0.54964703 0.22996078 0.43396303 0.581621   0.54964703]
 [0.8244706  0.42352942 0.5367059  0.7137128  0.48630136 0.8244706 ]
 [0.33764705 0.7579608  0.8521569  0.13615346 0.6037736  0.8521569 ]
 [0.3164706  0.22792158 0.6900392  0.96806407 0.66969764 0.6900392 ]
 [0.3242353  0.7797647  0.85741174 0.14239483 0.6218441  0.85741174]
 [0.9379608  0.8440784  0.82031375 0.633      0.12542854 0.9379608 ]
 [0.3747451  0.37639216 0.82062745 1.0006156  0.5433432  0.82062745]]
In [8]:
# Train the KNN model
knn = cv2.ml.KNearest_create()
knn.train(knntrain, cv2.ml.ROW_SAMPLE, knnrespo)

# Test with another sample
test = ix[0] + 20
newcomer = np.expand_dims(np.concatenate((bgr[test], hsv[test])), axis=0).astype(np.float32)
_,result,_,_ = knn.findNearest(newcomer, 1)
colournum = int(result[0, 0])

# Print result
print("BGR+HSV     = %s" % newcomer.reshape(-1))
print("Kleurnummer = %s" % colournum)
print("Kleurnaam   = %s" % colournames[colournum])
BGR+HSV     = [0.8900392  0.8483921  0.28588235 0.51148903 0.678798   0.8900392 ]
Kleurnummer = 6
Kleurnaam   = blauw
In [ ]: