کالیبراسیون دوربین و بازسازی سه‌بعدی

کالیبراسیون دوربین

۹ دقیقه مطالعهOpenCV 5.xبه‌روزرسانی:

هدف#

در این بخش، موارد زیر را یاد خواهیم گرفت:

  • انواع اعوجاج‌های ایجادشده توسط دوربین‌ها
  • نحوه یافتن مشخصات ذاتی (انترینسیک) و بیرونی (اکسترینسیک) یک دوربین
  • نحوه اصلاح اعوجاج (Undistort) تصاویر بر اساس این ویژگی‌ها

مفاهیم پایه#

برخی از دوربین‌های سوراخ سوزنی (Pinhole) اعوجاج قابل توجهی در تصاویر ایجاد می‌کنند. دو نوع عمده اعوجاج، اعوجاج شعاعی (Radial Distortion) و اعوجاج مماسی (Tangential Distortion) هستند.

اعوجاج شعاعی باعث می‌شود خطوط مستقیم، خمیده به نظر برسند. اعوجاج شعاعی هر چه نقاط از مرکز تصویر دورتر باشند، بیشتر می‌شود. به عنوان مثال، تصویری در زیر نشان داده شده است که در آن دو لبه یک صفحه شطرنج با خطوط قرمز مشخص شده‌اند. اما مشاهده می‌کنید که مرز صفحه شطرنج یک خط مستقیم نیست و با خط قرمز مطابقت ندارد. تمام خطوط مستقیم مورد انتظار به سمت بیرون برجسته شده‌اند. برای جزئیات بیشتر به Distortion (optics) مراجعه فرمایید.

کالیبراسیون دوربین — تصویر ۱

در بخش‌های زیرین چندین پارامتر جدید معرفی شده است. برای اطلاعات بیشتر به بخش Camera Calibration and 3D Reconstruction مراجعه کنید.

اعوجاج شعاعی را می‌توان به صورت زیر نمایش داد:

به طور مشابه، اعوجاج مماسی به این دلیل رخ می‌دهد که لنز تصویربرداری کاملاً موازی با صفحه سنسور تصویر قرار نگرفته است. بنابراین، برخی نواحی در تصویر ممکن است نزدیک‌تر از حد انتظار به نظر برسند. میزان اعوجاج مماسی را می‌توان به شکل زیر نشان داد:

به طور خلاصه، ما باید پنج پارامتر را پیدا کنیم که به عنوان ضرایب اعوجاج (Distortion Coefficients) شناخته می‌شوند:

علاوه بر این، ما به اطلاعات دیگری مانند پارامترهای ذاتی (Intrinsic) و بیرونی (Extrinsic) دوربین نیاز داریم. پارامترهای ذاتی مختص یک دوربین هستند. آن‌ها شامل اطلاعاتی مانند فاصله کانونی () و مراکز نوری () می‌باشند. فاصله کانونی و مراکز نوری را می‌توان برای ایجاد یک ماتریس دوربین استفاده کرد که می‌تواند برای رفع اعوجاج ناشی از لنزهای یک دوربین خاص استفاده شود. ماتریس دوربین برای هر دوربین منحصر به فرد است، بنابراین پس از محاسبه، می‌توان آن را در تصاویر دیگر گرفته‌شده توسط همان دوربین مجدداً استفاده کرد. این ماتریس به صورت یک ماتریس 3x3 بیان می‌شود:

پارامترهای بیرونی متناظر با بردارهای چرخش و انتقال هستند که مختصات یک نقطه سه‌بعدی را به یک دستگاه مختصات منتقل می‌کنند.

برای کاربردهای استریو، ابتدا باید این اعوجاج‌ها اصلاح شوند. برای یافتن این پارامترها، باید چند تصویر نمونه از یک الگوی به خوبی تعریف‌شده (مانند صفحه شطرنج) ارائه دهیم. ما نقاط خاصی را پیدا می‌کنیم که از قبل موقعیت‌های نسبی آن‌ها را می‌دانیم (مانند گوشه‌های مربع‌ها در صفحه شطرنج). ما مختصات این نقاط را در فضای دنیای واقعی و همچنین مختصات آن‌ها را در تصویر می‌دانیم، بنابراین می‌توانیم ضرایب اعوجاج را محاسبه کنیم. برای نتایج بهتر، حداقل به ۱۰ الگوی آزمایشی نیاز داریم.

کد#

همانطور که در بالا ذکر شد، حداقل به ۱۰ الگوی آزمایشی برای کالیبراسیون دوربین نیاز داریم. OpenCV همراه با چند تصویر از یک صفحه شطرنج ارائه می‌شود (فایل‌های samples/data/left01.jpg تا left14.jpg را ببینید)، بنابراین از این تصاویر استفاده خواهیم کرد. تصویری از صفحه شطرنج را در نظر بگیرید. داده ورودی مهم مورد نیاز برای کالیبراسیون دوربین، مجموعه‌ای از نقاط سه‌بعدی دنیای واقعی و مختصات دوبعدی متناظر این نقاط در تصویر است. نقاط تصویر دوبعدی را می‌توانیم به راحتی از تصویر پیدا کنیم (این نقاط تصویر مکان‌هایی هستند که دو مربع سیاه در صفحات شطرنج با هم تماس دارند).

نقاط سه‌بعدی از فضای دنیای واقعی چطور؟ آن تصاویر از یک دوربین ثابت گرفته شده‌اند و صفحات شطرنج در مکان‌ها و جهت‌های مختلف قرار گرفته‌اند. بنابراین ما باید مقادیر را بدانیم. اما برای سادگی، می‌توانیم بگوییم صفحه شطرنج در صفحه XY ثابت نگه داشته شده بود (بنابراین همواره Z=0 است) و دوربین متناسب با آن جابجا شده است. این فرضیه به ما کمک می‌کند تا فقط مقادیر X,Y را پیدا کنیم. اکنون برای مقادیر X,Y، می‌توانیم به سادگی نقاط را به عنوان (0,0)، (1,0)، (2,0)، ... که مکان نقاط را مشخص می‌کند ارسال کنیم. در این حالت، نتایجی که به دست می‌آوریم در مقیاس اندازه مربع صفحه شطرنج خواهد بود. اما اگر اندازه مربع را بدانیم (مثلاً ۳۰ میلی‌متر)، می‌توانیم مقادیر را به صورت (0,0)، (30,0)، (60,0)، ... ارسال کنیم. بدین ترتیب نتایج را بر حسب میلی‌متر به دست می‌آوریم. (در این حالت، چون ما آن تصاویر را نگرفته‌ایم اندازه مربع را نمی‌دانیم، بنابراین بر حسب اندازه مربع مقادیر را ارسال می‌کنیم).

نقاط سه‌بعدی نقاط شیء (Object Points) و نقاط دوبعدی تصویر نقاط تصویر (Image Points) نامیده می‌شوند.

راه‌اندازی اولیه#

برای یافتن الگو در صفحه شطرنج، می‌توانیم از تابع ()cv.findChessboardCorners استفاده کنیم. همچنین باید مشخص کنیم به دنبال چه نوع الگویی هستیم، مانند شبکه 8x8، شبکه 5x5 و غیره. در این مثال، از یک شبکه 6x7 استفاده می‌کنیم. (معمولاً یک صفحه شطرنج دارای ۸x۸ مربع و ۷x۷ گوشه داخلی است). این تابع نقاط گوشه و مقدار retval را برمی‌گرداند که در صورت به دست آمدن الگو مقدار آن True خواهد بود. این گوشه‌ها با ترتیب مشخص (از چپ به راست، از بالا به پایین) چیده می‌شوند.

پس از یافتن گوشه‌ها، می‌توانیم با استفاده از ()cv.cornerSubPix دقت آن‌ها را افزایش دهیم. همچنین می‌توانیم با استفاده از ()cv.drawChessboardCorners الگو را رسم کنیم. تمام این مراحل در کد زیر گنجانده شده‌اند:

import numpy as np
import cv2 as cv
import glob

# termination criteria
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(5,6,0)
cols = 6
rows = 7
objp = np.zeros((cols*rows,3), np.float32)
objp[:,:2] = np.mgrid[0:cols,0:rows].T.reshape(-1,2)

# Arrays to store object points and image points from all the images.
objpoints = [] # 3d point in real world space
imgpoints = [] # 2d points in image plane.

images = glob.glob('*.jpg')

for fname in images:
    img = cv.imread(fname)
    gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

    # Find the chess board corners
    ret, corners = cv.findChessboardCorners(gray, (cols, rows), None)

    # If found, add object points, image points (after refining them)
    if ret == True:
        objpoints.append(objp)

        corners2 = cv.cornerSubPix(gray,corners, (11,11), (-1,-1), criteria)
        imgpoints.append(corners2)

        # Draw and display the corners
        cv.drawChessboardCorners(img, (cols, rows), corners2, ret)
        cv.imshow('img', img)
        cv.waitKey(500)

cv.destroyAllWindows()

یک تصویر که الگو روی آن رسم شده در زیر نشان داده شده است:

کالیبراسیون دوربین — تصویر ۲

کالیبراسیون#

اکنون که نقاط شیء و نقاط تصویر خود را در اختیار داریم، آماده اجرای کالیبراسیون هستیم. می‌توانیم از تابع ()cv.calibrateCamera استفاده کنیم که ماتریس دوربین، ضرایب اعوجاج، بردارهای چرخش و انتقال و غیره را بازمی‌گرداند.

ret, mtx, dist, rvecs, tvecs = cv.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)

اصلاح اعوجاج (Undistortion)#

اکنون می‌توانیم تصویری را گرفته و اعوجاج آن را برطرف کنیم. OpenCV دو روش برای انجام این کار ارائه می‌دهد. با این حال در ابتدا، می‌توانیم ماتریس دوربین را بر اساس یک پارامتر مقیاس‌بندی آزاد با استفاده از ()cv.getOptimalNewCameraMatrix بهبود دهیم. اگر پارامتر مقیاس‌بندی alpha=0 باشد، تصویر بدون اعوجاج با حداقل پیکسل‌های ناخواسته را برمی‌گرداند؛ بنابراین ممکن است حتی برخی از پیکسل‌های گوشه‌های تصویر را حذف کند. اگر alpha=1 باشد، تمام پیکسل‌ها با برخی تصاویر سیاه اضافی حفظ می‌شوند. این تابع همچنین یک ناحیه مورد نظر (ROI) از تصویر را برمی‌گرداند که می‌توان از آن برای برش دادن نتیجه استفاده کرد.

بنابراین، تصویر جدیدی می‌گیریم (left12.jpg در این مورد که اولین تصویر در این فصل است):

img = cv.imread('left12.jpg')
h,  w = img.shape[:2]
newcameramtx, roi = cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))

۱. استفاده از ()cv.undistort#

این آسان‌ترین روش است. کافی است تابع را فراخوانی کرده و از ROI به دست آمده در بالا برای برش تصویر حاصل استفاده کنید.

# undistort
dst = cv.undistort(img, mtx, dist, None, newcameramtx)

# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv.imwrite('calibresult.png', dst)

۲. استفاده از بازتطبیق (remapping)#

این روش کمی دشوارتر است. ابتدا یک تابع نگاشت از تصویر اعوجاج‌یافته به تصویر بدون اعوجاج پیدا کنید. سپس از تابع remap استفاده نمایید.

# undistort
mapx, mapy = cv.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
dst = cv.remap(img, mapx, mapy, cv.INTER_LINEAR)

# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv.imwrite('calibresult.png', dst)

با این حال، هر دو روش یک نتیجه را ارائه می‌دهند. نتیجه را در زیر مشاهده کنید:

کالیبراسیون دوربین — تصویر ۳

می‌توانید در تصویر خروجی مشاهده کنید که تمام لبه‌ها مستقیم و صاف شده‌اند.

اکنون می‌توانید ماتریس دوربین و ضرایب اعوجاج را با استفاده از توابع ذخیره‌سازی در NumPy (مانند np.savez، np.savetxt و غیره) برای استفاده‌های بعدی ذخیره کنید.

خطای بازتاب تصویر (Re-projection Error)#

خطای بازتاب تصویر تخمین مناسبی از میزان دقت پارامترهای یافت‌شده به دست می‌دهد. هرچه خطای بازتاب به صفر نزدیک‌تر باشد، پارامترهای به دست آمده دقیق‌تر هستند. با داشتن ماتریس‌های ذاتی، اعوجاج، دوران و انتقال، ابتدا باید نقطه شیء را با استفاده از ()cv.projectPoints به نقطه تصویر تبدیل کنیم. سپس، می‌توانیم نرم (فاصله) بین آنچه با تبدیل خود به دست آورده‌ایم و الگوریتم یافتن گوشه محاسبه کنیم. برای یافتن RMSE (ریشه میانگین مربعات خطا)، خطاهای مجذور را روی تمام نقاط و تصاویر میانگین‌گیری کرده و سپس جذر آن را می‌گیریم:

mean_error = 0
for i in range(len(objpoints)):
    imgpoints2, _ = cv.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    error = cv.norm(imgpoints[i], imgpoints2, cv.NORM_L2SQR) / len(imgpoints2)
    mean_error += error

print( "total error: {}".format(np.sqrt(mean_error/len(objpoints))) )

تمرین‌ها#

  1. کالیبراسیون دوربین را با شبکه دایره‌ای (Circular grid) امتحان کنید.