-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcoin-counter.py
More file actions
207 lines (165 loc) · 7.62 KB
/
Copy pathcoin-counter.py
File metadata and controls
207 lines (165 loc) · 7.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# Raymond Mi
# Started 04-14-2026, completed 05-12-2026
# Coin Counter and Value Aggregator
# Takes 20 frames and averages the radius and position of coins
# Labels coins in the image with a radius (px), coin type, a box around the coin, and the centre point
# Credits to Raspberry Pi textbook chapters 8.1-8.4 (for the basic template code) and ChatGPT (for adjusting the parameters, adding blur, removing duplicates, grouping coins by radius and position, and the loonie vs. toonie classifier code)
import cv2
from imutils.video import VideoStream
from imutils import resize
import numpy as np
# Classifies loonies vs. toonies (similar radius)
def loonie_or_toonie(img, x, y, r):
# Look at the color of the image
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# Toonies have a golden center with a silver outer part
center_radius = int(r * 0.4)
outer_radius = int(r * 0.8)
center_pixels = []
outer_pixels = []
# Go through pixels in a section of the image between -outer_radius and outer_radius
for i in range(-outer_radius, outer_radius):
for j in range(-outer_radius, outer_radius):
# Find distance between pixels
dist = np.sqrt(i*i + j*j)
px = x + i
py = y + j
# Ensure pixels are not out of bounds
if px < 0 or py < 0 or px >= hsv.shape[1] or py >= hsv.shape[0]:
continue
# Get HSV (hue, saturation, value) for the pixel
h, s, v = hsv[py, px]
# Store center-region pixels
if dist < center_radius:
center_pixels.append((h, s))
# Store outer-ring pixels
elif center_radius < dist < outer_radius:
outer_pixels.append((h, s))
# Prevent errors if there are no pixels collected
if len(center_pixels) == 0 or len(outer_pixels) == 0:
return "Unknown"
# Compute average hue and saturation for center and outer ring
center_mean = np.mean([p[0] for p in center_pixels])
center_sat = np.mean([p[1] for p in center_pixels])
outer_mean = np.mean([p[0] for p in outer_pixels])
outer_sat = np.mean([p[1] for p in outer_pixels])
# Only classify it as a toonie if there as a large enough difference between the hue or saturation for the inner and outer ring
if abs(center_mean - outer_mean) > 10 or abs(center_sat - outer_sat) > 25:
return "Toonie"
# Otherwise, assume it is a loonie
return "Loonie"
all_circles = []
# Start the video stream
vs = VideoStream(src=0).start()
# Take 20 pictures of circles and place them into all_circles
for i in range(1, 20):
img = vs.read()
img = resize(img, width=600)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray) # Hist equalize is better contrast
blur = cv2.GaussianBlur(gray, (9, 9), 1.5) # Gaussian Blur is better
blur = cv2.medianBlur(blur, 5) # median blur is better blur
detected_circles = cv2.HoughCircles(blur, cv2.HOUGH_GRADIENT, 1.3, 80, param1 = 120, param2 = 35, minRadius = 35, maxRadius = 60) # tightened parameters once testing was complete
if detected_circles is not None:
for (x, y, r) in detected_circles[0]:
all_circles.append((x, y, r))
# Stop the VideoStream
vs.stop()
# Array to store the final coins for processing
final_coins = []
# Only loops through the coins if there are any, otherwise prints that there are no coins in the picture
if len(all_circles) > 0:
# Store groups of coins in the same area (similar coordinates)
groups = []
for (a, b, r) in all_circles:
matched = False
for group in groups:
gx, gy, gr = group[0]
if abs(a - gx) < 20 and abs(b - gy) < 20 and abs(r - gr) < 5: # Group by position and radius
group.append((a, b, r))
matched = True
break
if not matched:
groups.append([(a, b, r)])
# Take the mean of the coordinates/radius of coins in the same group
for group in groups:
gx = [c[0] for c in group]
gy = [c[1] for c in group]
gr = [c[2] for c in group]
avg_x = int(np.mean(gx))
avg_y = int(np.mean(gy))
avg_r = float(np.mean(gr))
final_coins.append((avg_x, avg_y, avg_r))
# Remove duplicate coins (multiple coins detected in the same place)
filtered_coins = []
for coin in final_coins:
a, b, r = coin
keep = True
for existing in filtered_coins:
ea, eb, er = existing
# Take the distance from centres of coins that are close together
dist = np.sqrt((a - ea) ** 2 + (b - eb) ** 2)
# Remove coins that are too close together
if dist < 0.6 * min(r, er):
if r > er:
filtered_coins.remove(existing)
else:
keep = False
break
if keep:
filtered_coins.append(coin)
# Set final_coins to be filtered_coins (removed duplicates)
final_coins = filtered_coins
amount = 0 # Value of coins
coinCount = [0, 0, 0, 0, 0] # Count of each coin
coinName = ["dime", "nickel", "quarter", "loonie", "toonie"] # Name of each coin for printing
display = img.copy() # Create image copy for labelling
# Add total value of coins based on their radius
for (a, b, r) in final_coins:
if (r >= 32 and r < 38):
coinCount[0] += 1
amount += 0.10
label = "Dime"
elif (r >= 38 and r < 42):
coinCount[1] += 1
label = "Nickel"
amount += 0.05
elif (r >= 42 and r < 46):
coinCount[2] += 1
amount += 0.25
label = "Quarter"
elif (r >= 46):
# Call the loonie_or_toonie function to figure out if it is a loonie or toonie
classification = loonie_or_toonie(img, a, b, r)
# Classify as a loonie or toonie based on the function return value, and add the appropriate value and count of coins
if classification == "Loonie":
coinCount[3] += 1
amount += 1.00
label = "Loonie"
elif classification == "Toonie":
coinCount[4] += 1
amount += 2.00
label = "Toonie"
else:
label = "Unknown"
# Identifies each coin, where it is located and what the code thinks it is
cv2.circle(display, (a, b), 3, (0, 0, 2,55), 2) # Outer circle
cv2.circle(display, (a, b), 3, (0, 0, 255), -1) # Centre point
cv2.circle(display, (a, b), int(r), (255, 0, 0), 2) # Bounding circle
cv2.putText(display, f"{label} ({r:.2f}px)", (int(a - r), int(b - r - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) # Draw label
# Print information about number of coins, total value, and number of each coin
print("Number of coins detected:", len(final_coins))
print("Total value of coins: $" + f"{amount:.2f}")
print("In this picture, the program found:")
for i in range(0, 5):
if coinCount[i] == 1:
print(f"{coinCount[i]} {coinName[i]}")
elif coinCount[i] != 0:
print(f"{coinCount[i]} {coinName[i]}s")
# Show the visualization of all coins
cv2.imshow("Detected Coins", display)
cv2.waitKey(0)
else:
print("No coins found in this picture.")
# Destroy the image window after a key is pressed
cv2.destroyAllWindows()