7.5 MiB
Widzenie komputerowe
07. Analiza wideo: przepływ optyczny, śledzenie obiektów [laboratoria]
Andrzej Wójtowicz (2021)
W poniższych materiałach zobaczymy w jaki sposób możemy przy pomocy przepływu optycznego dokonać stabilizacji obrazu oraz w jaki sposób śledzić obiekty znajdujące się na filmie.
Na początku załadujmy niezbędne biblioteki.
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import IPython.display
Przepływ optyczny
Naszym celem będzie znalezienie na poniższym filmie punktów kluczowych, które pozwolą nam w jakiś sposób sprawdzić jak przemieszcza się rowerzystka:
IPython.display.Video("vid/bike.mp4", width=800)
Załadujmy film:
bike = cv.VideoCapture("vid/bike.mp4")
Przy pomocy algorytmu Shi-Tomasi (rozwinięcie metody Harrisa) możemy znaleźć narożniki, które dobrze nadają się do śledzenia. W OpenCV algorytm jest zaimplementowany w funkcji cv.goodFeaturesToTrack()
:
corners_num = 100
corners_colors = np.random.randint(0, 255, (corners_num, 3))
_, frame_1 = bike.read()
frame_1_gray = cv.cvtColor(frame_1, cv.COLOR_BGR2GRAY)
keypoints_1 = cv.goodFeaturesToTrack(
frame_1_gray, mask=None, maxCorners=corners_num,
qualityLevel=0.3, minDistance=7, blockSize=7)
mask = np.zeros_like(frame_1)
count = 0
Aby sprawdzić w jaki sposób punkty przemieszczają się pomiędzy kolejnymi klatkami filmu, wykorzystamy algorytm Lucasa–Kanade, który jest zaimplementowany w funkcji cv.calcOpticalFlowPyrLK()
:
while True:
_, frame_2 = bike.read()
frame_2_gray = cv.cvtColor(frame_2, cv.COLOR_BGR2GRAY)
count += 1
keypoints_2, status, _ = cv.calcOpticalFlowPyrLK(
frame_1_gray, frame_2_gray, keypoints_1, None, winSize=(15, 15),
maxLevel=2, criteria=(cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03))
keypoints_2_good = keypoints_2[status==1]
keypoints_1_good = keypoints_1[status==1]
for i, (kp2, kp1) in enumerate(zip(keypoints_2_good, keypoints_1_good)):
a, b = kp2.ravel()
a, b = int(a), int(b)
c, d = kp1.ravel()
c, d = int(c), int(d)
cv.line(mask, (a, b), (c, d), corners_colors[i].tolist(), 8, cv.LINE_AA)
cv.circle(frame_2, (a ,b), 9, corners_colors[i].tolist(), -1)
display_frame = cv.add(frame_2, mask)
if count % 5 == 0:
plt.figure(figsize=(7,7))
plt.imshow(display_frame[:,:,::-1])
if count > 40:
break
frame_1_gray = frame_2_gray.copy()
keypoints_1 = keypoints_2_good.reshape(-1,1,2)
bike.release()
Możemy zauważyć, że część punktów kluczowych została wykryta poza głównym śledzonym obiektem, jednak mimo wszystko jesteśmy w stanie określić główny ruch przemieszczającego się obiektu.
Stabilizacja obrazu
Spróbujemy wykorzystać przepływ optyczny do stablizacji cyfrowej filmu nakręconego z ręki:
IPython.display.Video("vid/protest.mp4", width=800)
Załadujmy film oraz przygotujmy film wyjściowy, który będziemy wyświetlać obok oryginalnego, tak aby móc porównać otrzymane wyniki:
cap = cv.VideoCapture("vid/protest.mp4")
n_frames = int(cap.get(cv.CAP_PROP_FRAME_COUNT))
width = int(cap.get(cv.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv.CAP_PROP_FPS)
out = cv.VideoWriter('vid/gen-protest.avi', cv.VideoWriter_fourcc(*'MJPG'), fps, (width*2, height))
Pomiędzy poszczególnymi klatkami filmu znajdujemy punkty kluczowe i śledzimy w jaki sposób się one przemieściły. Na tej podstawie przy pomocy cv.estimateAffinePartial2D()
możemy oszacować transformacje (translację oraz obrót), które nastapiły między następującymi po sobie klatkami:
_, prev = cap.read()
prev_gray = cv.cvtColor(prev, cv.COLOR_BGR2GRAY)
transforms = np.zeros((n_frames-1, 3), np.float32)
for i in range(n_frames-2):
prev_pts = cv.goodFeaturesToTrack(prev_gray, maxCorners=200,
qualityLevel=0.01, minDistance=30, blockSize=3)
success, curr = cap.read()
if not success:
break
curr_gray = cv.cvtColor(curr, cv.COLOR_BGR2GRAY)
curr_pts, status, _ = cv.calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, None)
idx = np.where(status==1)[0]
prev_pts = prev_pts[idx]
curr_pts = curr_pts[idx]
mat, _ = cv.estimateAffinePartial2D(prev_pts, curr_pts)
# traslation
dx = mat[0,2]
dy = mat[1,2]
# rotation angle
da = np.arctan2(mat[1,0], mat[0,0])
transforms[i] = [dx,dy,da]
prev_gray = curr_gray
Przygotujemy też kilka funkcji pomocniczych. Posiadając serię transformacji wygładzimy ich poszczególne komponenty przy pomocy średniej kroczącej.
def moving_average(values, radius):
window_size = 2 * radius + 1
mask = np.ones(window_size)/window_size
values_padded = np.lib.pad(values, (radius, radius), 'edge')
values_smoothed = np.convolve(values_padded, mask, mode='same')
return values_smoothed[radius:-radius] # remove padding
def smooth(trajectory, radius=50):
smoothed_trajectory = np.copy(trajectory)
for i in range(smoothed_trajectory.shape[1]):
smoothed_trajectory[:,i] = moving_average(trajectory[:,i], radius)
return smoothed_trajectory
Możemy teraz policzyć jakie mieliśmy transformacje względem początku filmu, wygładzić je poprzez średnią kroczącą, a następnie nanieść wynikowe różnice na poszczególne transformacje:
trajectory = np.cumsum(transforms, axis=0)
smoothed_trajectory = smooth(trajectory)
difference = smoothed_trajectory - trajectory
transforms_smooth = transforms + difference
Ostatecznie na podstawie wygładzonych transformacji dostosowujemy poszczególne klatki filmu. Dodatkowo poprzez ustabilizowanie obrazu mogą pojawić się czarne obramowania na wynikowym obrazie, zatem poprzez niewielkie powiększenie obrazu zniwelujemy ten efekt:
cap.set(cv.CAP_PROP_POS_FRAMES, 0) # back to first frame
for i in range(n_frames-2):
success, frame = cap.read()
if not success:
break
dx = transforms_smooth[i,0]
dy = transforms_smooth[i,1]
da = transforms_smooth[i,2]
mat = np.zeros((2,3), np.float32)
mat[0,0] = np.cos(da)
mat[0,1] = -np.sin(da)
mat[1,0] = np.sin(da)
mat[1,1] = np.cos(da)
mat[0,2] = dx
mat[1,2] = dy
frame_stabilized = cv.warpAffine(frame, mat, (width, height))
mat = cv.getRotationMatrix2D((width/2, height/2), 0, 1.1)
frame_stabilized = cv.warpAffine(frame_stabilized, mat, (width, height))
frame_out = cv.hconcat([frame, frame_stabilized]) # frame by frame
out.write(frame_out)
out.release()
Na potrzeby wyświetlenie wynikowego filmu w przeglądarce, użyjemy kodeka H264:
!ffmpeg -y -hide_banner -loglevel warning -nostats -i vid/gen-protest.avi -vcodec libx264 vid/gen-protest.mp4
Wynikowy film:
IPython.display.Video("vid/gen-protest.mp4", width=800)
Śledzenie obiektów
Załóżmy, że chcemy na poniższym filmie śledzić przemieszczanie się piłkarek:
IPython.display.Video("vid/football.mp4", width=800)
Biblioteka OpenCV posiada kilka algorytmów pozwalających na śledzenie obiektów. Poniżej użyjemy algorytmu _Multiple Instance Learning:
video = cv.VideoCapture("vid/football.mp4")
_, frame = video.read()
bbox = (45, 350, 120, 270)
tracker = cv.legacy.TrackerMIL_create()
tracker.init(frame, bbox)
pt_1 = (int(bbox[0]), int(bbox[1]))
pt_2 = (int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3]))
cv.rectangle(frame, pt_1, pt_2, (0, 0, 255), 4, cv.LINE_8)
plt.figure(figsize=(7,7))
plt.imshow(frame[:,:,::-1]);
Możemy sprawdzić wyniki pomiędzy poszczególnymi klatkami, jednak tutaj na potrzeby prezentacji dodajmy odstęp co 10 klatek aby można było zauwazyć ruch. Dodatkowo możemy sprawdzić względną prędkość działania algorytmu:
count = 50
while count > 0:
ok, frame = video.read()
if not ok:
break
timer = cv.getTickCount()
ok, bbox = tracker.update(frame)
fps = cv.getTickFrequency() / (cv.getTickCount() - timer);
if ok:
pt_1 = (int(bbox[0]), int(bbox[1]))
pt_2 = (int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3]))
cv.rectangle(frame, pt_1, pt_2, (0,0,255), 4, cv.LINE_8)
else :
cv.putText(frame, "Tracking failure", (20, 180),
cv.FONT_HERSHEY_SIMPLEX, 2, (0,0,255), cv.LINE_AA)
cv.putText(frame, "FPS : " + str(int(fps)), (20,50),
cv.FONT_HERSHEY_SIMPLEX, 2, (0,0,255), cv.LINE_AA)
if count % 10 == 0:
plt.figure(figsize=(7,7))
plt.imshow(frame[:,:,::-1])
count -= 1
video.release()
Istnieje też możliwość jednoczesnego śledzenia kilku obiektów:
video = cv.VideoCapture("vid/football.mp4")
_, frame = video.read()
bboxes = [(45, 350, 120, 270), (755, 350, 120, 270)]
colors = [(0, 0, 255), (0, 255, 0)]
multi_tracker = cv.legacy.MultiTracker_create()
for bbox in bboxes:
multi_tracker.add(cv.legacy.TrackerMIL_create(), frame, bbox)
count = 50
while count > 0:
ok, frame = video.read()
if not ok:
break
timer = cv.getTickCount()
_, boxes = multi_tracker.update(frame)
for i, bbox in enumerate(boxes):
pt_1 = (int(bbox[0]), int(bbox[1]))
pt_2 = (int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3]))
cv.rectangle(frame, pt_1, pt_2, colors[i], 4, cv.LINE_8)
if count % 10 == 0:
plt.figure(figsize=(7,7))
plt.imshow(frame[:,:,::-1])
count -= 1
video.release()
Zadanie 1
Dla filmu vid/football.mp4
porównaj jakość śledzenia dla dostępnych algorytmów. Wyniki zapisz na jednym filmie.
video = cv.VideoCapture("vid/football.mp4")
_, frame = video.read()
trackers = []
bbox = (45, 350, 120, 270)
tracker_mil = cv.legacy.TrackerMIL_create()
tracker_mil.init(frame, bbox)
trackers.append((tracker_mil, 'MIL'))
tracker_b = cv.legacy.TrackerBoosting_create()
tracker_b.init(frame, bbox)
trackers.append((tracker_b, 'BOOSTING'))
tracker_kcf = cv.legacy.TrackerKCF_create()
tracker_kcf.init(frame, bbox)
trackers.append((tracker_kcf, 'KCF'))
tracker_tld = cv.legacy.TrackerTLD_create()
tracker_tld.init(frame, bbox)
trackers.append((tracker_tld, 'TLD'))
tracker_mf = cv.legacy.TrackerMedianFlow_create()
tracker_mf.init(frame, bbox)
trackers.append((tracker_mf, 'MEDIANFLOW'))
tracker_m = cv.legacy.TrackerMOSSE_create()
tracker_m.init(frame, bbox)
trackers.append((tracker_m, 'MOOSE'))
tracker_csrt = cv.legacy.TrackerCSRT_create()
tracker_csrt.init(frame, bbox)
trackers.append((tracker_csrt, 'CSRT'))
pt_1 = (int(bbox[0]), int(bbox[1]))
pt_2 = (int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3]))
cv.rectangle(frame, pt_1, pt_2, (0, 0, 255), 4, cv.LINE_8)
plt.figure(figsize=(7,7))
plt.imshow(frame[:,:,::-1]);
import warnings
warnings.filterwarnings('ignore')
count = 50
rows, cols = 2, 4
ok, frame = video.read()
plt.figure(figsize=(20,5));
plt.subplot(rows, cols, 1);
plt.imshow(frame[:,:,::-1]);
plt.axis('off');
cnt = 2
for t in trackers:
video = cv.VideoCapture("vid/football.mp4")
count = 50
while count > 0:
ok, frame = video.read()
if not ok:
break
timer = cv.getTickCount()
ok, bbox = t[0].update(frame)
fps = cv.getTickFrequency() / (cv.getTickCount() - timer);
if ok:
pt_1 = (int(bbox[0]), int(bbox[1]))
pt_2 = (int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3]))
cv.rectangle(frame, pt_1, pt_2, (0,0,255), 4, cv.LINE_8)
else :
cv.putText(frame, "Tracking failure", (20, 180),
cv.FONT_HERSHEY_SIMPLEX, 2, (0,0,255), cv.LINE_AA)
cv.putText(frame, f"{t[1]} FPS : " + str(int(fps)), (20,50),
cv.FONT_HERSHEY_SIMPLEX, 2, (0,0,255), cv.LINE_AA)
if count % 26 == 0:
plt.subplot(rows, cols, cnt);
plt.imshow(frame[:,:,::-1]);
plt.axis('off');
cnt += 1
count -= 1
video.release()