نمایه شدت (Intensity Profile) تولید شده برای تصویر اول با استفاده از مقادیر شدت رنگ پیکسلهای تصویر
نمایه شدت (Intensity Profile) تولید شده برای تصویر دوم با استفاده از مقادیر شدت رنگ پیکسلهای تصویر
روشهای مطابقت مبتنی بر شدت، از Scanline (یک Scanline، یک خط یا یک سطر در «الگوی اسکن کردن راستر» (Raster Scanning Pattern) است، نظیر یک خط ویدئویی در نمایشگرهای CRT یا مانیتورهای کامپیوتر) جهت تولید «نمایههای شدت» (Intensity Profile) استفاده میکنند. رویکرد Scanline روی «تصاویر اصلاح شده» (Rectified Image) اجرا میشود و خطوط افقی موجود در تصویر را یکی به یکی پردازش میکند.
مطابقت تصویر (Image correspondence)، از طریق حرکت دادن نمایه شدت متناظر با دوربین آفست (Offset Camera) در راستای افقی و کمینهسازی اختلاف (یا بیشینهسازی مشابهت) در مقادیر شدت (Intensity)، پیکسلها حاصل میشود. در صورتی قرار است مطابقت میان تصاویر رنگی حاصل شود، پیشنهاد میشود که به ازاء هر کدام از مؤلفهها یا کانالهای رنگی، یک نمایه شدت تولید و سپس، میانگین مقدار شدت حاصل محاسبه شود.
از رویکرد Scanline میتوان برای ساختن «پنجره لغزان» (Sliding Window) استفاده کرد که محاسبات لازم برای مطابقت تصویر (Image correspondence) را به صورت محلی انجام میدهد. بدین صورت که ابتدا از پنجره لغزان ساخته شده، جهت تولید نمایه شدت متناظر با یک پنجره به اندازه NxN، اطراف یک پیکسل مورد نظر، استفاده میشود. سپس، یک پنجره با اندازه مشابه، در راستای تصویر دیگر حرکت داده میشود که هدف آن کمینه کردن اختلافات یا بیشینه کردن مشابهت (میان مقادیر شدت) در نمایههای شدت متناظر است.
معیارهای مرسومی که برای مشخص کردن مشابهت مورد استفاده قرار میگیرند، عبارتند از:روش مجموع مربعات فاصلهها (Sum of Squared Distances | SSD)روش مجموع فاصلههای مطلق (Sub of Absolute Distance | SAD)روش «همبستگی متقابل نرمال شده» (Normalized Cross Correlation | NCC)
با فرض اینکه X
و Y نمایش دهنده مقدار شدت در دو پنجره باشند، تعداد N چندتایی به فرم (X1,Y1),……,(XN,YN)
، برای یک پنجره به اندازه NxN وجود خواهد داشت. در چنین حالتی، مقدار «همبستگی متقابل نرمال شده» (Normalized Cross Correlation | NCC) از طریق رابطه زیر به دست میآید:
Normalizedcrosscorrelation=∑ni=1(Xi–¯¯¯¯¯X)(Yi–¯¯¯¯Y)√[∑ni=1(Xi–¯¯¯¯¯X)2∑ni=1(Yi–¯¯¯¯Y)2]
در این رابطه، ¯¯¯¯¯X
و ¯¯¯¯Y
، میانگین نمونههای موجود در پنجرههای متناظر را نمایش میدهند. مقدار رابطه همبستگی متقابل نرمال شده، بین مقادیر 1 و 1- خواهد بود؛ مقدار 1 نشان دهنده «همبستگی کامل» (Perfect Correlation) میان مقادیر شدت پنجرهها خواهد بود.
از رابطه زیر، جهت محاسبه مجموع مربعات فاصلهها (Sum of Squared Distances | SSD) استفاده میشود:
SSD=n∑i=1(Xi–Yi)2
این رابطه، مربع «فاصله اقلیدسی» (Euclidean Distance) میان X
و Y
را محاسبه میکند. از رابطه زیر نیز جهت محاسبه مجموع فاصلههای مطلق (Sub of Absolute Distance | SAD) استفاده میشود:
SAD=n∑i=1∣Xi–Yi∣
این رابطه، فاصله مطلق میان مقادیر شدت را محاسبه میکند. روشهای دیگری برای محاسبه شباهت وجود دارند ولی به اندازه روشهای نمایش داده شده معروف و پرطرفدار نیستند. عوامل متعددی نظیر «همپوشانی» (Occlusion) و «نوردهی آینهای» (Specular Lighting)، میتوانند در تولید شرایط مطابقت ضعیف میان تصاویر سهیم باشند. نوردهی آینهای (Specular Lighting) به انعکاساتی اطلاق میشود که از «سطوح غیر لابرتنی» (Non-Lambertian Surfaces) ایجاد شده باشند. سطوح غیر لابرتنی، سطوحی «پخشی» (Diffuse) هستند که نور را به طور برابر در تمامی جهات منعکس میکنند. این عواملی زمانی مشکلساز میشوند که دوربینها، به دلیل اختلاف چشمانداز یا منظر (Perspectives)، تصاویر متفاوتی را از یک صحنه دریافت میکنند.مرحله بهینهسازی (Optimization) در مطابقت دو سویی
هدف روشهای بهینهسازی ناهمخوانی این است که با مشخص کردن بهترین مجموعه از ناهمخوانیهایی (Disparities) که نمایش دهنده «سطح صحنه» (Scene Surface) موجود در تصویر هستند، نقشه ناهمخوانی (Disparity Map) دقیقی را از تصاویر تولید کنند. مهمترین و شایعترین روشهای بهینهسازی ناهمخوانی (Disparity Optimization) عبارتند از:رویکرد «همه برای برنده» (Winner-Take-All)رویکرد «برنامهنویسی پویا» (Dynamic Programming)رویکرد «شبیهسازی تبرید» (Simulated Annealing)رویکرد «برشهای گرافی» (Graph Cuts)رویکرد بهینهسازی Scanline
رویکرد «همه برای برنده» (Winner-Take-All): این رویکرد، سادهترین و معمولترین رویکرد برای روشهای «تجمیع مبتنی بر پنجره» (Window-based Aggregation) محسوب میشود. در این روش، ناهمخوانی کمینه کننده مقدار هزینه، به عنوان ناهمخوانی اصلی انتخاب میشود.
رویکرد «برنامهنویسی پویا» (Dynamic Programming): در این رویکرد سعی میشود تا در ماتریس متشکل از هزینههای تطبیق دو طرفه میان دو Scanline متناظر، مسیر «بهینه سراسری» (Globally Optimized) و دارای کمترین هزینه ممکن انتخاب شود.
رویکرد بهینهسازی Scanline: این رویکرد، مشابه رویکرد برنامهنویسی پویا است؛ با این تفاوت که هزینه همپوشانی (Occlusion Cost) در آن وجود ندارد.
رویکرد «شبیهسازی تبرید» (Simulated Annealing): در این رویکرد، یک ناهمخوانی به طور تصادفی انتخاب و هزینه در آن پیکسل ارزیابی میشود. در مرحله بعد، این رویکرد، جهت پیدا کردن کمینه سراسری (Global Minimum)، به سمت چپ یا راست حرکت میکند و هزینه در این پیکسلها را ارزیابی میکند.
رویکرد «برشهای گرافی» (Graph Cuts): این رویکرد، از روش حرکت «مبادله آلفا-بتا» (α-β Swap) برای بهینهسازی ناهمخوانیها استفاده میکند.
در سیستمهای بینایی ماشین پس از اینکه مطابقت (Correspondence) میان تصاویر مشخص شد، از هندسه Epipolar یا Epipolar Geometry برای محاسبه نقشه ناهمخوانی (Disparity Map) استفاده میشود. هندسه Epipolar، شکلی از «هندسه تصویری» (Projective Geometry) محسوب میشود که برای مشخص کردن موقعیت سهبُعدی یک شیء، از منظر دو دوربین مورد استفاده قرار میگیرد.
هندسه Epipolar به کار گرفته شده جهت محاسبه عمق یک پیکسل تصویر شده. در این تصویر، C1 و C2 دوربین سمت چپ و راست را نمایش میدهند، در حالی که e1 و e2 برای نمایش دادن خط Epipolar مورد استفاده قرار میگیرد. همچنین، P1 و P2 مختصات تصویر شده (Projected Coordinates) در تصویر هستند.
در یک جهان ایدهآل و با در اختیار داشتن دوربینهایی که از «دقت» (Resolution) کامل و دقیقی برخوردار هستند، سیستم بینایی ماشین قادر خواهد بود موقعیت دقیق یک شیء را محاسبه کند. با این حال، از آنجایی که از دوربینهای با دقت محدود در سیستمهای بینایی ماشین استفاده میشود، زاویه «مثلثبندی» (Triangulation) سبب محدود شدن «دقت عمق» (Depth Resolution) در تصاویر گرفته شده خواهد شد. چنین پدیدهای سبب پیشبینی نادرست موقعیت اشیاء موجود در تصویر میشود؛ همچنین، محدود بودن دقت عمق در تصاویر دیجیتالی، سبب ایجاد درجهای از خطای ذاتی (Inherent Error) در آنها میشود.
به همین خاطر است که در سیستمهای بینایی ماشین توصیه میشود از تصاویر گرفته شده توسط چندین دوربین، به جای دو دوربین، برای مطابقت دو سویی (Stereo Correspondence) استفاده شود. زیرا وجود دوربینهای چندگانه در سیستم بینایی ماشین (Machine Vision)، اجازه تخمین دقیق موقعیت اشیاء موجود در تصویر را به سیستم میدهد؛ دلیل این امر، مشارکت چندین دوربین و دادههای حاصل از آنها، در تخمین و مشخص کردن موقعیت نهایی اشیاء در تصاویر است.
پس از اینکه موقعیت (Position) مطابقت در تصاویر مشخص شد، در مرحله بعد لازم است تا عمق (Depth) موقعیت مشخص شده، از صفحه دوربین (Plane of Camera) محاسبه شود تا از این طریق، نقشه ناهمخوانی (Disparity Map) تولید شود.
مرحله اصلاح یا پالایش (Refinement) در مطابقت دو سویی
اصلاح یا پالایش ناهمخوانی (Disparity Refinement) به فرایند هموار کردن (Smoothing) نقشه ناهمخوانی نهایی گفته میشود؛ در نتیجه این فرایند، نقشه ناهمخوانی نهایی از بازههای گسسته شده به مقادیر پیوسته نگاشت یا تبدیل میشوند. برای اصلاح یا پالایش ناهمخوانی، فرایند هموارسازی نقشه ناهمخوانی با استفاده از بهترین مقادیر ناهمخوانی محاسبه شده و یا بر اساس مقادیر ناهمخوانی پیکسلهای همسایه انجام میشود. در این فرایند معمولا فرض میشود که مقدار ناهمخوانی یک پیکسل مورد نظر، با مقدار ناهمخوانی پیکسل همسایه برابر است؛ بنابراین، هموارسازی ناهمخوانی پیکسلها با استفاده از مقادیر ناهمخوانی پیکسلهای همسایه (در سراسر تصویر)، سبب ایجاد مطابقت دو سویی دقیقتر در سیستمهای بینایی ماشین خواهد شد.
پیادهسازیهای مدرن انجام شده از سیستمهای بینایی ماشین و مطابقت دو سویی، به برنامهنویس و توسعهدهنده برنامههای کاربردی اجازه میدهد تا محاسبات هندسه Epipolar را روی «واحد پردازش گرافیکی» (Graphical Processing Unit | GPU) انجام دهند و از «سختافزار بافتزنی تصویری» (Projective Texture Hardware) برای شتاب بخشیدن به فرایند مطابقت دو سویی در سیستمهای بینایی ماشین استفاده کنند.پیادهسازی سیستم محاسبه نقشه ناهمخوانی و مطابقت دو سویی در پایتون
در مثال اول، محاسبه نقشه ناهمخوانی و مطابقت دو سویی با استفاده از تصاویر گرفته شده توسط دو دوربین مختلف نمایش داده میشود. در ابتدا، دو تصویر به عنوان ورودی وارد سیستم محاسبه نقشه ناهمخوانی میشوند. دو دوربین در فاصله مشخصی از یکدیگر قرار گرفته شدهاند. برای محاسبه نقشه ناهمخوانی، از رویکرد برشهای گرافی (Graph Cuts) استفاده شده است.
دو تصویر گرفته شده از یک صحنه توسط دوربینهای مختلف:
کدهای پیادهسازی سیستم محاسبه نقشه ناهمخوانی در پایتون:
def cut(disparity, image, threshold): for i in range(0, image.height): for j in range(0, image.width): # keep closer object if cv.GetReal2D(disparity,i,j) > threshold: cv.Set2D(disparity,i,j,cv.Get2D(image,i,j))
# loading the stereo pair
left = cv.LoadImage('scene_l.bmp',cv.CV_LOAD_IMAGE_GRAYSCALE)
right = cv.LoadImage('scene_r.bmp',cv.CV_LOAD_IMAGE_GRAYSCALE)
disparity_left = cv.CreateMat(left.height, left.width, cv.CV_16S)
disparity_right = cv.CreateMat(left.height, left.width, cv.CV_16S)
# data structure initialization
state = cv.CreateStereoGCState(16,2)
# running the graph-cut algorithm
cv.FindStereoCorrespondenceGC(left,right, disparity_left,disparity_right,state)
disp_left_visual = cv.CreateMat(left.height, left.width, cv.CV_8U)
cv.ConvertScale( disparity_left, disp_left_visual, -16 );
cv.Save( "disparity.pgm", disp_left_visual ); # save the map
# cutting the object farthest of a threshold (120)
cut(disp_left_visual,left,120)
cv.NamedWindow('Disparity map', cv.CV_WINDOW_AUTOSIZE)
cv.ShowImage('Disparity map', disp_left_visual)
cv.WaitKey()
نقشههای ناهمخوانی حاصل با مقادیر آستانه مختلف:
مثال دوم: در مثال زیر نیز دو تصویر از صحنه یکسان نمایش داده شدهاند که توسط دو دوربین با منظرهای (چشماندازها) مختلف گرفته شدهاند.
کدهای پیادهسازی سیستم محاسبه نقشه ناهمخوانی و مطابقت دو سویی در پایتون:
import cv2
import random
import numpy as np
import matplotlib
matplotlib.use("TkAgg")
from matplotlib import pyplot as plt
grayscale_max = 255
def load_image(filename): image = cv2.imread(filename, cv2.IMREAD_GRAYSCALE) return image
def show_image(title, image): max_val = image.max() # image = np.absolute(image) image = np.divide(image, max_val) # cv2.imshow(title, image) cv2.imwrite(title+str(random.randint(1, 100))+'.jpg', image*grayscale_max)
def add_padding(input, padding): rows = input.shape[0] columns = input.shape[1] output = np.zeros((rows + padding * 2, columns + padding * 2), dtype=float) output[ padding : rows + padding, padding : columns + padding] = input return output
def add_replicate_padding(image): # zero_padded = add_padding(image, padding) # size = image.shape[0] top_row = image[0, :] image = np.vstack((top_row, image)) bottom_row = image[-1, :] image = np.vstack((image, bottom_row)) left_column = image[:, 0] left_column = np.reshape(left_column, (left_column.shape[0], 1)) image = np.hstack((left_column, image)) right_column = image[:, -1] right_column = np.reshape(right_column, (right_column.shape[0], 1)) image = np.hstack((image, right_column)) return image
def euclid_dist(a, b): distance = np.linalg.norm(a - b) return distance
def get_search_bounds(column, block_size, width): disparity_range = 25 left_bound = column - disparity_range if left_bound width: right_bound = width - block_size + 1 return left_bound, right_bound
def search_bounds(column, block_size, width, rshift): disparity_range = 75 padding = block_size // 2 right_bound = column if rshift: left_bound = column - disparity_range if left_bound = (width - 2*padding): left_bound = width - 2*padding - 2 step = -1 return left_bound, right_bound, step
# max disparity 30
def disparity_map(left, right, block_size, rshift): padding = block_size // 2 left_img = add_padding(left, padding) right_img = add_padding(right, padding) height, width = left_img.shape # d_map = np.zeros((height - padding*2, width - padding*2), dtype=float) d_map = np.zeros(left.shape , dtype=float) for row in range(height - block_size + 1): for col in range(width - block_size + 1): bestdist = float('inf') shift = 0 left_pixel = left_img[row:row + block_size, col:col + block_size] l_bound, r_bound, step = search_bounds(col, block_size, width, rshift) # for i in range(l_bound, r_bound - padding*2): for i in range(l_bound, r_bound, step): right_pixel = right_img[row:row + block_size, i:i + block_size] # if euclid_dist(left_pixel, right_pixel) c - left_pixel > 0: right_pixel = d_map_right[r, int(c - left_pixel)] else: right_pixel = d_map_right[r, c] if left_pixel == right_pixel: consistency_map[r, c] = left_pixel else: consistency_map[r, c] = 0 sum = 0 for r in range(rows): for c in range(cols): if consistency_map[r, c] != 0: sum = sum + (left_ground_truth[r, c] - consistency_map[r, c]) ** 2 mse_c_left = sum / (rows * cols) return mse_c_left, consistency_map
def consistency_map_mse_r(d_map_left, d_map_right, right_ground_truth): rows, cols = d_map_right.shape consistency_map = np.zeros((rows, cols)) for r in range(rows): for c in range(cols): right_pixel = d_map_right[r, c] if c + right_pixel
نقشههای ناهمخوانی حاصل:
بازسازی صحنه (Scene Reconstruction) در بینایی ماشین
بازسازی صحنه، به فرایند ساختن یک مدل سهبُعدی اطلاق میشود که نمایش دهنده تصویر گرفته شده توسط دوربینها باشد. از جمله کاربردهای مهم این سیستم بینایی ماشین در جهان واقعی میتوان به بازسازی محیط صحنه جرم و تحلیل آن اشاره کرد. علاوه بر این، ساختن نقشههای سهبُعدی ساختمانها برای به نمایش گذاشتن «جهانهای مجازی» (Virtual Worlds) و بازسازی لحظات خاطرهانگیز از مسافرت یا تعطیلات خانوادگی، از جمله کاربردهای مهم فرایند بازسازی صحنه در سیستمهای بینایی ماشین محسوب میشوند. بازسازی صحنه، سبب ایجاد «فریمهای اطلاعاتی مرتب» (Ordered Information Frames) در فرمتی آشنا برای انسانها میشود (درک تصاویر، فریمهای ویدئویی و اطلاعات بامعنی موجود در آنها را برای انسانها تسهیل میکند).
برای بازسازی صحنه در بینایی ماشین و کاربردهای مرتبط، معمولا دو مدل دوربین (Camera Model) مختلف در نظر گرفته میشود؛ «دوربین کالیبره شده» (Calibrated Camera) و «دوربین کالیبره نشده» (Uncalibrated Camera). اصطلاح دوربین کالیبره شده در بینایی ماشین و کاربردهای مرتبط، به دوربینی اطلاق میشود که «نقطه کانونی» (Focal Points)، «میدان دید» (Field of View) و «نوع لنز» (Type of Lens) مشخص داشته باشد. در نقطه مقابل، در حوزه بینایی ماشین به دوربینهایی که ویژگیهای مشخصه آن نظیر نقطه کانونی، میدان دید و نوع لنز آن مشخص نیست و تنها از طریق آزمایش هنگام عکس گرفتن میتوان این دسته از اطلاعات را در مورد آنها کشف کرد، دوربین کالیبره نشده گفته میشود. دوربینهای کالیبره نشده از تکنیکهایی به نام «ساختار به وسیله حرکت» (Structure from Motion | SFM) برای استخراج اطلاعات موقعیتی استفاده میکنند.
تاکنون تحقیقات و مطالعات زیادی در مورد سیستمهای بینایی ماشین و بازسازی صحنه منتشر شده است که از دورینهای کالیبره نشده استفاده کردهاند. با این حال مطالعه سیستمهای بینایی ماشین و بازسازی صحنه که مبتنی بر دوربینهای کالیبره نشده هستند، از حوزه این مطلب خارج است. در این مطلب، روی بازسازی صحنه با استفاده از دوربینهای کالیبره شده تمرکز میشود. از آنجایی که در دوربینهای کالیبره شده، نقطه کانونی، میدان دید و نوع لنز مشخص است و همچنین، فاصله و جهت دوربینها نسبت به یکدیگر شناخته شده است، سیستم قادر خواهد بود تا در مرحله «مطابقت دو سویی» (Stereo Correspondence) از محاسبات هندسه Epipolar استفاده کند.
همانطور که پیش از این نیز اشاره شد، از نقشه ناهمخوانی تولید شده در محله مطابقت دو سویی، میتوان جهت مشخص کردن موقعیت سهبُعدی پیکسل استفاده کرد. به پیکسلی که یه موقعیت سهبُعدی در فضا را اشغال کرده باشد، «پیکسل حجمی» (Volumetric Pixel) یا Voxel گفته میشود. با تصویر کردن (Projecting) هزاران Voxel در فضای اقلیدسی، سیستم بینایی ماشین قادر به بازسازی صحنههایی خواهد بود که توسط انسانها قابل درک هستند. همچنین، صحنه را میتوان از طریق رنگ کردن Voxelها به وسیله میانگین گرفتن از رنگ پیکسلهای اصلی (در تصاویر دوبُعدی)، بافتزنی (Texturing) کرد.
با این حال در چند دهه اخیر، با توجه به محدودیتهای موجود در زمینه «ذخیرهسازی داده» (Data Storage) به صورت فیزیکی، ذخیرهسازی پیکسل سهبُعدی امکانپذیر نبود. روشهای قدیمیتر، از طریق انجام عملیات «دروننمایی» (Interpolation) در عرض «نواحی محلی» (Local Regions) و تولید کردن «مِشهای سطحی» (Surface Meshes)، اقدام به کاهش پیچیدگی صحنهها (Scene Complexity) میکردند. در چنین حالتی و با استفاده از نمایشهای هندسی جوش خورده (Fused) به یکدیگر، بسیاری از پیکسلهای سهبُعدی (Voxels)، در یک «مِش تقریب زده شده» (Approximated Mesh) ادغام شده و از بین میروند؛ در نتیجه، پیچیدگی صحنههای تولید شده کاهش پیدا میکند تا فضای کمتری برای ذخیرهسازی صحنهها نیاز باشد.
سپس، الگوریتمهای هموارسازی (Smoothing) روی مِشهای (Mesh) حاصل اجرا میشوند تا اثر نویز در آنها کاهش پیدا کند. بافت صحنه نیز با ترکیب رنگبندی (Coloration) پیکسلهای سهبُعدی همسایه تخمین زده میشود. اتخاذ چنین روشی در گذشته، برای ذخیرهسازی پیکسلهای سهبُعدی، منجر به کاهش دقت دادهها و از بین رفتن ویژگیهای کوچک در تصویر میشد.
بسیاری از روشهای مدرن، رویکرد متفاوتی را اتخاذ میکنند و از طریق درونیابی اطلاعات «زیرپیکسلهای سهبُعدی» (Sub-Voxel) با استفاده از همسایههای محاصره کننده آنها، جزئیات دقیقتر و بیشتری را به صحنهها اضافه میکنند. در چنین حالتی، صحنه به صورت «اسپارس» (Sparse) توسط پیکسلهای سهبُعدی اشغال میشود. در نتیجه، از روشهای «نمایش داده اسپارس» (Sparse Data Representation) برای نمایش پیکسلهای سهبُعدی استفاده میشود؛ به غیر از حالات خاصی که در آنها، جهت «بازسازی متراکم صحنه» (Dense Scene Reconstruction)، پیکسلهای سهبُعدی در یک «گرید» (Grid) محصور میشوند.
زمانی که موقعیت و جهت دوربین به مقدار اندکی تغییر کرده باشد، جهت کالیبره کردن سریع دوربین، «اطلاعات ویژگی» (Feature Information) نظیر «لبه» (Edge) گوشه (Corner) را میتوان در صحنه ذخیره کرد. با بهبود دقت (Resolution) دوربین و یا اضافه کردن تصاویر بیشتر به مجموعه دادههای تهیه شده برای بازسازی صحنه، میتوان جزئیات موجود در صحنه را بهبود بخشید؛ مانند در دست گرفتن یک شیء ناشناخته و چرخاندن آن در دست، جهت ساختن یک تصویر ذهنی بهتر از ساختار آن.
به چنین روشهایی «ساختار به وسیله حرکت» (Structure From Motion | SFM) گفته میشود. حرکت دادن دوربین، سبب ایجاد تغییرات اندکی در موقعیت و جهت آن میشود و به سیستم اجازه میدهد تا تصاویر جدیدی را از صحنه بگیرد. در این جا هدف سیستم بینایی ماشین این است تا مجموعه تصاویر جدید گرفته شده را، با صحنهای که پیش از این توسط مجموعه تصاویر قبلی بازسازی شده بود، ترکیب کند.
برای چنین کاری، در گام اول باید موقعیت و جهت جدید دوربین نسبت به وضعیت قبلی مشخص شود. برای چنین کاری، «ویژگیهای اسپارس» (Sparse Features) برجسته (لبهها و گوشههایی در تصاویر، که با احتمال بسیار بالا، با یکدیگر مطابقت داده شدهاند) با یکدیگر مقایسه و موقعیت و جهت جدید آنها «برونیابی» (Extrapolate) میشود. به محض اینکه پارامترهای جدید دوربین مشخص شدند، میتوان پیکسلهای سهبُعدی (Voxel) جدید را به صحنه بازسازی شده اضافه کرد. همچنین، با سوار کردن دو دوربین روی یک شاسی، ضبط کردن «جریانهای ویدئویی» (Video Streams) از هر کدام از این دوربینها و حرکت دادن دوربینها در یک اتاق، میتوان به «تصاویر دو سویی» (Stereo Images) متوالی دست پیدا کرد.
استنتاج کردن و بیرون آوردن هر کدام از اشیاء موجود در صحنه بازسازی شده، برای انسان کار بسیار سادهای است. در حالی که چنین کاری، فرایندی بسیار پیچیده برای یک سیستم کامپیوتری محسوب میشود. یک روش ساده ولی مؤثر برای شناسایی اشیاء موجود در صحنههای بازسازی شده، خوشهبندی کردن پیکسلهای سهبُعدی (Voxels) بر اساس رنگبندی و فاصله اقلیدسی و نگاشت کردن آنها به اشیاء منحصر به فرد موجود در تصویر است. فاکتورهایی نظیر «تعامل انسانی» (Human Interaction)، «الگوریتمهای یادگیری» (Learning Algorithms) و یا تجربه، میتوانند به سیستم بینایی ماشین جهت ایجاد تناظر و ارتباط میان اشیاء کمک کنند (به عنوان نمونه، یک صندلی از پایه و بدنه آن تشکیل شده است و با ترکیب این دو شیء، میتوان صندلی را در صحنههای بازسازی شده شناسایی کرد).
نکته شایان توجه در مورد این دسته از سیستمهای بینایی ماشین این است که روشهای بازسازی صحنه، یک فرض اساسی را در سیستم مطرح میکنند؛ صحنههای موجود در تصاویر، «مانا» (Static | ایستا) هستند. به عبارت دیگر، صحنه میان تصاویر ثابت است و تغییری نمیکند. ولی این فرضیه، فرضیه واقعگرایانهای برای جهان واقعی نیست. برای غلبه بر تغییرات موجود در صحنهها، میتوان نرخ نمونهگیری (Sampling Rate) دوربینها را افزایش داد و به طور منظم و دورهای، صحنهای که در آن پیکسلهای سهبُعدی (Voxels) تغییر میکنند را دور انداخت.
نرخ نمونهگیری دوربینها به این دلیل افزایش داده میشود که هر چقدر نرخ تغییرات میان صحنهها (در واحد زمان) به صفر نزدیک میشود، نرخ تغییرات در حرکت اشیاء (میان صحنهها) نیز به صفر همگرا میشود؛ در نتیجه، یک صحنه استاتیک آنی (Instantaneous Static Scene) تشکیل میشود.پیادهسازی سیستم بازسازی صحنه در پایتون
با استفاده از قطعه کد زیر، تصاویر رنگی (Color Images) و تصاویر عمق (Depth Images) با یکدیگر ترکیب میشوند تا صحنه (Scene) سهبُعدی متناظر با این تصاویر بازسازی یا ساخته شود. در این قطعه کد، 1000 تصویر (متشکل از تصاویر RGB و تصاویر عمق متناظر آنها) از مجموعه داده 7scenes انتخاب شدهاند. صحنه بازسازی شده، یک صحنه متشکل از 405x267x289 پیکسل سهبُعدی است. دادههای مورد استفاده از طریق لینک [+] قابل دسترسی هستند.
کدهای پیادهسازی سیستم بازسازی صحنه در پایتون:
import numpy as np
from skimage import measure
try: import pycuda.driver as cuda import pycuda.autoinit from pycuda.compiler import SourceModule FUSION_GPU_MODE = 1
except Exception as err: print('Warning: %s'%(str(err))) print('Failed to import PyCUDA. Running fusion in CPU mode.') FUSION_GPU_MODE = 0
class TSDFVolume(object): def __init__(self,vol_bnds,voxel_size): # Define voxel volume parameters self._vol_bnds = vol_bnds # rows: x,y,z columns: min,max in world coordinates in meters self._voxel_size = voxel_size # in meters (determines volume discretization and resolution) self._trunc_margin = self._voxel_size*5 # truncation on SDF # Adjust volume bounds self._vol_dim = np.ceil((self._vol_bnds[:,1]-self._vol_bnds[:,0])/self._voxel_size).copy(order='C').astype(int) # ensure C-order contigous self._vol_bnds[:,1] = self._vol_bnds[:,0]+self._vol_dim*self._voxel_size self._vol_origin = self._vol_bnds[:,0].copy(order='C').astype(np.float32) # ensure C-order contigous print("Voxel volume size: %d x %d x %d"%(self._vol_dim[0],self._vol_dim[1],self._vol_dim[2])) # Initialize pointers to voxel volume in CPU memory self._tsdf_vol_cpu = np.ones(self._vol_dim).astype(np.float32) self._weight_vol_cpu = np.zeros(self._vol_dim).astype(np.float32) # for computing the cumulative moving average of observations per voxel self._color_vol_cpu = np.zeros(self._vol_dim).astype(np.float32) # Copy voxel volumes to GPU if FUSION_GPU_MODE: self._tsdf_vol_gpu = cuda.mem_alloc(self._tsdf_vol_cpu.nbytes) cuda.memcpy_htod(self._tsdf_vol_gpu,self._tsdf_vol_cpu) self._weight_vol_gpu = cuda.mem_alloc(self._weight_vol_cpu.nbytes) cuda.memcpy_htod(self._weight_vol_gpu,self._weight_vol_cpu) self._color_vol_gpu = cuda.mem_alloc(self._color_vol_cpu.nbytes) cuda.memcpy_htod(self._color_vol_gpu,self._color_vol_cpu) # Cuda kernel function (C++) self._cuda_src_mod = SourceModule(""" __global__ void integrate(float * tsdf_vol, float * weight_vol, float * color_vol, float * vol_dim, float * vol_origin, float * cam_intr, float * cam_pose, float * other_params, float * color_im, float * depth_im) { // Get voxel index int gpu_loop_idx = (int) other_params[0]; int max_threads_per_block = blockDim.x; int block_idx = blockIdx.z*gridDim.y*gridDim.x+blockIdx.y*gridDim.x+blockIdx.x; int voxel_idx = gpu_loop_idx*gridDim.x*gridDim.y*gridDim.z*max_threads_per_block+block_idx*max_threads_per_block+threadIdx.x; int vol_dim_x = (int) vol_dim[0]; int vol_dim_y = (int) vol_dim[1]; int vol_dim_z = (int) vol_dim[2]; if (voxel_idx > vol_dim_x*vol_dim_y*vol_dim_z) return; // Get voxel grid coordinates (note: be careful when casting) float voxel_x = floorf(((float)voxel_idx)/((float)(vol_dim_y*vol_dim_z))); float voxel_y = floorf(((float)(voxel_idx-((int)voxel_x)*vol_dim_y*vol_dim_z))/((float)vol_dim_z)); float voxel_z = (float)(voxel_idx-((int)voxel_x)*vol_dim_y*vol_dim_z-((int)voxel_y)*vol_dim_z); // Voxel grid coordinates to world coordinates float voxel_size = other_params[1]; float pt_x = vol_origin[0]+voxel_x*voxel_size; float pt_y = vol_origin[1]+voxel_y*voxel_size; float pt_z = vol_origin[2]+voxel_z*voxel_size; // World coordinates to camera coordinates float tmp_pt_x = pt_x-cam_pose[0*4+3]; float tmp_pt_y = pt_y-cam_pose[1*4+3]; float tmp_pt_z = pt_z-cam_pose[2*4+3]; float cam_pt_x = cam_pose[0*4+0]*tmp_pt_x+cam_pose[1*4+0]*tmp_pt_y+cam_pose[2*4+0]*tmp_pt_z; float cam_pt_y = cam_pose[0*4+1]*tmp_pt_x+cam_pose[1*4+1]*tmp_pt_y+cam_pose[2*4+1]*tmp_pt_z; float cam_pt_z = cam_pose[0*4+2]*tmp_pt_x+cam_pose[1*4+2]*tmp_pt_y+cam_pose[2*4+2]*tmp_pt_z; // Camera coordinates to image pixels int pixel_x = (int) roundf(cam_intr[0*3+0]*(cam_pt_x/cam_pt_z)+cam_intr[0*3+2]); int pixel_y = (int) roundf(cam_intr[1*3+1]*(cam_pt_y/cam_pt_z)+cam_intr[1*3+2]); // Skip if outside view frustum int im_h = (int) other_params[2]; int im_w = (int) other_params[3]; if (pixel_x = im_w || pixel_y = im_h || cam_pt_z self._vol_bnds[dim,1]: # expand upper bounds # n_voxels_expand = int(np.ceil((new_bnds[dim,1]-self._vol_bnds[dim,1])/self._voxel_size)) # new_chunk_size = np.round((self._vol_bnds[:,1]-self._vol_bnds[:,0])/self._voxel_size).astype(int) # new_chunk_size[dim] = n_voxels_expand # size of expanding region (i.e. chunk) # # Initialize chunks and concatenate to current voxel volume # self._tsdf_vol_cpu = np.concatenate((self._tsdf_vol_cpu,np.ones(new_chunk_size)),axis=dim) # self._weight_vol_cpu = np.concatenate((self._weight_vol_cpu,np.zeros(new_chunk_size)),axis=dim) # self._color_vol_cpu = np.concatenate((self._color_vol_cpu,np.zeros(new_chunk_size)),axis=dim) # self._vol_bnds[dim,1] += n_voxels_expand*self._voxel_size # update voxel volume bounds def integrate(self,color_im,depth_im,cam_intr,cam_pose,obs_weight=1.): im_h = depth_im.shape[0] im_w = depth_im.shape[1] # Fold RGB color image into a single channel image color_im = color_im.astype(np.float32) color_im = np.floor(color_im[:,:,2]*256*256+color_im[:,:,1]*256+color_im[:,:,0]) # GPU mode: integrate voxel volume (calls CUDA kernel) if FUSION_GPU_MODE: for gpu_loop_idx in range(self._n_gpu_loops): self._cuda_integrate(self._tsdf_vol_gpu, self._weight_vol_gpu, self._color_vol_gpu, cuda.InOut(self._vol_dim.astype(np.float32)), cuda.InOut(self._vol_origin.astype(np.float32)), cuda.InOut(cam_intr.reshape(-1).astype(np.float32)), cuda.InOut(cam_pose.reshape(-1).astype(np.float32)), cuda.InOut(np.asarray([gpu_loop_idx,self._voxel_size,im_h,im_w,self._trunc_margin,obs_weight],np.float32)), cuda.InOut(color_im.reshape(-1).astype(np.float32)), cuda.InOut(depth_im.reshape(-1).astype(np.float32)), block=(self._max_gpu_threads_per_block,1,1),grid=(int(self._max_gpu_grid_dim[0]),int(self._max_gpu_grid_dim[1]),int(self._max_gpu_grid_dim[2]))) # CPU mode: integrate voxel volume (vectorized implementation) else: # Get voxel grid coordinates xv,yv,zv = np.meshgrid(range(self._vol_dim[0]),range(self._vol_dim[1]),range(self._vol_dim[2]),indexing='ij') vox_coords = np.concatenate((xv.reshape(1,-1),yv.reshape(1,-1),zv.reshape(1,-1)),axis=0).astype(int) # Voxel coordinates to world coordinates world_pts = self._vol_origin.reshape(-1,1)+vox_coords.astype(float)*self._voxel_size # World coordinates to camera coordinates world2cam = np.linalg.inv(cam_pose) cam_pts = np.dot(world2cam[:3,:3],world_pts)+np.tile(world2cam[:3,3].reshape(3,1),(1,world_pts.shape[1])) # Camera coordinates to image pixels pix_x = np.round(cam_intr[0,0]*(cam_pts[0,:]/cam_pts[2,:])+cam_intr[0,2]).astype(int) pix_y = np.round(cam_intr[1,1]*(cam_pts[1,:]/cam_pts[2,:])+cam_intr[1,2]).astype(int) # Skip if outside view frustum valid_pix = np.logical_and(pix_x >= 0, np.logical_and(pix_x = 0, np.logical_and(pix_y 0)))) depth_val = np.zeros(pix_x.shape) depth_val[valid_pix] = depth_im[pix_y[valid_pix],pix_x[valid_pix]] # Integrate TSDF depth_diff = depth_val-cam_pts[2,:] valid_pts = np.logical_and(depth_val > 0,depth_diff >= -self._trunc_margin) dist = np.minimum(1.,np.divide(depth_diff,self._trunc_margin)) w_old = self._weight_vol_cpu[vox_coords[0,valid_pts],vox_coords[1,valid_pts],vox_coords[2,valid_pts]] w_new = w_old + obs_weight self._weight_vol_cpu[vox_coords[0,valid_pts],vox_coords[1,valid_pts],vox_coords[2,valid_pts]] = w_new tsdf_vals = self._tsdf_vol_cpu[vox_coords[0,valid_pts],vox_coords[1,valid_pts],vox_coords[2,valid_pts]] self._tsdf_vol_cpu[vox_coords[0,valid_pts],vox_coords[1,valid_pts],vox_coords[2,valid_pts]] = np.divide(np.multiply(tsdf_vals,w_old)+dist[valid_pts],w_new) # Integrate color old_color = self._color_vol_cpu[vox_coords[0,valid_pts],vox_coords[1,valid_pts],vox_coords[2,valid_pts]] old_b = np.floor(old_color/(256.*256.)) old_g = np.floor((old_color-old_b*256.*256.)/256.) old_r = old_color-old_b*256.*256.-old_g*256. new_color = color_im[pix_y[valid_pts],pix_x[valid_pts]] new_b = np.floor(new_color/(256.*256.)) new_g = np.floor((new_color-new_b*256.*256.)/256.) new_r = new_color-new_b*256.*256.-new_g*256. new_b = np.minimum(np.round(np.divide(np.multiply(old_b,w_old)+new_b,w_new)),255.); new_g = np.minimum(np.round(np.divide(np.multiply(old_g,w_old)+new_g,w_new)),255.); new_r = np.minimum(np.round(np.divide(np.multiply(old_r,w_old)+new_r,w_new)),255.); self._color_vol_cpu[vox_coords[0,valid_pts],vox_coords[1,valid_pts],vox_coords[2,valid_pts]] = new_b*256.*256.+new_g*256.+new_r; # Copy voxel volume to CPU def get_volume(self): if FUSION_GPU_MODE: cuda.memcpy_dtoh(self._tsdf_vol_cpu,self._tsdf_vol_gpu) cuda.memcpy_dtoh(self._color_vol_cpu,self._color_vol_gpu) return self._tsdf_vol_cpu,self._color_vol_cpu # Get mesh of voxel volume via marching cubes def get_mesh(self): tsdf_vol,color_vol = self.get_volume() # Marching cubes verts,faces,norms,vals = measure.marching_cubes_lewiner(tsdf_vol,level=0) verts_ind = np.round(verts).astype(int) verts = verts*self._voxel_size+self._vol_origin # voxel grid coordinates to world coordinates # Get vertex colors rgb_ tablighat...
برچسب : نویسنده : agahya بازدید : 136