{ "cells": [ { "cell_type": "markdown", "id": "73e26798", "metadata": {}, "source": [ "![Logo 1](img/aitech-logotyp-1.jpg)\n", "
\n", "

Widzenie komputerowe

\n", "

07. Analiza wideo: przepływ optyczny, śledzenie obiektów [laboratoria]

\n", "

Andrzej Wójtowicz (2021)

\n", "
\n", "\n", "![Logo 2](img/aitech-logotyp-2.jpg)" ] }, { "cell_type": "markdown", "id": "f124f5cd", "metadata": {}, "source": [ "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.\n", "\n", "Na początku załadujmy niezbędne biblioteki." ] }, { "cell_type": "code", "execution_count": null, "id": "ed69629c", "metadata": {}, "outputs": [], "source": [ "import cv2 as cv\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", "import IPython.display" ] }, { "cell_type": "markdown", "id": "f9ef8d67", "metadata": {}, "source": [ "# Przepływ optyczny\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "id": "1629fc29", "metadata": {}, "outputs": [], "source": [ "IPython.display.Video(\"vid/bike.mp4\", width=800)" ] }, { "cell_type": "markdown", "id": "aa176c9a", "metadata": {}, "source": [ "Załadujmy film:" ] }, { "cell_type": "code", "execution_count": null, "id": "2463bd1d", "metadata": {}, "outputs": [], "source": [ "bike = cv.VideoCapture(\"vid/bike.mp4\")" ] }, { "cell_type": "markdown", "id": "dde041a3", "metadata": {}, "source": [ "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()`](https://docs.opencv.org/4.5.3/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541):" ] }, { "cell_type": "code", "execution_count": null, "id": "36492aa6", "metadata": {}, "outputs": [], "source": [ "corners_num = 100\n", "corners_colors = np.random.randint(0, 255, (corners_num, 3))\n", "\n", "_, frame_1 = bike.read()\n", "frame_1_gray = cv.cvtColor(frame_1, cv.COLOR_BGR2GRAY)\n", "keypoints_1 = cv.goodFeaturesToTrack(\n", " frame_1_gray, mask=None, maxCorners=corners_num,\n", " qualityLevel=0.3, minDistance=7, blockSize=7)\n", "\n", "mask = np.zeros_like(frame_1)\n", "count = 0" ] }, { "cell_type": "markdown", "id": "028dded7", "metadata": {}, "source": [ "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()`](https://docs.opencv.org/4.5.3/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323):" ] }, { "cell_type": "code", "execution_count": null, "id": "14b62820", "metadata": {}, "outputs": [], "source": [ "while True:\n", " _, frame_2 = bike.read()\n", " frame_2_gray = cv.cvtColor(frame_2, cv.COLOR_BGR2GRAY)\n", " count += 1\n", "\n", " keypoints_2, status, _ = cv.calcOpticalFlowPyrLK(\n", " frame_1_gray, frame_2_gray, keypoints_1, None, winSize=(15, 15),\n", " maxLevel=2, criteria=(cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03))\n", "\n", " keypoints_2_good = keypoints_2[status==1]\n", " keypoints_1_good = keypoints_1[status==1]\n", " \n", " for i, (kp2, kp1) in enumerate(zip(keypoints_2_good, keypoints_1_good)):\n", " a, b = kp2.ravel()\n", " a, b = int(a), int(b)\n", " c, d = kp1.ravel()\n", " c, d = int(c), int(d)\n", " cv.line(mask, (a, b), (c, d), corners_colors[i].tolist(), 8, cv.LINE_AA)\n", " cv.circle(frame_2, (a ,b), 9, corners_colors[i].tolist(), -1)\n", " \n", " display_frame = cv.add(frame_2, mask)\n", " if count % 5 == 0:\n", " plt.figure(figsize=(7,7))\n", " plt.imshow(display_frame[:,:,::-1])\n", " if count > 40:\n", " break\n", "\n", " frame_1_gray = frame_2_gray.copy()\n", " keypoints_1 = keypoints_2_good.reshape(-1,1,2)\n", " \n", "bike.release()" ] }, { "cell_type": "markdown", "id": "d8c01f59", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "id": "879a813e", "metadata": {}, "source": [ "## Stabilizacja obrazu\n", "\n", "Spróbujemy wykorzystać przepływ optyczny do stablizacji cyfrowej filmu nakręconego z ręki:" ] }, { "cell_type": "code", "execution_count": null, "id": "d8686953", "metadata": {}, "outputs": [], "source": [ "IPython.display.Video(\"vid/protest.mp4\", width=800)" ] }, { "cell_type": "markdown", "id": "c3541db4", "metadata": {}, "source": [ "Załadujmy film oraz przygotujmy film wyjściowy, który będziemy wyświetlać obok oryginalnego, tak aby móc porównać otrzymane wyniki:" ] }, { "cell_type": "code", "execution_count": null, "id": "0b18aad1", "metadata": {}, "outputs": [], "source": [ "cap = cv.VideoCapture(\"vid/protest.mp4\")\n", "n_frames = int(cap.get(cv.CAP_PROP_FRAME_COUNT))\n", "width = int(cap.get(cv.CAP_PROP_FRAME_WIDTH)) \n", "height = int(cap.get(cv.CAP_PROP_FRAME_HEIGHT))\n", "fps = cap.get(cv.CAP_PROP_FPS)\n", "\n", "out = cv.VideoWriter('vid/gen-protest.avi', cv.VideoWriter_fourcc(*'MJPG'), fps, (width*2, height))" ] }, { "cell_type": "markdown", "id": "27355bd0", "metadata": {}, "source": [ "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()`](https://docs.opencv.org/4.5.3/d9/d0c/group__calib3d.html#gad767faff73e9cbd8b9d92b955b50062d) możemy oszacować transformacje (translację oraz obrót), które nastapiły między następującymi po sobie klatkami:" ] }, { "cell_type": "code", "execution_count": null, "id": "c00b1e9d", "metadata": {}, "outputs": [], "source": [ "_, prev = cap.read()\n", "prev_gray = cv.cvtColor(prev, cv.COLOR_BGR2GRAY)\n", "\n", "transforms = np.zeros((n_frames-1, 3), np.float32)\n", "\n", "for i in range(n_frames-2):\n", " prev_pts = cv.goodFeaturesToTrack(prev_gray, maxCorners=200,\n", " qualityLevel=0.01, minDistance=30, blockSize=3)\n", " \n", " success, curr = cap.read() \n", " if not success: \n", " break\n", " curr_gray = cv.cvtColor(curr, cv.COLOR_BGR2GRAY) \n", " \n", " curr_pts, status, _ = cv.calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, None)\n", " \n", " idx = np.where(status==1)[0]\n", " prev_pts = prev_pts[idx]\n", " curr_pts = curr_pts[idx]\n", " \n", " mat, _ = cv.estimateAffinePartial2D(prev_pts, curr_pts)\n", " # traslation\n", " dx = mat[0,2]\n", " dy = mat[1,2]\n", " # rotation angle\n", " da = np.arctan2(mat[1,0], mat[0,0])\n", " \n", " transforms[i] = [dx,dy,da]\n", " \n", " prev_gray = curr_gray" ] }, { "cell_type": "markdown", "id": "ba9fce7d", "metadata": {}, "source": [ "Przygotujemy też kilka funkcji pomocniczych. Posiadając serię transformacji wygładzimy ich poszczególne komponenty przy pomocy średniej kroczącej." ] }, { "cell_type": "code", "execution_count": null, "id": "0fd89a26", "metadata": {}, "outputs": [], "source": [ "def moving_average(values, radius): \n", " window_size = 2 * radius + 1 \n", " mask = np.ones(window_size)/window_size \n", "\n", " values_padded = np.lib.pad(values, (radius, radius), 'edge') \n", " values_smoothed = np.convolve(values_padded, mask, mode='same') \n", " \n", " return values_smoothed[radius:-radius] # remove padding\n", "\n", "def smooth(trajectory, radius=50): \n", " smoothed_trajectory = np.copy(trajectory) \n", " for i in range(smoothed_trajectory.shape[1]):\n", " smoothed_trajectory[:,i] = moving_average(trajectory[:,i], radius)\n", "\n", " return smoothed_trajectory" ] }, { "cell_type": "markdown", "id": "9f4d1df0", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": null, "id": "efb52c4d", "metadata": {}, "outputs": [], "source": [ "trajectory = np.cumsum(transforms, axis=0)\n", "smoothed_trajectory = smooth(trajectory) \n", "\n", "difference = smoothed_trajectory - trajectory\n", "transforms_smooth = transforms + difference" ] }, { "cell_type": "markdown", "id": "0ef3b313", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": null, "id": "b8d4528b", "metadata": {}, "outputs": [], "source": [ "cap.set(cv.CAP_PROP_POS_FRAMES, 0) # back to first frame\n", "\n", "for i in range(n_frames-2):\n", " success, frame = cap.read() \n", " if not success:\n", " break\n", "\n", " dx = transforms_smooth[i,0]\n", " dy = transforms_smooth[i,1]\n", " da = transforms_smooth[i,2]\n", "\n", " mat = np.zeros((2,3), np.float32)\n", " mat[0,0] = np.cos(da)\n", " mat[0,1] = -np.sin(da)\n", " mat[1,0] = np.sin(da)\n", " mat[1,1] = np.cos(da)\n", " mat[0,2] = dx\n", " mat[1,2] = dy\n", "\n", " frame_stabilized = cv.warpAffine(frame, mat, (width, height))\n", " \n", " mat = cv.getRotationMatrix2D((width/2, height/2), 0, 1.1)\n", " frame_stabilized = cv.warpAffine(frame_stabilized, mat, (width, height))\n", "\n", " frame_out = cv.hconcat([frame, frame_stabilized]) # frame by frame\n", " \n", " out.write(frame_out)\n", " \n", "out.release()" ] }, { "cell_type": "markdown", "id": "c204ec3a", "metadata": {}, "source": [ "Na potrzeby wyświetlenie wynikowego filmu w przeglądarce, użyjemy kodeka H264:" ] }, { "cell_type": "code", "execution_count": null, "id": "4b58bce1", "metadata": {}, "outputs": [], "source": [ "!ffmpeg -y -hide_banner -loglevel warning -nostats -i vid/gen-protest.avi -vcodec libx264 vid/gen-protest.mp4" ] }, { "cell_type": "markdown", "id": "fce1d5cd", "metadata": {}, "source": [ "Wynikowy film:" ] }, { "cell_type": "code", "execution_count": null, "id": "041e29a5", "metadata": {}, "outputs": [], "source": [ "IPython.display.Video(\"vid/gen-protest.mp4\", width=800)" ] }, { "cell_type": "markdown", "id": "5fd935d2", "metadata": {}, "source": [ "# Śledzenie obiektów" ] }, { "cell_type": "markdown", "id": "dbeb1ae1", "metadata": {}, "source": [ "Załóżmy, że chcemy na poniższym filmie śledzić przemieszczanie się piłkarek:" ] }, { "cell_type": "code", "execution_count": null, "id": "7a31e78d", "metadata": {}, "outputs": [], "source": [ "IPython.display.Video(\"vid/football.mp4\", width=800)" ] }, { "cell_type": "markdown", "id": "2fc45895", "metadata": {}, "source": [ "Biblioteka OpenCV posiada [kilka algorytmów](https://docs.opencv.org/4.5.3/dc/d6b/group__tracking__legacy.html) pozwalających na śledzenie obiektów. Poniżej użyjemy algorytmu [*Multiple Instance Learning*](https://docs.opencv.org/4.5.3/d9/dbc/classcv_1_1legacy_1_1TrackerMIL.html):" ] }, { "cell_type": "code", "execution_count": null, "id": "a35e3ad7", "metadata": {}, "outputs": [], "source": [ "video = cv.VideoCapture(\"vid/football.mp4\")\n", "_, frame = video.read()\n", "\n", "bbox = (45, 350, 120, 270)\n", "\n", "tracker = cv.legacy.TrackerMIL_create()\n", "tracker.init(frame, bbox)\n", "\n", "pt_1 = (int(bbox[0]), int(bbox[1]))\n", "pt_2 = (int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3]))\n", "cv.rectangle(frame, pt_1, pt_2, (0, 0, 255), 4, cv.LINE_8)\n", "\n", "plt.figure(figsize=(7,7))\n", "plt.imshow(frame[:,:,::-1]);" ] }, { "cell_type": "markdown", "id": "934dde10", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": null, "id": "b7650ee1", "metadata": {}, "outputs": [], "source": [ "count = 50\n", "\n", "while count > 0:\n", "\n", " ok, frame = video.read()\n", " if not ok:\n", " break\n", "\n", " timer = cv.getTickCount()\n", " \n", " ok, bbox = tracker.update(frame)\n", " \n", " fps = cv.getTickFrequency() / (cv.getTickCount() - timer);\n", "\n", " if ok:\n", " pt_1 = (int(bbox[0]), int(bbox[1]))\n", " pt_2 = (int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3]))\n", " cv.rectangle(frame, pt_1, pt_2, (0,0,255), 4, cv.LINE_8)\n", " else :\n", " cv.putText(frame, \"Tracking failure\", (20, 180), \n", " cv.FONT_HERSHEY_SIMPLEX, 2, (0,0,255), cv.LINE_AA)\n", "\n", " cv.putText(frame, \"FPS : \" + str(int(fps)), (20,50), \n", " cv.FONT_HERSHEY_SIMPLEX, 2, (0,0,255), cv.LINE_AA)\n", "\n", " if count % 10 == 0:\n", " plt.figure(figsize=(7,7))\n", " plt.imshow(frame[:,:,::-1])\n", " count -= 1\n", "\n", "video.release()" ] }, { "cell_type": "markdown", "id": "bf17ff66", "metadata": {}, "source": [ "Istnieje też możliwość jednoczesnego śledzenia kilku obiektów:" ] }, { "cell_type": "code", "execution_count": null, "id": "d2e56fa9", "metadata": {}, "outputs": [], "source": [ "video = cv.VideoCapture(\"vid/football.mp4\")\n", "_, frame = video.read()\n", "\n", "bboxes = [(45, 350, 120, 270), (755, 350, 120, 270)]\n", "colors = [(0, 0, 255), (0, 255, 0)]\n", "\n", "multi_tracker = cv.legacy.MultiTracker_create()\n", "\n", "for bbox in bboxes:\n", " multi_tracker.add(cv.legacy.TrackerMIL_create(), frame, bbox)" ] }, { "cell_type": "code", "execution_count": null, "id": "9f989b8a", "metadata": {}, "outputs": [], "source": [ "count = 50\n", "\n", "while count > 0:\n", "\n", " ok, frame = video.read()\n", " if not ok:\n", " break\n", "\n", " timer = cv.getTickCount()\n", " \n", " _, boxes = multi_tracker.update(frame)\n", " \n", " for i, bbox in enumerate(boxes):\n", " pt_1 = (int(bbox[0]), int(bbox[1]))\n", " pt_2 = (int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3]))\n", " cv.rectangle(frame, pt_1, pt_2, colors[i], 4, cv.LINE_8)\n", "\n", " if count % 10 == 0:\n", " plt.figure(figsize=(7,7))\n", " plt.imshow(frame[:,:,::-1])\n", " count -= 1\n", "\n", "video.release()" ] }, { "cell_type": "markdown", "id": "b1d84a3a", "metadata": {}, "source": [ "# Zadanie 1\n", "\n", "Dla filmu `vid/football.mp4` porównaj jakość śledzenia dla dostępnych algorytmów. Wyniki zapisz na jednym filmie.\n", "\n", "![Porówanie algorytmów śledzenia obiektów](img/football-multi.png)" ] } ], "metadata": { "author": "Andrzej Wójtowicz", "email": "andre@amu.edu.pl", "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "lang": "pl", "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" }, "subtitle": "07. Analiza wideo [laboratoria]", "title": "Widzenie komputerowe", "year": "2021" }, "nbformat": 4, "nbformat_minor": 5 }