Skip to content

Commit 67f9a6e

Browse files
author
Tomasz bla Fortuna
committed
Complete workshop code files.
0 parents  commit 67f9a6e

File tree

3 files changed

+361
-0
lines changed

3 files changed

+361
-0
lines changed

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Computer vision workshop
2+
========================
3+
4+
1. You need at least a computer with Python and OpenCV installed. It worked on
5+
Mac OS X, Windows with Anaconda and of course runs on Linux (python3-opencv
6+
package being only dependency on Debian).
7+
2. For full workshop you need an USB camera, raspberry PI (version 2 or 3 preferred),
8+
and a servo. Something to do a ramp and colorful balls (glass ones for example).
9+
10+
3. Contents:
11+
- template.py contains initial template for participants to move boilerplate aside.
12+
- qlqi.py contains working algorithm that can be explained in around ~4h.
13+
- example.webm - highly compressed test video of balls going down the ramp.
14+
15+
4. Code license: Apache, Author: Tomasz Fortuna. Have fun.
16+

qlqi.py

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Computer vision workshop template - after filling!
5+
6+
NOTE: This wasn't a workshop on a correct Python program composition.
7+
8+
Prepared for "Exatel Security Days Programming Workshop" @07.06.2019
9+
Author: Tomasz bla Fortuna.
10+
License: Apache
11+
"""
12+
13+
import sys
14+
from time import time
15+
16+
import cv2
17+
import numpy as np
18+
19+
20+
class Servo:
21+
"""
22+
Servo control using PWM on Raspberry PI.
23+
"""
24+
25+
def __init__(self, servo_pin=18):
26+
try:
27+
import pigpio
28+
self.pi = pigpio.pi()
29+
except ImportError:
30+
print("No pigpio - simulating SERVO")
31+
self.pi = None
32+
33+
def set(self, value):
34+
if self.pi is not None:
35+
self.pi.set_servo_pulsewidth(18, value)
36+
37+
38+
servo = Servo()
39+
40+
41+
class Capture:
42+
"""
43+
Read data from camera or from a file.
44+
"""
45+
def __init__(self, filename=None, camera=None, size=None):
46+
"""
47+
Push filename if reading from file or camera if reading from camera. Don't use both.
48+
49+
Size if you want to force capture size.
50+
"""
51+
self.filename = filename
52+
self.camera = camera
53+
54+
if camera is not None:
55+
self.capture = cv2.VideoCapture(camera)
56+
57+
if size is not None:
58+
width, height = size
59+
self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, width)
60+
self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
61+
print("Set size to %dx%d" % (width, height))
62+
63+
elif filename is not None:
64+
self.capture = cv2.VideoCapture(filename)
65+
else:
66+
raise Exception("You failed at thinking")
67+
68+
self.width = int(self.capture.get(cv2.CAP_PROP_FRAME_WIDTH))
69+
self.height = int(self.capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
70+
71+
print("Initialized capturing device, size: %dx%d" % (self.width,
72+
self.height))
73+
74+
def frames(self):
75+
"Frame iterator - grab and yield frame."
76+
while True:
77+
(grabbed, frame) = self.capture.read()
78+
79+
if frame is None:
80+
break
81+
82+
yield frame
83+
84+
85+
def detect_kulka(frame, diff):
86+
"""
87+
Ball (kulka) detection algorithm.
88+
89+
Args:
90+
diff: difference from the averaged background.
91+
frame: current color frame.
92+
"""
93+
# Erosion/dilatation morphology filtering:
94+
# Erosion removes small artefacts, but "thins" our difference.
95+
# Dilatation brings "thickness" back, but not on removed artefacts.
96+
kernel = np.ones((5,5),np.uint8)
97+
diff = cv2.erode(diff, kernel, iterations=1)
98+
diff = cv2.dilate(diff, kernel, iterations=3)
99+
100+
# Treshold "diff" to get a "mask" with our ball.
101+
status, mask = cv2.threshold(diff, 20, 255,
102+
cv2.THRESH_BINARY)
103+
104+
# Convert BGR frame to HSV to get "Hue".
105+
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
106+
107+
# Calculate histogram of the Hue (channel 0 of HSV)
108+
h_hist = cv2.calcHist([hsv], channels=[0],
109+
mask=mask, histSize=[6],
110+
ranges=[0, 179])
111+
112+
# Heuristic to differentiate red and non-red color (edges of histogram vs center)
113+
edge = h_hist[0] + h_hist[-1]
114+
center = sum(h_hist) - edge
115+
116+
if abs(edge - center) < 200:
117+
# Difference not big enough.
118+
print("INVALID KULKA", edge, center)
119+
return None
120+
if edge > 2000:
121+
# Red ball found.
122+
print("RED KULKA", edge, center)
123+
servo.set(1800)
124+
else:
125+
# Non-red ball found.
126+
print("NON-RED KULKA", edge, center)
127+
servo.set(1500)
128+
129+
130+
def main_loop(filename):
131+
"Loop and detect"
132+
if '/dev/' in filename:
133+
# A bit hacky, but works.
134+
capture = Capture(camera=filename)
135+
else:
136+
capture = Capture(filename)
137+
138+
# Stats
139+
start = time()
140+
frame_no = 0
141+
142+
# "Motion detected" frame counter.
143+
det_cnt = 0
144+
145+
# Previous frame (for motion detection)
146+
prev_frame = None
147+
148+
# Averaged background (for ball-mask calculation)
149+
background = None
150+
151+
for frame in capture.frames():
152+
# Let's count FPS
153+
frame_no += 1
154+
if frame_no % 10 == 0:
155+
took = time() - start
156+
print("fps: %.2f" % (frame_no/took))
157+
158+
# Get grey frame to speed up "motion detection" on rPI. 1 channel needs
159+
# to be calculated later, not 3 of them.
160+
grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
161+
162+
if prev_frame is None:
163+
# Initialize previous frame and background.
164+
prev_frame = grey
165+
background = grey
166+
continue
167+
168+
# absdiff calculates absolute difference on each pixel without problems
169+
# of saturation of overflowing int8 (0-255) ranges.
170+
diff = cv2.absdiff(grey, prev_frame)
171+
172+
# Saturated subtraction - when subtracting 30 from 20 we get 0. So we
173+
# remove all the low-value noise from the diff.
174+
diff = cv2.subtract(diff, 30)
175+
176+
# Calculate total brightness of all not-filtered pixels on the
177+
# difference image.
178+
diff_total = diff.sum()
179+
180+
if diff_total > 1000:
181+
# Difference exceeds some threshold - movement detected, count frames.
182+
det_cnt += 1
183+
else:
184+
# No movement - reset movement counters.
185+
det_cnt = 0
186+
# And "running average" our background.
187+
background = cv2.addWeighted(background, 0.9, grey, 0.1, 0)
188+
189+
if det_cnt == 3:
190+
# On Xth moving frame we detect the ball color.
191+
192+
# Calculate difference from the background - does better job then
193+
# difference on the two consecutive frames, because we get the
194+
# difference only where the ball IS and not - where is was on the
195+
# previous frame.
196+
diff = cv2.absdiff(background, grey)
197+
198+
# That's how you can display some frame:
199+
#cv2.imshow("NEW DIFF", diff)
200+
201+
# Call ball detection algorithm.
202+
detect_kulka(frame, diff)
203+
204+
prev_frame = grey
205+
206+
# Remove imshows if running on rpi.
207+
cv2.imshow("Frame", frame)
208+
209+
x = cv2.waitKey(25)
210+
if x == ord('q'):
211+
break
212+
213+
# You can easily save some interesting frames to the disc.
214+
#output.write(frame)
215+
216+
# Destroy windows. If any.
217+
cv2.destroyAllWindows()
218+
219+
220+
if __name__ == "__main__":
221+
try:
222+
main_loop(sys.argv[1])
223+
except KeyboardInterrupt:
224+
print("Exiting on keyboard interrupt at", end=' ')

template.py

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Computer vision workshop - empty template for participants.
5+
6+
Prepared for "Exatel Security Days Programming Workshop" @07.06.2019
7+
Author: Tomasz bla Fortuna.
8+
License: Apache
9+
"""
10+
11+
import sys
12+
from time import time
13+
14+
import cv2
15+
import numpy as np
16+
17+
18+
class Servo:
19+
"""
20+
Servo control using PWM on Raspberry PI.
21+
"""
22+
23+
def __init__(self, servo_pin=18):
24+
try:
25+
import pigpio
26+
self.pi = pigpio.pi()
27+
except ImportError:
28+
print("No pigpio - simulating SERVO")
29+
self.pi = None
30+
31+
def set(self, value):
32+
if self.pi is not None:
33+
self.pi.set_servo_pulsewidth(18, value)
34+
35+
36+
servo = Servo()
37+
38+
39+
class Capture:
40+
"""
41+
Read data from camera and do initial, basic filtering.
42+
"""
43+
def __init__(self, filename=None, camera=None, size=None):
44+
"""
45+
Push filename if reading from file or camera if reading from camera. Don't use both.
46+
47+
Size if you want to force capture size.
48+
"""
49+
self.filename = filename
50+
self.camera = camera
51+
52+
if camera is not None:
53+
self.capture = cv2.VideoCapture(camera)
54+
55+
if size is not None:
56+
width, height = size
57+
self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, width)
58+
self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
59+
print("Set size to %dx%d" % (width, height))
60+
61+
elif filename is not None:
62+
self.capture = cv2.VideoCapture(filename)
63+
else:
64+
raise Exception("You failed at thinking")
65+
66+
self.width = int(self.capture.get(cv2.CAP_PROP_FRAME_WIDTH))
67+
self.height = int(self.capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
68+
69+
print("Initialized capturing device, size: %dx%d" % (self.width,
70+
self.height))
71+
72+
def frames(self):
73+
"Frame iterator - grab and yield frame."
74+
while True:
75+
start = time()
76+
(grabbed, frame) = self.capture.read()
77+
took = time() - start
78+
79+
if frame is None:
80+
break
81+
82+
yield frame
83+
84+
85+
def main_loop(filename):
86+
"Loop and detect"
87+
if '/dev/' in filename:
88+
# A bit hacky, but works.
89+
capture = Capture(camera=filename)
90+
else:
91+
capture = Capture(filename)
92+
93+
# Stats
94+
start = time()
95+
frame_no = 0
96+
97+
for frame in capture.frames():
98+
# Let's count FPS
99+
frame_no += 1
100+
if frame_no % 10 == 0:
101+
took = time() - start
102+
print("fps: %.2f" % (frame_no/took))
103+
104+
105+
cv2.imshow("Frame", frame)
106+
x = cv2.waitKey(25)
107+
if x == ord('q'):
108+
break
109+
110+
# Save if you want to.
111+
#output.write(frame)
112+
113+
# Destroy windows. If any.
114+
cv2.destroyAllWindows()
115+
116+
117+
if __name__ == "__main__":
118+
try:
119+
main_loop(sys.argv[1])
120+
except KeyboardInterrupt:
121+
print("Exiting on keyboard interrupt at", end=' ')

0 commit comments

Comments
 (0)