#!/usr/bin/env python
# coding: utf-8
# # Object Counting on a Convey Belt
#
#
# ### Let's first import required libraries
# In[1]:
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
# magic commands
get_ipython().run_line_magic('config', 'IPCompleter.greedy=True')
get_ipython().run_line_magic('config', 'Completer.use_jedi = False')
get_ipython().run_line_magic('matplotlib', 'inline')
# ### Let's load and visualize the template image and the convey belt snapshot at a given time.ΒΆ
#
# In[2]:
template_im = cv.imread(r'template.png', cv.IMREAD_GRAYSCALE)
belt_im = cv.imread(r'belt.png', cv.IMREAD_GRAYSCALE)
fig, ax = plt.subplots(1,2,figsize=(10,10))
ax[0].imshow(template_im, cmap='gray')
ax[1].imshow(belt_im, cmap='gray')
plt.show()
# ## Part-I :
#
# ### Otsu's thresholding
#
# * Otsu's method avoids having to choose a value and determines an optimal global threshold value from the image histogram automatically. It is returned as the first output.
# In[3]:
th_t, img_t = cv.threshold(template_im,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
th_b, img_b = cv.threshold(belt_im,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
# ### Morphological closing
#
# Carrying out morphological closing to remove small holes inside the
# foreground. Use a \$3 \\times 3\$ kernel.
#
# 1. Erosion: a pixel element is '1' if all the pixel under the kernel is '1'. So it decreases the white region
# 2. Dilation: a pixel element is '1' if atleast one pixel under the kernel is '1'. So it increases the white region
#
# Morphological closing is just Dilation followed by Erosion.
# In[4]:
# 3x3 matrix with all ones, with uint8 dtype
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3,3))
closing_t = cv.morphologyEx(img_t, cv.MORPH_CLOSE, kernel) # Dilation followed by Erosion
closing_b = cv.morphologyEx(img_b, cv.MORPH_CLOSE, kernel) # Dilation followed by Erosion
# ### Connected component analysis
#
# Apply the `connectedComponentsWithStats` function
#
#
# In[5]:
retval_t, labels_t, stats_t, centroids_t = cv.connectedComponentsWithStats(closing_t)
retval_b, labels_b, stats_b, centroids_b = cv.connectedComponentsWithStats(closing_b)
def cca_stats(img_name, retval, labels, stats, centroids):
print(img_name.center(50,"="))
print("Number of labels: ", retval)
plt.imshow(labels.astype('uint8'), cmap ='gray'); plt.show()
print("Stats: \n", stats,'\n') # stats[label,cv.CC_STAT_quantity])
print("Centroids: \n", centroids,'\n')
cca_stats("Template Image", retval_t, labels_t, stats_t, centroids_t)
cca_stats("Belt Image", retval_b, labels_b, stats_b, centroids_b)
# - How many connected components are detected in each image?
# **Template Image** = 2 (*including background*)
# **Belt Image** = 4 (*including background*)
#
# - What are the statistics? Interpret these statistics.
# Statistics are properties related to each connected component. Statistics object is a 2D array where each column represent a different quantity related to a given connected component as described below.
#
# **Column 1:** *cv.CC_STAT_LEFT*: the leftmost (x) coordinate which is the inclusive start of the bounding box in the horizontal direction.
# **Column 2:** *cv.CC_STAT_TOP*: the topmost (y) coordinate which is the inclusive start of the bounding box in the vertical direction.
# **Column 3:** *cv.CC_STAT_WIDTH*: the horizontal size of the bounding box.
# **Column 4:** *cv.CC_STAT_HEIGHT*: the vertical size of the bounding box.
# **Column 5:** *cv.CC_STAT_AREA*: the total area (in pixels) of the connected component.
#
# - What are the centroids?
# Each row of the 2D `Cenroids` obejct represents the (x,y) coordinates of centroid of the corresponding connected component.
# ### Contour analysis
#
# Use `findContours` function to retrieve the *extreme outer* contours.
#
# Contours is a Python list of all the contours in the image. Each individual contour is a Numpy array of (x,y) coordinates of boundary points of the object. Display these countours.
# In[6]:
# cv.RETR_EXTERNAL retrieve only the extreme outer contours
contours_t,_ = cv.findContours(closing_t, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
contours_b,_ = cv.findContours(closing_b, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# Visualizing contours (-1 argument to plot all the contours)
im_contours_belt = np.zeros((belt_im.shape[0],belt_im.shape[1],3), np.uint8)
conts = cv.drawContours(im_contours_belt, contours_b, -1, (0,255,0), 3).astype('uint8')
plt.imshow(conts), plt.show()
# ### Count the number of matching hexagonal nuts in `belt.png`.
#
# `cv.matchShapes` function enables us to compare two shapes, or two contours and returns a metric showing the similarity. The lower the result, the better match it is. `retval=cv.matchShapes(contour1, contour2, method, parameter)
# `
# In[7]:
label = 1 # remember that the label of the background is 0
belt = ((labels_b >= label)*255).astype('uint8')
plt.imshow(belt, cmap ='gray'),plt.show()
belt_cont,_ = cv.findContours(belt, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
for j,c in enumerate(belt_cont):
print("Contour ", j +1,"-->", cv.matchShapes(contours_t[0], c, cv.CONTOURS_MATCH_I1, 0.0))
# ## Part - II
#
# ### Frame tracking through image moments.
#
# Use the `cv.contourArea()`, see
# [this](https://docs.opencv.org/master/dd/d49/tutorial_py_contour_features.html)
# and calculate the the area of the `contours_b[1]`
#
# In[8]:
ca = cv.contourArea(contours_b[1])
print(ca)
# Use the `cv.moments` to extract the x and y coordinates of the centroid
# of `contours_b[1]`.
#
# In[9]:
M = cv.moments(contours_b[1])
print("Area = ", M['m00'])
cx, cy = int(M['m10']/M['m00']), int(M['m01']/M['m00'])
print("Centroid = ({}, {})".format(cx,cy))
# Make a variable called `count` to represent the number of contours and
# set it to the value 1. Make an np array \[cx, cy, ca, count\] and name
# this as `object_prev_frame`
#
#
# In[10]:
count = 1
object_prev_frame = np.array([cx, cy, ca, count])
# Similarly, you can create the `object_curr_frame`(to describe the
# current values) and define the threshold `delta_x` to check whether the
# corresponding element of both the `object_curr_frame` and
# `object_prev_frame` are less than the `delta_x`. You can set `delta_x`
# as 15 or so. (Here the `delta_x` can be thought of as the movement of
# the cx from frame to frame)
#
# In[11]:
delta_x = 15
# ## Part - III
#
# ### 1. Implement the function `get_indexed_image`, which takes an image as the input, performs thresholding, closing, and connected component analysis and return retval, labels, stats, centroids. (Grading)
#
# In[12]:
def get_indexed_image(im):
""" Thresholding, closing, and connected component analysis lumped
"""
_, img = cv.threshold(im,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3,3))
closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel) # Dilation followed by Erosion
retval, labels, stats, centroids = cv.connectedComponentsWithStats(closing)
return retval, labels, stats, centroids
# ### 2. Implement the function `is_new`, which checks the dissimilarity between 2 vectors. (Grading)
#
# In[13]:
def is_new(a, b, delta, i):
""" Vector Dissimilarity with an Array of Vectors
Checks if vector b is similar to a one or more vectors in a outside the tolerances specified in delta.
vector i specifies which elements in b to compare with those in a.
"""
absolute_different = np.abs(a - b) # getting the absolute differences
states = [] # arry to store result of each column
for element in i:
# compare each value in ith column with the specified delta
absolute_different[:,element] = (absolute_different[:,element] > delta[element])
# check whether the condition is true for all the values in that column
states.append(absolute_different[:,element].all())
'Check whether the absolute different between all the elements of ith column of each array is greater than the ith delta value (See thee example in the next cell)'
# summarize the results of each column to get the final result.
state = np.array(states).all()
return state
# In[14]:
# check is_new expected answer False
a = np.array([[1.36100e+03, 5.53000e+02, 5.99245e+04, 2.00000e+00],
[7.61000e+02, 4.53000e+02, 5.99385e+04, 1.00000e+00],
[1.55200e+03, 2.43000e+02, 6.00585e+04, 3.00000e+00]])
b = np.array( [7.51000e+02, 4.53000e+02, 5.99385e+04, 3.00000e+00])
delta = np.array([delta_x])
i = np.array([0])
assert is_new(a, b, delta, i) == False, " Check the function "
# ### 3. If the array `a` is in the shape of (number of nuts , len(object_prev_frame)) ( i.e. array `a` is made by stacking all the `object_prev_frame` for each frame. If b is in the form of \[cx, cy, ca, count\], write the function `prev_index` to find the index of a particular nut in the previous frame. (Grading)
#
# In[15]:
def prev_index(a, b, delta, i):
""" Returns Previous Index
Returns the index of the apppearance of the object in the previous frame.
(See thee example in the next cell)
"""
index = -1
absolute_different = np.absolute(a - b)
matching_rows = []
for element in i:
absolute_different[:,element] = (absolute_different[:,element] <= delta[element])
# find the row where the above condition is true.
matching_row = np.where(absolute_different[:,element])[0]
matching_rows.append(matching_row)
# get the best match out of all the matches
values, counts = np.unique(matching_rows, return_counts=True)
best_match = values[np.argmax(counts)]
matching_nut = a[best_match]
index = matching_nut[-1] # since count keeps the index of a nut.
return index
# In[16]:
# check prev_index expected answer 1
a = np.array([[1.36100e+03, 5.53000e+02, 5.99245e+04, 2.00000e+00],
[7.61000e+02, 4.53000e+02, 5.99385e+04, 1.00000e+00],
[1.55200e+03, 2.43000e+02, 6.00585e+04, 3.00000e+00]])
b = np.array( [7.51000e+02, 4.53000e+02, 5.99385e+04, 3.00000e+00])
delta = np.array([delta_x])
i = np.array([0])
assert prev_index(a,b,delta,i) == 1, " Check the function "
# ### Access video frames
# You can use following code snippet load and access each frame of a video
#
# In[17]:
color_frames = [] # list to store RGB frames
cap = cv.VideoCapture('conveyor_with_rotation.mp4') # give the correct path here
while cap.isOpened():
ret, frame = cap.read()
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
color_frames.append(frame)
cv.imshow("Frame", frame)
if cv.waitKey(1) == ord('q'):
break
cap.release()
cv.destroyAllWindows()
# ### 3. Implement a code to detect hexagonal nuts in a moving convey belt. (Grading)
#
# ## Steps:
#
# 1. Use the above code snippet to access each frame and remember to
# convert the frame into grey scale. Name the variable as `grey`
# 2. Call `get_indexed_image` and extract
# `retval, labels, stats, centroids`.
# 3. Find contours of all nuts present in a given frame of the belt.
# 4. Initiate a 3-D array with zeros to draw contours. Call this
# `im_contours_belt`
# 5. Draw each contour. Use `cv.drawContours`. [See
# this](https://docs.opencv.org/master/d4/d73/tutorial_py_contours_begin.html)
# In[18]:
gray_frames = [] # list to store Gray Scale frames
cap = cv.VideoCapture('conveyor_with_rotation.mp4') # give the correct path here
print("Video capturing is in progress...")
while cap.isOpened():
ret, frame = cap.read()
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
frame = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) # convert to grayscale
gray_frames.append(frame) # store the grayscale frame images
if cv.waitKey(1) == ord('q'): #keyboard interruption
break
cap.release()
cv.destroyAllWindows()
print("Video capturing completed.")
# In[19]:
# visualizing some of the captured frames
plt.figure(figsize=(30,20))
for i in range(9):
plt.subplot(3,3,i+1)
plt.imshow(gray_frames[100 +i], cmap ='gray')
plt.xlabel("Frame " + str(100 +i))
plt.show()
# In[20]:
# Find contours of all nuts present in a given frame of the belt
contour_plots = []
contours_list = []
for gray in gray_frames:
# finding contours
_, labels, _, _ = get_indexed_image(gray) # Conn: Comp: Analysis
belt = ((labels >= 1)*255).astype('uint8')
contours,_ = cv.findContours(belt, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
contours_list.append(contours)
# plotting contours
im_contours_belt = np.zeros((belt.shape[0],belt.shape[1],3), np.uint8)
cont_plot = cv.drawContours(im_contours_belt, contours, -1, (0,255,0), 5).astype('uint8')
contour_plots.append(cont_plot)
# In[21]:
# visualizing some of the contour plots on frames
plt.figure(figsize=(30,20))
for i in range(9):
plt.subplot(3,3,i+1)
plt.imshow(contour_plots[100 +i])
plt.xlabel("Frame " + str(100 +i))
plt.show()
# ## Object detection and tracking
#
# For each contour of the belt frame,
#
# 1. Use `is_new` and `prev_index` functions to track each frame and get
# the indices of each nut.
# 2. Write a code to detect and track hexagonal nuts in each frame.
#
# **Hint**: *If you are thresholding on areas (template and contour) you
# can use 500 as the threshold. You can set the matching threshold to be
# 0.5 and experiment*
# In[22]:
# frame tracking through image moments
# extracting details about each contour in each frame
video = []
print("Details extraction is in progress...")
for gray in gray_frames:
# finding contours
_, labels, _, _ = get_indexed_image(gray)
belt = ((labels >= 1)*255).astype('uint8')
contours,_ = cv.findContours(belt, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
count = 0 # number of nuts in a given frame
frame = []
for contour in contours:
metric = cv.matchShapes(contours_t[0], contour, cv.CONTOURS_MATCH_I1, 0.0)
# Set the matching threshold to be 0.5
if metric <= 0.5: # if only a complete hexagonal nut
count +=1
M = cv.moments(contour)
ca = M['m00']
cx, cy = int(M['m10']/M['m00']), int(M['m01']/M['m00'])
frame.append(np.array([cx, cy, ca, count]))
video.append(frame)
print("Details extraction completed.")
# In[23]:
# Nut counter Implemetation
# last element keeps the number of nuts in a given frame.
# Therefore, initial number of nuts is as follows.
total_nuts = int(video[0][-1][-1])
print("Number of nuts in the zeroth frame: ",total_nuts)
delta_x = np.array([15])
i = np.array([0])
prev_frame = video[0] # Reference frame to compare with the upcoming frame
for frame_num in range(1, len(video)):
current_frame = video[frame_num] # frame to be compared
for nut in current_frame:
# Checking the current nut with previous frame
# if it is a new nut count it.
if is_new(prev_frame, nut, delta_x, i):
total_nuts +=1
prev_frame = current_frame
print("Total number of nuts found: ",total_nuts)
# In[24]:
# trackig nuts: An extension of the previous counting nuts algorithm
print("Detection and indexing is in progress...")
total_nuts = int(video[0][-1][-1]) #initial number of nuts in frame 0
delta_x = np.array([15])
i = np.array([0])
prev_frame = video[0] # Reference frame to compare with the upcoming frame
for frame_num in range(1, len(video)):
current_frame = video[frame_num] # frame to be compared
for nut in current_frame:
# Checking the current nut with previous frame
# if it is a new nut count it and assign a new index
if is_new(prev_frame, nut, delta_x, i):
total_nuts +=1
nut[-1] = total_nuts # assignment of a new index
else:
# to be tracked the nut must not be a new nut
nut_index = prev_index(prev_frame, nut, delta_x, i)
nut[-1] = nut_index # assign the same index it had previously, for old nuts
prev_frame = current_frame
print("Detection and indexing completed.")
# In[25]:
# frame annotation with the extracted details
annotated_frames =[]
frame_num = 0
print("Frame annotation is in progress...")
for frame, color_frame, contours in zip(video, color_frames, contours_list):
# Annotation was done on the frames of the original video
img = color_frame
y = 0 # to change the vertical position of 'object details' of a frame
for nut in frame:
# Annotate the nut index
img = cv.putText(img, str(int(nut[-1])),\
(int(nut[0]),int(nut[1])),cv.FONT_HERSHEY_SIMPLEX, 2, (255,0,0), 4)
# Annotate the nut(object) details
img = cv.putText(img, "Object {}: {:04} {:04} {:05}".format(int(nut[-1]), int(nut[0]), int(nut[1]), nut[2]),\
(50,850 + 70*y), cv.FONT_HERSHEY_SIMPLEX, 2, (255,0,0), 4)
y +=1 # change vertical position for next object details annotation.
# Annotation of the frame number and draw the extracted contours
img = cv.putText(img, "Frame "+str(frame_num) , (50,750) , cv.FONT_HERSHEY_SIMPLEX, 2, (255,0,0), 3)
img = cv.drawContours(img, contours, -1, (255,0,0), 5).astype('uint8')
# Index number 180631J
img = cv.putText(img, "180631J" , (50,150) , cv.FONT_HERSHEY_SIMPLEX, 2, (255,0,0), 3)
annotated_frames.append(img)
frame_num +=1
print("Frame annotation completed.")
# In[31]:
# # visulaizing some annotated frames
plt.figure(figsize=(30,20))
for i in range(9):
plt.subplot(3,3,i+1)
plt.imshow(annotated_frames[100 +i][:,:,::-1])
plt.xlabel("Frame " + str(100 +i))
plt.show()
# plt.figure(figsize = (10,10))
# plt.imshow(annotated_frames[100][:,:,::-1])
# plt.savefig("object_detection.png")
# ### Save the frames as a video
# In[27]:
# Defining necessary parameters for video write
output = '180631j_en2550_a05.mp4' # Name of the output video file.
fourcc = cv.VideoWriter_fourcc(*'MP4V') # 4-character code of codec used to compress the frames
duration = 9 # duration of the source video
fps = int(len(annotated_frames)/duration) # Framerate of the created video stream
height, width,_ = annotated_frames[0].shape
frame_size = (width, height)
isColor = True # to write color images to the video
print('Frame Size(WxH) :', frame_size ,'\nFrames Per Second :', fps)
# Creating the Video Writer object
out = cv.VideoWriter(output, fourcc, fps, frame_size, isColor)
print("Video writer in progress...")
for frame in annotated_frames:
out.write(frame)
# Release everything if job is finished
out.release()
print("Video writing completed.")