3 камеры на один Raspberry Pi3
Использование OpenCV для получения живой стереокартинки,
исправления искажений, получения карты глубин и
автофокусировки
В этом году сайту исполнилось 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)
Ниже представлены несколько снимков экрана, демонстрирующие
возможности того что у меня получилось.
Рассмотрим подробнее возможности, предоставляемые меню:
Приведены два снимка меню, как они выглядят в разных режимах. А
теперь рассмотрим построчно:
- Режим камеры (P, auto, off) и размер снимка
- Баланс белого. В центре предустановленные режимы. При off
слева уровень красного, справа синего
- B&W - черно белый снимок, full - полноэкранный режим для
монитора HDMI, DP - исправление искажений для объективов рыбий
глаз
- Кнопка позволяет вывести в следующей строке выдержку, выбранную
автоматикой
- Ручная установка выдержки
- Предпросмотр через GPU и монитор, подключенный к HDMI, Stop -
остановка всех режимов просмотра
- Изменение масштаба выводимой картинки. Для GPU, если задан
размер кадра больший экрана, это фрагмент, для CV просто
увеличение изображения
- Сдвиг вправо-влево увеличенного изображения
- Выбор камеры. L -левая стереокамера, R- правая. При выборе
камеры кнопка меняет цвет на красный
- Предпросмотр средствами CV всей картинки и два варианта вывода
стерео изображения. ST- последовательно делаются два снимка,
объединяются в одно изображение и выводятся на экран, ST1 -
снимки выводятся сразу после съемки по отдельности
- Частота кадров. Определяет максимальную по длительности
выдержку
- Чувствительность. 0(автомат), 100, 200, 400, 800
- Компенсация экспозиции
- Контраст
- Dynamic Range Compression - сжатие динамического
диапазона
- Управление диафрагмой объективов Canon. Закрыть на пол деления
(cd+), открыть (cd-), полностью открыть (cdo)
- Управление фокусировкой объективов Canon. inf к бесконечности,
macro к ближнему плану, AF - автоматическая фокусировка. Когда
выбраны не управляемые объективы, кнопки сереют
- Кнопка производит замер локального контраста и выводит
его в поле рядом. Этот параметр служит для оценки качества
фокусировки и используется при автофокусировке. В процессе
автофокусировки выводятся текущие значения. Последняя в ряду
кнопка, запускающая другой алгоритм фокусировки. Более быстрый
чем на предыдущей строке, но все равно около 15 секунд
- Кнопки разных режимов съемки. F1- без исправления искажений, но
с записью параметров в EXIF, F2 с исправлениями, если стоит
соответствующая галочка в строке 3, но без EXIF, SF - съемка
стереопары и запись единым кадром, DP - съемка и запись
стереопары с коррекцией вне зависимости от галочки и вычисление
и запись карты глубин.
Исправление искажений при постановке галочки в поле 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 - фокусное расстояние в пикселях. Т.е. если у нас объектив рыбий глаз с фокусным расстоянием 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 клавиатурой с сенсорной панелью.
В программе много невычищенного мусора, тем не менее для желающих публикую ее полный текст.
10.10.2016
Установите проигрыватель Flash
|
Облако тегов:
...
|