شناسایی و ردیابی حرکت با پایتون و OpenCV

کمیل آقابابایی ارشد نرم فزار _ babaiekomeil@gmail.comدر این مقاله نحوه ساخت یک سیستم ردیابی و شناسایی حرکت پایه برای نظارت خانگی با استفاده از تکنیک‌های بینایی کامپیوتری بررسی می‌شود. این مثال با هر دو فیلم از پیش ضبط شده و پخش زنده از وب کم شما کار خواهد کرد. با این حال ، ما این سیستم را در لپ تاپ ها/دسک تاپ های خود توسعه خواهیم داد.کمی در مورد تفریق پس زمینهتفریق پس‌زمینه در بسیاری از برنامه‌های بینایی رایانه‌ای بسیار مهم است . به عنوان مثال می توان از آن برای شمارش تعداد اتومبیل‌هایی که از باجه عوارضی عبور می‌کنند استفاده کرد و یا برای شمارش تعداد افرادی که در داخل و خارج از یک فروشگاه قدم می‌زنند استفاده کنیم .و ما از آن برای تشخیص حرکت استفاده می کنیم.قبل از اینکه کد گذاری را در این پست شروع کنیم, اجازه دهید بگویم که راه‌های بسیار زیادی برای تشخیص حرکت, ردیابی و آنالیز در OpenCV وجود دارد. بعضی‌ها خیلی ساده هستند. و برخی دیگر بسیار پیچیده هستند.دو روش اصلی عبارتند از: شکل مدل آمیخته گاوسی(Gaussian Mixture Model - based foreground) و تقسیم‌بندی پس‌زمینه(background segmentation)1- یک مدل مخلوط پس‌زمینه تطبیقی بهبودیافته برای ردیابی بلادرنگ با تشخیص سایه توسط KaewTraKulPong و همکاران، که از طریق تابع cv2.BackgroundSubtractorMOG در دسترس است.2- مدل مخلوط گاوسی تطبیقی بهبود یافته برای تفریق پس‌زمینه و تخمین تراکم تطبیقی کارآمد در هر پیکسل تصویر برای وظیفه تفریق پس‌زمینه، همچنین توسط Zivkovic، از طریق تابع cv2.BackgroundSubtractorMOG2در دسترس است.و در نسخه‌های جدیدتر OpenCV، تقسیم‌بندی پیش‌زمینه و پس‌زمینه مبتنی بر بیزی (احتمال) را داریم که از مقاله Godbehereو همکاران در سال 2012 قابل دریافت می باشد. ما می‌توانیم این پیاده‌سازی را در تابع cv2.createBackgroundSubtractorGMGپیدا کنیمهمه این روش‌ها مربوط به تقسیم پس‌زمینه از پیش‌زمینه هستند (و حتی مکانیسم‌هایی را برای ما فراهم می‌کنند تا بین حرکت واقعی و فقط سایه و یا تغییرات کوچک نور تشخیص دهیم)!پس چرا این مساله اینقدر مهم است ؟ و چرا ما اهمیت می‌دهیم که پیکسل ها به پیش‌زمینه تعلق دارند و چه پیکسل بخشی از پس‌زمینه هستند ؟خوب، در تشخیص حرکت، ما تمایل داریم که فرض زیر را داشته باشیم:پس‌زمینه جریان ویدیوی ما تا حد زیادی ثابت و بدون تغییر در فریم‌های متوالی یک ویدیو می باشد. بنابراین، اگر بتوانیم پس‌زمینه را مدل‌سازی کنیم و آن را برای مقایسه با تغییرات اساسی در نظر ‌بگیریم.در نتیجه اگر تغییر اساسی وجود داشته باشد، می‌توانیم آن را تشخیص دهیم که این تغییر معمولاً همان حرکت در ویدیوی ما می باشد.اکنون بدیهی است که در دنیای واقعی این فرض به راحتی می تواند شکست بخورد. به دلیل سایه، انعکاس، شرایط نوری و هر تغییر احتمالی دیگری در محیط، پس زمینه ما می تواند در فریم های مختلف یک ویدیو کاملاً متفاوت به نظر برسد و اگر پس‌زمینه متفاوت به نظر برسد، می‌تواند الگوریتم‌های ما را از بین ببرد. به همین دلیل است که موفق‌ترین سیستم‌های تشخیص پس زمینه تفریق/پیش زمینه از دوربین های نصب شده ثابت و در شرایط نوری کنترل شده استفاده می کنند.روش هایی که در بالا ذکر کردم، اگرچه بسیار قدرتمند هستند، اما از نظر محاسباتی نیز گران هستند و از آنجایی که هدف نهایی ما این است که این سیستم را در پایان این سری 2 قسمتی روی Raspberry Pi استقرار دهیم، بهتر است به رویکردهای ساده پایبند باشیم.در پست‌های آینده به این روش‌های قدرتمندتر خواهم پرداخت.انشاالله اما فعلاً می‌خواهم آن را ساده و کارآمد نگه دارم.در ادامه این پست ، من می‌خواهم (احتمالا) ابتدایی‌ترین سیستم تشخیص و ردیابی حرکتی را که می‌توانید بسازید، شرح دهم. کامل نخواهد بود، اما می‌تواند روی Piاجرا شود و همچنان نتایج خوبی ارائه دهد.شناسایی و ردیابی حرکت با پیتون و OpenCVبسیار خوب، آیا شما آماده‌اید که به من کمک کنید تا یک سیستم نظارت خانگی ایجاد کنم ؟یک ویرایشگر باز کنید، یک فایل جدید ایجاد کنید، نام آن را motion_detector.py بگذارید، و اجازه دهید کدنویسی کنیم:Basic motion detection and tracking with Python and OpenCV1. # import the necessary packages2. from imutils.video import VideoStream3. import argparse4. import datetime5. import imutils6. import time7. import cv28.9. # construct the argument parser and parse the arguments10. ap = argparse.ArgumentParser()11. ap.add_argument("-v", "--video", help="path to the video file")12. ap.add_argument("-a", "--min-area", type=int, default=500, help="minimum area size")13. args = vars(ap.parse_args())14.15. # if the video argument is None, then we are reading from webcam16. if args.get("video", None) is None:17. vs = VideoStream(src=0).start()18. time.sleep(2.0)19.20. # otherwise, we are reading from a video file21. else:22. vs = cv2.VideoCapture(args["video"])23.24. # initialize the first frame in the video stream25. firstFrame = Noneخطوط 2-7 بسته های لازم ما را وارد می کنند. همه اینها باید بسیار آشنا به نظر برسند، به جز شاید بسته imutils، که مجموعه ای از توابع راحتی است که من برای آسان کردن کارهای اساسی پردازش تصویر ایجاد کرده ام. اگر قبلا imutil را روی سیستم خود نصب نکرده اید، می توانید آن را از طریق pip نصب کنید:pip install imutilsدر مرحله بعد، آرگومان های خط فرمان خود را در خطوط 13-10 تجزیه می کنیم. ما در اینجا دو سوئیچ تعریف می‌کنیم. اولی، --video، اختیاری است. این به سادگی مسیری را برای یک فایل ویدیویی از پیش ضبط شده تعریف می کند که ما می توانیم حرکت را در آن تشخیص دهیم. اگر مسیری برای یک فایل ویدیویی ارائه نکنید، OpenCVاز وب کم شما برای تشخیص حرکت استفاده می کند.ما همچنین --min-areaکه بیانگر حداقل اندازه (بر حسب پیکسل) برای ناحیه ای از تصویر که به عنوان "حرکت" واقعی در نظر گرفته می شود را تعریف می کنیم. همانطور که بعداً در این آموزش بحث خواهیم کرد، ما اغلب مناطق کوچکی از یک تصویر را می یابیم که احتمالاً به دلیل نویز یا تغییرات در شرایط نوری تغییر اساسی کرده اند. در واقع, این مناطق کوچک اصلاً حرکت واقعی نیستند - بنابراین ما حداقل اندازه یک منطقه را برای مبارزه و فیلتر کردن این تشخیص‌های مثبت کاذب مشخص خواهیم کرد.در خطوط 22-16 vs یک ارجاع به شی مقابل ما را نشان می دهد. در صورتی که در مسیر فایل ویدیویی ارائه نشده باشد (خطوط 18-16)، ما یک ارجاع به وب کم فعال می کنیم و منتظر می مانیم تا تصویر دریافت شود و اگر یک فایل ویدئویی ارائه شده باشد، در خطوط 21 و 22 یک اشاره گر برای آن ایجاد می کنیم.در نهایت، این قطعه کد را با تعریف متغیری به نام firstFrame به پایان می‌رسانیم.آیا حدس زده‌اید که firstFrameچیست؟اگر حدس می‌زنید که firstFrameاولین فریم از جریان ویدئو / webcamرا ذخیره می‌کند، درست می‌گویید.فرض: اولین فریم فایل ویدیوی ما شامل هیچ حرکتی نیست و فقط پس‌زمینه خواهد بود – بنابراین، می‌توانیم پس‌زمینه جریان ویدیوی خود را تنها با استفاده از اولین فریم ویدیو مدل‌سازی کنیم.بدیهی است که ما در اینجا یک فرض بسیار بزرگ را انجام می دهیم. اما باز هم، هدف ما اجرای این سیستم بر روی Raspberry Pi است، بنابراین نمی توانیم خیلی پیچیده باشیم. و همانطور که در بخش نتایج این پست خواهید دید، ما می‌توانیم به راحتی حرکت را در حین ردیابی فردی که در اتاق راه می‌رود، تشخیص دهیم.# حلقه بر روی فریم ویدئو27. # loop over the frames of the video28. while True:29. # grab the current frame and initialize the occupied/unoccupied30. # text31. frame = vs.read()32. frame = frame if args.get("video", None) is None else frame[1]33. text = "Unoccupied"34.35. # if the frame could not be grabbed, then we have reached the end36. # of the video37. if frame is None:38. break39.40. # resize the frame, convert it to grayscale, and blur it41. frame = imutils.resize(frame, width=500)42. gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)43. gray = cv2.GaussianBlur(gray, (21, 21), 0)44.45. # if the first frame is None, initialize it46. if firstFrame is None:47. firstFrame = gray48. continueپس حالا که ما یک مرجع برای جریان video / webcam داریم , می‌توانیم حلقه را بر روی هر یک از فریم‌ها در خط ۲۸ شروع کنیم .با فراخوانی vs.read () در خط ۳۱ ، یک فریمی را که به درستی گرفته شده را برای ما باز می‌گرداند.ما همچنین رشته‌ای به نام text تعریف می‌کنیم و آن را مقداردهی اولیه می‌کنیم تا نشان دهد اتاقی که ما نظارت می‌کنیم «غیر اشغال» «Unoccupied» است. اگر واقعاً در اتاق فعالیت وجود دارد، می توانیم این رشته را به روز کنیم.و در صورتی که یک فریم از فایل ویدیویی با موفقیت خوانده نشود، در خطوط 37 و 38 از حلقه خارج می‌شویم.اکنون می توانیم فریم خود را پردازش کرده و آن را برای تحلیل و تشخیص حرکت آماده کنیم (خطوط 43-41). ابتدا اندازه آن را به عرض 500 پیکسل تغییر می دهیم - نیازی به پردازش تصاویر بزرگ و خام مستقیماً از جریان ویدیو نیست. ما همچنین تصویر را به مقیاس خاکستری تبدیل می کنیم زیرا رنگ هیچ تاثیری بر الگوریتم تشخیص حرکت ما ندارد. در نهایت، ما Gaussian blurring را برای صاف کردن تصاویر خود اعمال می کنیم.درک این نکته مهم است که حتی فریم های متوالی یک جریان ویدیویی یکسان نخواهند بود!با توجه به تغییرات ریز در سنسورهای دوربین دیجیتال, هیچ دو فریم یک‌سان نخواهند بود - مطمئناً برخی از پیکسل ها مقادیر شدت متفاوتی دارند. با این حال، ما باید این را در نظر بگیریم و هموارسازی گاوسی را برای میانگین شدت پیکسل در یک منطقه 21×21 اعمال کنیم (خط 43). این کمک می کند تا نویز با فرکانس بالا را که می تواند الگوریتم تشخیص حرکت ما را از بین ببرد، صاف شود.همانطور که در بالا اشاره شد, باید پس‌زمینه تصویر خود را به گونه‌ای مدل‌سازی کنیم. دوباره, ما این فرض را ایجاد می‌کنیم که فریم اول جریان ویدئوییحاوی هیچ حرکت و نمونه خوبی از چیزی است که پس‌زمینه ما شبیه آن است.اگر firstFrame مقداردهی اولیه نشده باشد، آن را برای مرجع ذخیره می کنیم و به پردازش فریم بعدی جریان ویدیو ادامه می دهیم (خطوط 48-46).در اینجا مثالی از اولین فریم یک ویدئوی نمونه آورده شده است:شکل 1: نمونه فریم اول یک فایل ویدئویی.توجه کنید که چگونه یک عکس ثابت از پس زمینه است و هیچ حرکتی انجام نمی شود.فریم فوق این فرض را برآورده می کند که اولین فریم ویدیو صرفاً پس زمینه ثابت است - هیچ حرکتی انجام نمی شود.با توجه به این تصویر پس زمینه ایستا، ما اکنون آماده هستیم تا در واقع تشخیص حرکت و ردیابی را انجام دهیم:50. # compute the absolute difference between the current frame and51. # first frame52. frameDelta = cv2.absdiff(firstFrame, gray)53. thresh = cv2.threshold(frameDelta, 25, 255, cv2.THRESH_BINARY)[1]54. # dilate the thresholded image to fill in holes, then find contours55. # on thresholded image56. thresh = cv2.dilate(thresh, None, iterations=2)57. cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,58. cv2.CHAIN_APPROX_SIMPLE)59. cnts = imutils.grab_contours(cnts)60.61. # loop over the contours62. for c in cnts:63. # if the contour is too small, ignore it64. if cv2.contourArea(c) < args["min_area"]:65. continue66.67. # compute the bounding box for the contour, draw it on the frame,68. # and update the text69. (x, y, w, h) = cv2.boundingRect(c)70. cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)71. text = "Occupied"اکنون که پس‌زمینه خود را از طریق متغیر firstFrame مدل‌سازی کرده‌ایم، می‌توانیم از آن برای محاسبه تفاوت بین فریم اولیه و فریم‌های جدید بعدی از جریان ویدیو استفاده کنیم.محاسبه تفاوت بین دو فریم یک تفریق ساده است، که در آن قدر مطلق تفاوت‌های شدت پیکسل متناظر آنها را می‌گیریم (خط 52):delta = |background_model – current_frame|نمونه ای از یک فریم دلتا را می توان در زیر مشاهده کرد:شکل 2: نمونه ای از دلتای فریم، تفاوت بین فریم اولیه و فریم فعلی.توجه کنید که پس‌زمینه تصویر چطور به وضوح سیاه است. با این حال, مناطقی که دارای حرکت هستند (مانند منطقه خودم که در اتاق راه می‌روم) خیلی روشن تر است. این بدان معناست که فریم های دارای دلتای بزرگتر نشان دهنده انجام حرکت در تصویر می باشند.ما سپس چارچوب (دلتا ) را در خط ۵۳ برای نشان دادن مناطقی از تصویر که تنها تغییرات قابل‌توجهی در مقادیر شدت پیکسل دارند , بدست خواهیم آورد . اگر دلتا کم‌تر از ۲۵ باشد , پیکسل را دور انداخته و آن را به رنگ سیاه ( به عنوان مثال پس‌زمینه ) تنظیم می‌کنیم . اگر دلتا بزرگ‌تر از ۲۵ است , آن را به سفید ( به عنوان مثال پیش‌زمینه ) تنظیم می‌کنیم . نمونه ای از تصویر دلتا آستانه ما را می توان در زیر مشاهده کرد:شکل 3: آستانه تصویر Delta Frame برای تقسیم پیش زمینه از پس زمینه.دوباره, توجه داشته باشید که پس‌زمینه تصویر سیاه است, در حالی که پیش‌زمینه (و جایی که حرکت در حال انجام است) سفید است.با توجه به این تصویر آستانه ای، اعمال تشخیص کانتور برای یافتن خطوط کلی این مناطق سفید ساده است (خطوط 57-59).ما شروع به حلقه زدن روی هر یک از خطوط در خط 62 می کنیم، جایی که خطوط کوچک و نامربوط را در خط 64 و 65 فیلتر می کنیم.اگر ناحیه کانتور بزرگتر از مساحت --min-area ارائه شده ما باشد، کادر محدود کننده اطراف پیش زمینه و ناحیه حرکت را در خطوط 69 و 70 ترسیم می کنیم. ما همچنین رشته وضعیت متنی خود را به روز رسانی می‌کنیم تا نشان دهیم که اتاق " اشغال‌شده " ( Occupied "") است.# draw the text and timestamp on the frame73. cv2.putText(frame, "Room Status: {}".format(text), (10, 20),cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)74. cv2.putText(frame,datetime.datetime.now().strftime("%A%d%B%Y%I:%M:%S%p"),(10, frame.shape[0] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 0, 255), 1)# show the frame and record if the user presses a key75. cv2.imshow("Security Feed", frame)76. cv2.imshow("Thresh", thresh)77. cv2.imshow("Frame Delta", frameDelta)78. key = cv2.waitKey(1) & 0xFF# if the `q` key is pressed, break from the lop79. if key == ord("q"):break# cleanup the camera and close any open windows80. vs.stop() if args.get("video", None) is None else vs.release()81. cv2.destroyAllWindows()بقیه این مثال به سادگی همه چیز را جمع بندی می کند. وضعیت اتاق را روی تصویر در گوشه سمت چپ بالا ترسیم می‌کنیم و به دنبال آن یک مهر زمانی (برای اینکه شبیه فیلم امنیتی "واقعی" باشد) در پایین سمت چپ می‌کشیم.خطوط 75-77 نتایج کار ما را نشان می‌دهد و به ما امکان می‌دهد اگر حرکتی در ویدیوی ما تشخیص داده شد، به همراه دلتای فریم و تصویر آستانه‌ای تجسم کنیم تا بتوانیم اسکریپت خود را اشکال‌زدایی کنیم.توجه: اگر کد را در این پست دانلود می‌کنید و می‌خواهید آن را روی فایل‌های ویدیویی خود اعمال کنید، احتمالاً باید مقادیر cv2.threshold و آرگومان min-area-- را تنظیم کنید تا بهترین نتایج را برای نورپردازی خود به دست آورید.در نهایت، خطوط 80 و81 نشانگر جریان ویدئو را پاکسازی کرده و آزاد می کنند.منبع:https://pyimagesearch.com/2015/05/25/basic-motion-detection-and-tracking-with-python-and-opencv/