Skip to content

Commit 698aaef

Browse files
Copilotalexarje
andauthored
Implement GPU support for optical flow, face blurring, and pose estimation
Agent-Logs-Url: https://github.com/fourMs/MGT-python/sessions/37e34660-f746-453a-b428-74fc3f53285f Co-authored-by: alexarje <114316+alexarje@users.noreply.github.com>
1 parent 04146ab commit 698aaef

7 files changed

Lines changed: 139 additions & 12 deletions

File tree

musicalgestures/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ffmpeg_cmd,
1515
get_length,
1616
generate_outfilename,
17+
get_cuda_device_count,
1718
)
1819
from musicalgestures._mglist import MgList
1920

musicalgestures/_blurfaces.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def mg_blurfaces(self,
8080
save_data=True,
8181
data_format='csv',
8282
color=(0, 0, 0),
83+
use_gpu=True,
8384
target_name=None,
8485
overwrite=False):
8586
"""
@@ -101,6 +102,7 @@ def mg_blurfaces(self,
101102
save_data (bool, optional): Whether to save the scaled coordinates of the face mask (time (ms), x1, y1, x2, y2) for each frame to a file. Defaults to True.
102103
data_format (str, optional): Specifies format of blur_faces-data. Accepted values are 'csv', 'tsv' and 'txt'. For multiple output formats, use list, e.g. ['csv', 'txt']. Defaults to 'csv'.
103104
color (tuple, optional): Customized color of the rectangle boxes. Defaults to black (0, 0, 0).
105+
use_gpu (bool, optional): Whether to attempt GPU (CUDA) acceleration for face detection. Falls back to CPU automatically if CUDA is unavailable. Defaults to True.
104106
target_name (str, optional): Target output name. Defaults to None (which assumes that the input filename with the suffix "_blurred" should be used).
105107
overwrite (bool, optional): Whether to allow overwriting existing files or to automatically increment target filenames to avoid overwriting. Defaults to False.
106108
@@ -123,7 +125,7 @@ def mg_blurfaces(self,
123125
pb = MgProgressbar(total=self.length, prefix='Blurring faces:')
124126

125127
# Create an instance of the CenterFace class
126-
centerface = CenterFace()
128+
centerface = CenterFace(use_gpu=use_gpu)
127129
output_stream = cv2.VideoWriter(target_name, cv2.VideoWriter_fourcc('M','J','P','G'), self.fps, (self.width, self.height))
128130
# Create an empty list to append the mask coordinates
129131
data = []

musicalgestures/_centerface.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,24 @@
33
import numpy as np
44

55
import musicalgestures
6+
from musicalgestures._utils import get_cuda_device_count
67

78
class CenterFace(object):
89

9-
def __init__(self, landmarks=True):
10+
def __init__(self, landmarks=True, use_gpu=False):
1011

1112
module_path = os.path.abspath(os.path.dirname(musicalgestures.__file__))
1213

1314
self.landmarks = landmarks
1415
self.net = cv2.dnn.readNetFromONNX(module_path + '/models/centerface.onnx')
16+
17+
if use_gpu:
18+
if get_cuda_device_count() > 0:
19+
self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
20+
self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
21+
else:
22+
print('OpenCV CUDA backend is unavailable. CenterFace will use CPU.')
23+
1524
self.img_h_new, self.img_w_new, self.scale_h, self.scale_w = 0, 0, 0, 0
1625

1726
def __call__(self, img, height, width, threshold=0.5):

musicalgestures/_flow.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def dense(
4747
angle_of_view=0,
4848
scaledown=1,
4949
skip_empty=False,
50+
use_gpu=True,
5051
target_name=None,
5152
overwrite=False):
5253
"""
@@ -68,6 +69,7 @@ def dense(
6869
angle_of_view (int, optional): angle of view of camera, for reporting flow in meters per second. Defaults to 0.
6970
scaledown (int, optional): factor to scaledown frame size of the video. Defaults to 1.
7071
skip_empty (bool, optional): If True, repeats previous frame in the output when encounters an empty frame. Defaults to False.
72+
use_gpu (bool, optional): Whether to attempt GPU (CUDA) acceleration using `cv2.cuda.FarnebackOpticalFlow`. Falls back to CPU automatically if CUDA is unavailable or the required OpenCV CUDA modules are not installed. Defaults to True.
7173
target_name (str, optional): Target output name for the video. Defaults to None (which assumes that the input filename with the suffix "_flow_dense" should be used).
7274
overwrite (bool, optional): Whether to allow overwriting existing files or to automatically increment target filenames to avoid overwriting. Defaults to False.
7375
@@ -100,6 +102,28 @@ def dense(
100102

101103
size = (int(width/scaledown), int(height/scaledown))
102104

105+
# Determine whether to use GPU-accelerated Farneback optical flow
106+
from musicalgestures._utils import get_cuda_device_count
107+
_use_gpu = False
108+
farneback_gpu = None
109+
if use_gpu:
110+
if not hasattr(cv2.cuda, 'FarnebackOpticalFlow'):
111+
print('cv2.cuda.FarnebackOpticalFlow is unavailable (requires opencv-contrib built with CUDA). Switching to CPU for dense optical flow.')
112+
elif get_cuda_device_count() <= 0:
113+
print('OpenCV CUDA backend is unavailable. Switching to CPU for dense optical flow.')
114+
else:
115+
_use_gpu = True
116+
farneback_gpu = cv2.cuda.FarnebackOpticalFlow.create(
117+
numLevels=levels,
118+
pyrScale=pyr_scale,
119+
fastPyramids=False,
120+
winSize=winsize,
121+
numIters=iterations,
122+
polyN=poly_n,
123+
polySigma=poly_sigma,
124+
flags=flags,
125+
)
126+
103127
if velocity:
104128
pb = MgProgressbar(total=length, prefix='Rendering dense optical flow velocity:')
105129

@@ -118,6 +142,10 @@ def dense(
118142

119143
ret, frame1 = vidcap.read()
120144
prev_frame = cv2.cvtColor(cv2.resize(frame1, size), cv2.COLOR_BGR2GRAY)
145+
146+
if _use_gpu:
147+
gpu_prev_frame = cv2.cuda_GpuMat()
148+
gpu_prev_frame.upload(prev_frame)
121149

122150
prev_rgb = None
123151
hsv = np.zeros_like(frame1)
@@ -134,7 +162,14 @@ def dense(
134162
if ret == True:
135163
next_frame = cv2.cvtColor(cv2.resize(frame2, size), cv2.COLOR_BGR2GRAY)
136164

137-
flow = cv2.calcOpticalFlowFarneback(prev_frame, next_frame, None, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags)
165+
if _use_gpu:
166+
gpu_next_frame = cv2.cuda_GpuMat()
167+
gpu_next_frame.upload(next_frame)
168+
gpu_flow_result = farneback_gpu.calc(gpu_prev_frame, gpu_next_frame, None)
169+
flow = gpu_flow_result.download()
170+
gpu_prev_frame = gpu_next_frame
171+
else:
172+
flow = cv2.calcOpticalFlowFarneback(prev_frame, next_frame, None, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags)
138173

139174
if velocity:
140175
# Cumulative sum of optical flow vectors
@@ -285,6 +320,7 @@ def sparse(
285320
of_max_level=2,
286321
of_criteria=(cv2.TERM_CRITERIA_EPS |
287322
cv2.TERM_CRITERIA_COUNT, 10, 0.03),
323+
use_gpu=True,
288324
target_name=None,
289325
overwrite=False):
290326
"""
@@ -299,6 +335,7 @@ def sparse(
299335
of_win_size (tuple, optional): Size of the search window at each pyramid level. Defaults to (15, 15).
300336
of_max_level (int, optional): 0-based maximal pyramid level number. If set to 0, pyramids are not used (single level), if set to 1, two levels are used, and so on. If pyramids are passed to input then the algorithm will use as many levels as pyramids have but no more than `maxLevel`. Defaults to 2.
301337
of_criteria (tuple, optional): Specifies the termination criteria of the iterative search algorithm (after the specified maximum number of iterations criteria.maxCount or when the search window moves by less than criteria.epsilon). Defaults to (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03).
338+
use_gpu (bool, optional): Whether to attempt GPU (CUDA) acceleration using `cv2.cuda.SparsePyrLKOpticalFlow`. Falls back to CPU automatically if CUDA is unavailable or the required OpenCV CUDA modules are not installed. Defaults to True.
302339
target_name (str, optional): Target output name for the video. Defaults to None (which assumes that the input filename with the suffix "_flow_sparse" should be used).
303340
overwrite (bool, optional): Whether to allow overwriting existing files or to automatically increment target filenames to avoid overwriting. Defaults to False.
304341
@@ -330,6 +367,24 @@ def sparse(
330367
height = int(vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
331368
length = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
332369

370+
# Determine whether to use GPU-accelerated sparse optical flow
371+
from musicalgestures._utils import get_cuda_device_count
372+
_use_gpu = False
373+
lk_gpu = None
374+
if use_gpu:
375+
if not hasattr(cv2.cuda, 'SparsePyrLKOpticalFlow'):
376+
print('cv2.cuda.SparsePyrLKOpticalFlow is unavailable (requires opencv-contrib built with CUDA). Switching to CPU for sparse optical flow.')
377+
elif get_cuda_device_count() <= 0:
378+
print('OpenCV CUDA backend is unavailable. Switching to CPU for sparse optical flow.')
379+
else:
380+
_use_gpu = True
381+
iters = of_criteria[1] if len(of_criteria) > 1 else 10
382+
lk_gpu = cv2.cuda.SparsePyrLKOpticalFlow.create(
383+
winSize=of_win_size,
384+
maxLevel=of_max_level,
385+
iters=iters,
386+
)
387+
333388
pb = MgProgressbar(
334389
total=length, prefix='Rendering sparse optical flow video:')
335390

@@ -362,6 +417,12 @@ def sparse(
362417
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
363418
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
364419

420+
if _use_gpu:
421+
gpu_old_gray = cv2.cuda_GpuMat()
422+
gpu_old_gray.upload(old_gray)
423+
gpu_p0 = cv2.cuda_GpuMat()
424+
gpu_p0.upload(p0)
425+
365426
# Create a mask image for drawing purposes
366427
mask = np.zeros_like(old_frame)
367428

@@ -373,8 +434,16 @@ def sparse(
373434
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
374435

375436
# calculate optical flow
376-
p1, st, err = cv2.calcOpticalFlowPyrLK(
377-
old_gray, frame_gray, p0, None, **lk_params)
437+
if _use_gpu:
438+
gpu_frame_gray = cv2.cuda_GpuMat()
439+
gpu_frame_gray.upload(frame_gray)
440+
gpu_p1, gpu_st = lk_gpu.calc(gpu_old_gray, gpu_frame_gray, gpu_p0, None, None)
441+
p1 = gpu_p1.download()
442+
st = gpu_st.download()
443+
gpu_old_gray = gpu_frame_gray
444+
else:
445+
p1, st, err = cv2.calcOpticalFlowPyrLK(
446+
old_gray, frame_gray, p0, None, **lk_params)
378447

379448
# Select good points
380449
good_new = p1[st == 1]
@@ -400,6 +469,9 @@ def sparse(
400469
# Now update the previous frame and previous points
401470
old_gray = frame_gray.copy()
402471
p0 = good_new.reshape(-1, 1, 2)
472+
if _use_gpu:
473+
gpu_p0 = cv2.cuda_GpuMat()
474+
gpu_p0.upload(p0)
403475

404476
else:
405477
pb.progress(length)

musicalgestures/_pose.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import numpy as np
66
import pandas as pd
7-
from musicalgestures._utils import MgProgressbar, convert_to_avi, extract_wav, embed_audio_in_video, roundup, frame2ms, generate_outfilename, in_colab, ffmpeg_cmd
7+
from musicalgestures._utils import MgProgressbar, convert_to_avi, extract_wav, embed_audio_in_video, roundup, frame2ms, generate_outfilename, in_colab, get_cuda_device_count, ffmpeg_cmd
88
import musicalgestures
99
import itertools
1010

@@ -104,12 +104,7 @@ def pose(
104104
print('Sorry, OpenCV GPU acceleration is not supported in Colab. Switching to CPU.')
105105
device = 'cpu'
106106
elif device == 'gpu':
107-
cuda_devices = 0
108-
try:
109-
cuda_devices = cv2.cuda.getCudaEnabledDeviceCount()
110-
except Exception:
111-
cuda_devices = 0
112-
if cuda_devices <= 0:
107+
if get_cuda_device_count() <= 0:
113108
print('OpenCV CUDA backend is unavailable. Switching to CPU.')
114109
device = 'cpu'
115110

musicalgestures/_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,6 +1616,21 @@ def unwrap_str(string):
16161616
return string
16171617

16181618

1619+
def get_cuda_device_count():
1620+
"""
1621+
Returns the number of CUDA-capable GPU devices visible to OpenCV.
1622+
1623+
Returns:
1624+
int: Number of available CUDA devices, or 0 if the OpenCV CUDA
1625+
module is unavailable or no devices are detected.
1626+
"""
1627+
import cv2
1628+
try:
1629+
return cv2.cuda.getCudaEnabledDeviceCount()
1630+
except Exception:
1631+
return 0
1632+
1633+
16191634
def in_colab():
16201635
"""
16211636
Check's if the environment is a Google Colab document.

tests/test_flow.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ def test_with_target_name(self, testvideo_avi):
5454
assert type(result) == musicalgestures.MgVideo
5555
assert os.path.isfile(result.filename) == True
5656

57+
def test_use_gpu_true(self, testvideo_avi):
58+
# use_gpu=True should work (falls back to CPU when CUDA is unavailable)
59+
mg = musicalgestures.MgVideo(testvideo_avi)
60+
result = mg.flow.dense(use_gpu=True, overwrite=True)
61+
assert type(result) == musicalgestures.MgVideo
62+
assert os.path.isfile(result.filename) == True
63+
64+
def test_use_gpu_false(self, testvideo_avi):
65+
mg = musicalgestures.MgVideo(testvideo_avi)
66+
result = mg.flow.dense(use_gpu=False, overwrite=True)
67+
assert type(result) == musicalgestures.MgVideo
68+
assert os.path.isfile(result.filename) == True
69+
5770

5871
class Test_flow_sparse:
5972
def test_normal_case(self, testvideo_avi):
@@ -80,3 +93,23 @@ def test_with_target_name(self, testvideo_avi):
8093
result = mg.flow.sparse(target_name=target_name, overwrite=True)
8194
assert type(result) == musicalgestures.MgVideo
8295
assert os.path.isfile(result.filename) == True
96+
97+
def test_use_gpu_true(self, testvideo_avi):
98+
# use_gpu=True should work (falls back to CPU when CUDA is unavailable)
99+
mg = musicalgestures.MgVideo(testvideo_avi)
100+
result = mg.flow.sparse(use_gpu=True, overwrite=True)
101+
assert type(result) == musicalgestures.MgVideo
102+
assert os.path.isfile(result.filename) == True
103+
104+
def test_use_gpu_false(self, testvideo_avi):
105+
mg = musicalgestures.MgVideo(testvideo_avi)
106+
result = mg.flow.sparse(use_gpu=False, overwrite=True)
107+
assert type(result) == musicalgestures.MgVideo
108+
assert os.path.isfile(result.filename) == True
109+
110+
111+
class Test_get_cuda_device_count:
112+
def test_returns_int(self):
113+
result = musicalgestures.get_cuda_device_count()
114+
assert isinstance(result, int)
115+
assert result >= 0

0 commit comments

Comments
 (0)