-
Notifications
You must be signed in to change notification settings - Fork 1
/
camera_stream.py
339 lines (257 loc) · 12.1 KB
/
camera_stream.py
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
##########################################################################
# threaded frame capture from camera to avoid camera frame buffering delays
# (always delivers the latest frame from the camera)
# Copyright (c) 2018-2021 Toby Breckon, Durham University, UK
# Copyright (c) 2015-2016 Adrian Rosebrock, http://www.pyimagesearch.com
# MIT License (MIT)
# based on code from this tutorial, with changes to make object method call
# compatible with cv2.VideoCapture(src) as far as possible, optional OpenCV
# Transparent API support (disabled by default) and improved thread management:
# https://www.pyimagesearch.com/2015/12/21/increasing-webcam-fps-with-python-and-opencv/
##########################################################################
# suggested basic usage - as per example in canny.py found at:
# https://github.com/tobybreckon/python-examples-cv/blob/master/canny.py
# try:
# import camera_stream
# cap = camera_stream.CameraVideoStream()
# print("INFO: using CameraVideoStream() threaded capture")
# except BaseException:
# print("INFO: CameraVideoStream() module not found")
# cap = cv2.VideoCapture()
# in the above example this makes use of the CameraVideoStream if it is
# available (i.e. camera_stream.py is in the module search path) and
# falls back to using cv2.VideoCapture otherwise
# use with other OpenCV video backends just explicitly pass the required
# OpenCV flag as follows:
# ....
# import camera_stream
# cap = camera_stream.CameraVideoStream()
# ....
#
# cap.open("your | gstreamer | pipeline", cv2.CAP_GSTREAMER)
#
# Ref: https://docs.opencv.org/4.x/d4/d15/group__videoio__flags__base.html
# OpenCV T-API usage - alternative usage to enable OpenCV Transparent API
# h/w acceleration where available on all subsequent processing of image
# ....
# import camera_stream
# cap = camera_stream.CameraVideoStream(use_tapi=True)
# ....
##########################################################################
# import the necessary packages
from threading import Thread
import cv2
import sys
import atexit
import logging
##########################################################################
# handle older versions of OpenCV (that had a different constuctor
# prototype for cv2.VideoCapture() it appears) semi-gracefully
(majorCV, minorCV, _) = cv2.__version__.split(".")
if ((majorCV <= '3') and (minorCV <= '4')):
raise NameError('OpenCV version < 3.4,'
+ ' not compatible with CameraVideoStream()')
##########################################################################
# set up logging
log_level = logging.CRITICAL # change to .INFO / .DEBUG for useful info
log_msg_format = '%(asctime)s - Thead ID: %(thread)d - %(message)s'
logging.basicConfig(format=log_msg_format, level=log_level)
##########################################################################
# set up global variables and atexit() function to facilitate safe thread exit
# without a segfault from the VideoCapture object as experienced on some
# platforms
# (as __del__ and __exit__ are not called outside a 'with' construct)
exitingNow = False # global flag for program exit
threadList = [] # list of current threads (i.e. multi-camera/thread safe)
###########################
def closeDownAllThreadsCleanly():
global exitingNow
global threadList
# set exit flag to cause each thread to exit
exitingNow = True
# for each thread wait for it to exit
for thread in threadList:
thread.join()
###########################
atexit.register(closeDownAllThreadsCleanly)
##########################################################################
class CameraVideoStream:
def __init__(self, src=None, apiPreference=None,
name="CameraVideoStream", use_tapi=False):
# initialize the thread name
self.name = name
# initialize the variables used to indicate if the thread should
# be stopped or suspended
self.stopped = False
self.suspend = False
# set these to null values initially
self.grabbed = 0
self.frame = None
# set the initial timestamps to zero
self.timestamp = 0
self.timestamp_last_read = 0
self.use_timestamps = False
# set internal framecounters to -1
self.framecounter = -1
self.framecounter_last_read = -1
# set OpenCV Transparent API usage
self.tapi = use_tapi
# set some sensible backends for real-time video capture from
# directly connected hardware on a per-OS basis,
# that can we overidden via the open() method
# + remember timestamps only seem to work on linux
if sys.platform.startswith('linux'): # all Linux
self.backend_default = cv2.CAP_V4L
self.use_timestamps = True
elif sys.platform.startswith('win'): # MS Windows
self.backend_default = cv2.CAP_DSHOW
self.use_timestamps = False
elif sys.platform.startswith('darwin'): # macOS
self.backend_default = cv2.CAP_AVFOUNDATION
self.use_timestamps = False
else:
self.backend_default = cv2.CAP_ANY # auto-detect via OpenCV
# if a source was specified at init, proceed to open device
if not (src is None):
self.open(src, apiPreference)
if not (apiPreference == cv2.CAP_V4L):
self.use_timestamps = False
def open(self, src=0, apiPreference=None):
# determine backend to specified by user
if (apiPreference is None):
apiPreference = self.backend_default
# check if aleady opened via init method
if (self.grabbed > 0):
return True
# initialize the video camera stream
self.camera = cv2.VideoCapture(src, apiPreference)
# when the backend is v4l (linux) set the buffer size to 1
# (as this is implemented for this backend and not others)
if (apiPreference == cv2.CAP_V4L):
self.camera.set(cv2.CAP_PROP_BUFFERSIZE, 1)
# read the first frame from the stream (and its timestamp)
(self.grabbed, self.frame) = self.camera.read()
self.timestamp = self.camera.get(cv2.CAP_PROP_POS_MSEC)
self.framecounter += 1
logging.info("GRAB - frame %d @ time %f",
self.framecounter, self.timestamp)
# only start the thread if in-fact the camera read was successful
if (self.grabbed):
# create the thread to read frames from the video stream
thread = Thread(target=self.update, name=self.name, args=())
# append thread to global array of threads
threadList.append(thread)
# get thread id we will use to address thread on list
self.threadID = len(threadList) - 1
# start thread and set it to run in background
threadList[self.threadID].daemon = True
threadList[self.threadID].start()
return (self.grabbed > 0)
def update(self):
# keep looping infinitely until the thread is stopped
while True:
# if the thread indicator variable is set or exiting, stop the
# thread
if self.stopped or exitingNow:
self.grabbed = 0 # set flag to ensure isOpen() returns False
self.camera.release() # cleanly release camera hardware
return
# otherwise, read the next frame from the stream
# provided we are not suspended (and get timestamp)
if not (self.suspend):
self.camera.grab()
latest_timestamp = self.camera.get(cv2.CAP_PROP_POS_MSEC)
if ((latest_timestamp > self.timestamp)
or (self.use_timestamps is False)):
(self.grabbed, self.frame) = self.camera.retrieve()
self.framecounter += 1
logging.info("GRAB - frame %d @ time %f",
self.framecounter, latest_timestamp)
logging.debug("GRAB - inter-frame diff (ms) %f",
latest_timestamp - self.timestamp)
self.timestamp = latest_timestamp
else:
logging.info("GRAB - same timestamp skip %d",
latest_timestamp)
def grab(self):
# return status of most recent grab by the thread
return self.grabbed
def retrieve(self):
# same as read() in the context of threaded capture
return self.read()
def read(self):
# remember the timestamp/count of the lastest image returned by read()
# so that subsequent calls to .get() can return the timestamp
# that is consistent with the last image the caller got via read()
frame_offset = (self.framecounter - self.framecounter_last_read)
self.timestamp_last_read = self.timestamp
self.framecounter_last_read = self.framecounter
for skip in range(1, frame_offset):
logging.info("SKIP - frame %d", self.framecounter_last_read
- frame_offset + skip)
logging.info("READ - frame %d @ time %f",
self.framecounter, self.timestamp)
# return the frame most recently read
if (self.tapi):
# return OpenCV Transparent API UMat frame for H/W acceleration
return (self.grabbed, cv2.UMat(self.frame))
# return standard numpy frame
return (self.grabbed, self.frame)
def isOpened(self):
# indicate that the camera is open successfully
return (self.grabbed > 0)
def release(self):
# indicate that the thread should be stopped
self.stopped = True
def set(self, property_name, property_value):
# set a video capture property (behavior as per OpenCV manual for
# VideoCapture)
# first suspend thread
self.suspend = True
# set value - wrapping it in grabs() so it takes effect
self.camera.grab()
ret_val = self.camera.set(property_name, property_value)
self.camera.grab()
# whilst we are still suspended flush the frame buffer held inside
# the object by reading a new frame with new settings otherwise a race
# condition will exist between the thread's next call to update() after
# it un-suspends and the next call to read() by the object user
(self.grabbed, self.frame) = self.camera.read()
self.timestamp = self.camera.get(cv2.CAP_PROP_POS_MSEC)
self.framecounter += 1
logging.info("GRAB - frame %d @ time %f",
self.framecounter, self.timestamp)
# restart thread by unsuspending it
self.suspend = False
return ret_val
def get(self, property_name):
# get a video capture property
# intercept calls to get the current timestamp or frame nunber
# of the frame and explicitly return that of the last image
# returned to the caller via read() or retrieve() from this object
if (property_name == cv2.CAP_PROP_POS_MSEC):
return self.timestamp_last_read
elif (property_name == cv2.CAP_PROP_POS_FRAMES):
return self.framecounter_last_read
# default to behavior as per OpenCV manual for
# VideoCapture()
return self.camera.get(property_name)
def getBackendName(self):
# get a video capture backend (behavior as per OpenCV manual for
# VideoCapture)
return self.camera.getBackendName()
def getExceptionMode(self):
# get a video capture exception mode (behavior as per OpenCV manual for
# VideoCapture)
return self.camera.getExceptionMode()
def setExceptionMode(self, enable):
# get a video capture exception mode (behavior as per OpenCV manual for
# VideoCapture)
return self.camera.setExceptionMode(enable)
def __del__(self):
self.stopped = True
self.suspend = True
def __exit__(self, exec_type, exc_value, traceback):
self.stopped = True
self.suspend = True
##########################################################################