В этом году сайту исполнилось 20 лет, а двенадцать лет назад была сформулирована идея фотокубиков и началось их коллекционирование. Теперь их скопилось достаточно, чтобы было можно быстро материализовать любую идею в прототип и проверить ее на прочность. А уж что получится, воздушный замок или карточный домик, это вопрос везения :-)
Были взяты три Pi камеры и кубик управления объективами
Canon. Связь с Raspberry Pi осуществлялась через USB, хотя
логичнее было подключиться через последовательный порт. Но у
Arduino 5 В, а у Raspberry 3 В, и было лень согласовывать
напряжения. Две камеры образовали стереопару, а оставшаяся могла
использоваться практически с любыми сменными объективами от
Зенита, Nikon и Canon. Причем для последних было реализовано
дистанционное управление диафрагмой, фокусировка и
автофокусировка. В отличие от предыдущих многокамерных проектов в
этот раз использовался всего один компьютер Raspberry Pi3 и блок
переключения камер - Multi
Camera Adapter. Конечно, с точки зрения получения идеально
синхронных снимков это шаг назад, но зато появилась возможность
существенно обогатить возможности интерфейса и получить почти
живую стереокартинку на удаленном компьютере или телефоне. Если
конечно считать, что считанные кадры в секунду это жизнь.
Переключателей можно подключить к одному компьютеру аж 4 и получить в результате управление 16 камерами. Однако выяснилось, что ведут эти блоки себя немного по-разному, и я пока ограничился единственным и подключил к нему всего 3 камеры. Мощность нового компьютера Raspberry Pi3 позволяла установить на него OpenCV за разумное время в пару часов и получить относительно резвую картинку даже с преобразованием проекции объектива рыбий глаз в нормальную. Причем это можно было делать именно как преобразование проекции, а не подгонкой параметров дисторсии. Т.е. знание типа объектива и его фокусного расстояния без длительной калибровки сразу давало удовлетворительный результат.
Установка OpenCV 3 была проведена по инструкции Адриана
Розеброка (Adrian Rosebrock) Install
guide: Raspberry Pi 3 + Raspbian Jessie + OpenCV 3 для
Python 3. Следуя инструкции, я установил все в виртуальное
окружение (Virtualenv), хотя для машины, которая будет выполнять
всего одну программу, это пожалуй лишнее. На мой взгляд, для
подобных задач лучше иметь сменную карту памяти с системой,
настроенной на выполнение именно конкретной задачи. Впрочем, для
экспериментов это довольно удобное решение.
Когда установка была собрана, я задался задачей написать достаточно удобную программу для перебора всех возможных вариантов использования и настройки камер. Подбор параметров без графического интерфейса занимает слишком много времени. Поэтому оттачивая графический интерфейс, я надеюсь в дальнейшем сэкономить время и при написании программ для камер, которым интерфейс вообще не требуется. Требования к интерфейсу были следующие: Он должен был быть виден на экране, подключенном через HDMI при включенном предпросмотре силами GPU. Поскольку у меня монитор широкоформатный, а камера дает изображение с отношением сторон 4:3, то необходимо было уместить все кнопки в узкой полосе сбоку от экрана, причем меню должно было сразу оказаться в нужном месте и не требовать перетаскивания. На всякий случай для выхода из предварительного просмотра была предусмотрена большая кнопка, попасть по которой можно и вслепую, если она будет закрыта живой картинкой. Просмотр в виде живой картинки средствами GPU дает очень высокую частоту смены кадров и возможность просмотра фрагмента изображения с размером большим разрешения экрана. Это очень удобно при юстировке оптической системы. Для широкоугольных объективов смещение и перекос на десятую мм уже смертелен и часто стопорный винт уже дает неприемлемое смещение объектива. Однако основное назначение программы это формирование изображения для предпросмотра средствами OpenCV, как на экране подключенного через HDMI монитора, так и на удаленном рабочем столе через VNC. С учетом имеющегося опыта интерфейс был реализован на Tkinter и Python 3. У Адриана есть хороший пример и на эту тему OpenCV with Tkinter, но для Python 2. Синтаксис может и не сильно отличается, но достаточно, чтобы можно было прямо воспользоваться кодом. Кроме того, необходимость двойного преобразования форматов наводит на мысль о использовании других средств для создания графического интерфейса. Сперва надо переставить каналы и преобразовать из формата OpenCV в формат PIL/Pillow (Pillow - форк библиотеки PIL, Python Imaging Library), а затем этот формат преобразовать в формат ImageTk.
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = Image.fromarray(image)
image = ImageTk.PhotoImage(image)
Ниже представлены несколько снимков экрана, демонстрирующие возможности того что у меня получилось.
Рассмотрим подробнее возможности, предоставляемые меню:
Приведены два снимка меню, как они выглядят в разных режимах. А
теперь рассмотрим построчно:
Исправление искажений при постановке галочки в поле DP. Срабатывает на лету.
Для коррекции искажений за основу взят пример opencv-python-fisheye-example. Рассмотрим подпрограмму делающую исправленный стереоснимок:
# Стереоснимок
def sinxv3s():
global flag,frs
if flag==0:
sels()
else:
flag=0
picam2()
panelB = None
panelA = None
camera.resolution = (480, 640)
camsetcv()
rawCapture = PiRGBArray(camera, size=(480, 640))
time.sleep(0.1)
j=0
k=0
for frame in camera.capture_continuous(rawCapture, format="bgr", \
use_video_port=True):
if j==0:
image0 = frame.array
gp.output(7, False)
j=1
else:
image1 = frame.array
gp.output(7, True)
j=0
rawCapture.truncate(0)
k=k+1
if k == 2:
break
# Меняем проекцию с рыбьего глаза на прямолинейную
if flagdp.get() == 1:
fc=120*fcams/(z1+20)
cx=240-6*xt*(1-z1/100)
K = np.array([[ fc, 0. , cx],
[ 0. , fc, 320],
[ 0. , 0. , 1. ]])
# Дисторсию кладем равной нулю
D = np.array([0., 0., 0., 0.])
# используем Knew для масштабирования
Knew = K.copy()
fsc=0.89 +(100-z1)/(500-2*z1)
Knew[(0,1), (0,1)] = fsc*Knew[(0,1), (0,1)]
image0 = cv2.fisheye.undistortImage(image0, K, D=D, Knew=Knew)
image1 = cv2.fisheye.undistortImage(image1, K, D=D, Knew=Knew)
imgL = image0
imgR = image1
rows,cols,ch = imgL.shape
# Совмещение изображений
M = np.float32([[1,0,-14],[0,1,-17]])#сдвиг x,y
M1 = cv2.getRotationMatrix2D((cols/2,rows/2),-3.3,1)#угол, масштаб
# Применяем сдвиг и поворот
dst = cv2.warpAffine(imgL,M1,(cols,rows))
dst = cv2.warpAffine(dst,M,(910,610))
# Объединяем изображения
dst[0:610, 455:910] = imgR[0:610, 10:465]#y1:y2,x1:x2
# Записываем стереопару
frs=frs+1
cv2.imwrite("/home/pi/fotopicam/"+ffs+"st%03d.jpg" % frs, dst)
# Вывод изображения на экран
image = cv2.cvtColor(dst, cv2.COLOR_BGR2RGB)
image = Image.fromarray(image)
image = ImageTk.PhotoImage(image)
if panelA is None :
panelA = Label(image=image)
panelA.image = image
panelA.grid(row=0,column=0,rowspan=24)
else:
panelA.configure(image=image)
panelA.image = image
root.update()
while flag==0:
time.sleep(0.1)
root.update()
panelA.grid_forget()
Используется функция image = cv2.fisheye.undistortImage(image, K, D, Knew). Где К матрица вида:
| fc | 0 | cx |
| 0 | fc | cy |
| 0 | 0 | 1 |
fc - фокусное расстояние в пикселях. Т.е. если у нас объектив рыбий глаз с фокусным расстоянием 18 мм, матрица размером 36х24 мм и кадр 640х480 пикселей, то фокусное расстояние будет равно 320. Фокусное расстояние в матрице встречается дважды, предполагается, что объектив может быть анаморфотным и иметь разное фокусное расстояние в вертикальной горизонтальной плоскостях. cx и cy координаты центра. D - дисторсия, мы ее полагаем равной 0. Сделав серию снимков шахматной доски, можно вычислить точные значения коэффициентов для конкретного объектива. Угол обзора и фокусное расстояние в мм можно получить командой: finfo=cv2.calibrationMatrixValues(K,(640,480),36,24)
В идеале надо максимально совместить изображения, физически перемещая и вращая камеры. В данном случае я этого не делал и решил проверить, насколько качественно и быстро это можно сделать программно. Для совмещения стереоизображений в OpenCV есть специальные функции, однако я не разобрался с синтаксисом и просто вращаю и сдвигаю одно из изображений.
Вычисление карты глубин:
Для вычисления карты глубин в OpenCV есть несколько функций. У меня лучше получилось с StereoBM. Важно отметить, что результат сильно зависит от качества совмещения по вертикали и исправления искажений. Модуль вычисления карты глубин приведен ниже.
# Карта глубин
def sinxv4s():
global flag,frs
if flag==0:
sels()
else:
flag=0
picam2()
panelB = None
panelA = None
camera.resolution = (480, 640)
camsetcv()
rawCapture = PiRGBArray(camera, size=(480, 640))
time.sleep(0.1)
j=0
k=0
for frame in camera.capture_continuous(rawCapture, format="bgr", \
use_video_port=True):
if j==0:
image0 = frame.array
gp.output(7, False)
j=1
else:
image1 = frame.array
gp.output(7, True)
j=0
rawCapture.truncate(0)
k=k+1
if k == 2:
break
# Правим дисторсию
if flagdp.get() >= 0:
fc=120*fcams/(z1+20)
cx=240-6*xt*(1-z1/100)
K = np.array([[ fc, 0. , cx],
[ 0. , fc, 320],
[ 0. , 0. , 1. ]])
D = np.array([0., 0., 0., 0.])
Knew = K.copy()
fsc=0.89 +(100-z1)/(500-2*z1)
Knew[(0,1), (0,1)] = fsc*Knew[(0,1), (0,1)]
image0 = cv2.fisheye.undistortImage(image0, K, D=D, Knew=Knew)
image1 = cv2.fisheye.undistortImage(image1, K, D=D, Knew=Knew)
imgL = image0
imgR = image1
rows,cols,ch = imgL.shape
# Совмещение изображений
mm=(z1-100)/4 -17
M = np.float32([[1,0,-14],[0,1,mm]])#сдвиг x,y
M1 = cv2.getRotationMatrix2D((cols/2,rows/2),-3.3,1)#угол, масштаб
# Применяем сдвиг и поворот
dst = cv2.warpAffine(imgL,M1,(cols,rows))
dst = cv2.warpAffine(dst,M,(910,610))
# Объединяем изображения
dst[0:610, 455:910] = imgR[0:610, 10:465]#y1:y2,x1:x2
# Записываем стереопару
frs=frs+1
cv2.imwrite("/home/pi/fotopicam/"+ffs+"st%03d.jpg" % frs, dst)
# Вычисляем карту глубин
stereo = cv2.StereoBM_create(numDisparities=32, blockSize=25)
imge = cv2.cvtColor(dst, cv2.COLOR_BGR2GRAY)
imgR1 = imge[0:610, 455:910]
imgL1 = imge[0:610, 0:455]
disp = stereo.compute(imgL1,imgR1)
cv2.imwrite("/home/pi/fotopicam/"+ffs+"stdp%03d.jpg" % frs, disp)
image = (disp-0)/1
imge[0:610, 455:910] = image[0:610, 0:455]
image = Image.fromarray(imge)
image = ImageTk.PhotoImage(image)
if panelA is None :
panelA = Label(image=image)
panelA.image = image
panelA.grid(row=0,column=0,rowspan=24)
else:
panelA.configure(image=image)
panelA.image = image
root.update()
while flag==0:
time.sleep(0.1)
root.update()
panelA.grid_forget()
Автофокусировка:
Результат фокусировки на миру. Реализован простейший алгоритм. Сперва сдвигаем объектив на минимальную дистанцию, затем движемся к бесконечности, анализируя локальный контраст с помощью оператора Лапласа. Если изображение абсолютно не резкое, как в нашем случае светосильного длинофокусного для данной матрицы объектива 50/1,4 то определить в какую сторону надо двигаться, нереально, приходится начинать с края. Ниже приведена подпрограмма, отвечающая за фокусировку.
# Быстрая фокусировка
def fcanon4():
global flag
if flag==0:
sels()
else:
flag=0
picam3()
panelB = None
panelA = None
camera.resolution = (640, 480)
camera.rotation = 180
fram=int(Spinbox1.get())
camera.framerate = fram
#camera.zoom = (0,0,1,1)
serialcmd = "800000000006" #Сфокусироваться на минимальную дистанцию
port.write(serialcmd.encode())
rawCapture = PiRGBArray(camera, size=(640, 480))
time.sleep(0.1)
fm=0
for frame in camera.capture_continuous(rawCapture, format="bgr", \
use_video_port=True):
canfi()
image = frame.array
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
fm = cv2.Laplacian(gray, cv2.CV_64F).var()
labelf.config(text= str(int(fm)))
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = Image.fromarray(image)
image = ImageTk.PhotoImage(image)
if panelA is None :
panelA = Label(image=image)
panelA.image = image
panelA.grid(row=0,column=0,rowspan=22)
else:
panelA.configure(image=image)
panelA.image = image
rawCapture.truncate(0)
root.update()
if flag == 1:
break
if fm > 30:
break
for frame in camera.capture_continuous(rawCapture, format="bgr", \
use_video_port=True):
fm1=fm
serialcmd = "8000000007t03"
port.write(serialcmd.encode())
image = frame.array
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
fm = cv2.Laplacian(gray, cv2.CV_64F).var()
labelf.config(text= str(int(fm)))
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = Image.fromarray(image)
image = ImageTk.PhotoImage(image)
if panelA is None :
panelA = Label(image=image)
panelA.image = image
panelA.grid(row=0,column=0,rowspan=22)
else:
panelA.configure(image=image)
panelA.image = image
rawCapture.truncate(0)
root.update()
if flag == 1:
break
if fm1 > fm:
break
serialcmd = "8000000007t]-"
port.write(serialcmd.encode())
time.sleep(0.1)
port.write(serialcmd.encode())
panelA.grid_forget()
panelB = None
panelA = None
flag=1
Дистанционная ручная фокусировка тоже может быть полезна, например при съемках луны, когда малейшее сотрясение нежелательно.
Снимок сделан объективом Canon EF 135 мм.
Кстати, не сложно объединить данную конструкцию с кубиком вращающим камеру и реализовать слежение.
Для настройки баланса белого возможен как выбор предустановленных настроек, так и регулировка усиления в красном и синем каналах. На нижеприведенном снимке усиление в красном канале увеличено примерно в два раза, а в синем уменьшено.
Просмотр стереопары через телефон:
Просмотр стерео картинок на компьютере подробнее описан в статьях Стереокамера на 2-х Raspberry Pi и Стереосъемка.
Управлять камерой можно с телефона или компьютера через VNC, а если к камере подключен монитор, то можно воспользоваться Bluetooth клавиатурой с сенсорной панелью.
В программе много невычищенного мусора, тем не менее для желающих публикую ее полный текст.