3 камеры на один  Raspberry Pi3

Использование OpenCV для получения живой стереокартинки, исправления искажений, получения карты глубин и автофокусировки 

Raspberry
          Pi

В этом году сайту исполнилось 20 лет, а двенадцать лет назад была сформулирована идея фотокубиков и началось их коллекционирование. Теперь их скопилось достаточно, чтобы было можно быстро материализовать любую идею в прототип и проверить ее на прочность. А уж что получится, воздушный замок или карточный домик, это вопрос везения :-)

Были взяты три Pi камеры и кубик управления объективами Canon. Связь с Raspberry Pi осуществлялась через USB, хотя логичнее было подключиться через последовательный порт. Но у Arduino 5 В, а у Raspberry 3 В, и было лень согласовывать напряжения. Две камеры образовали стереопару, а оставшаяся могла использоваться практически с любыми сменными объективами от Зенита, Nikon и Canon. Причем для последних было реализовано дистанционное управление диафрагмой, фокусировка и автофокусировка. В отличие от предыдущих многокамерных проектов в этот раз использовался всего один компьютер Raspberry Pi3 и блок переключения камер - Multi Camera Adapter. Конечно, с точки зрения получения идеально синхронных снимков это шаг назад, но зато появилась возможность существенно обогатить возможности интерфейса и получить почти живую стереокартинку на удаленном компьютере или телефоне. Если конечно считать, что считанные кадры в секунду это жизнь.

Raspberry
          Pi

Переключателей можно подключить к одному компьютеру аж 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)

Ниже представлены несколько снимков экрана, демонстрирующие возможности того что у меня получилось.

Raspberry Pi

Рассмотрим подробнее возможности, предоставляемые меню:

Raspberry Pi

Приведены два снимка меню, как они выглядят в разных режимах. А теперь рассмотрим построчно:

  1. Режим камеры (P, auto, off) и размер снимка
  2. Баланс белого. В центре предустановленные режимы. При off слева уровень красного, справа синего
  3. B&W - черно белый снимок, full - полноэкранный режим для монитора HDMI, DP - исправление искажений для объективов рыбий глаз
  4. Кнопка позволяет вывести в следующей строке выдержку, выбранную автоматикой
  5. Ручная установка выдержки
  6. Предпросмотр через GPU и монитор, подключенный к HDMI, Stop - остановка всех режимов просмотра
  7. Изменение масштаба выводимой картинки. Для GPU, если задан размер кадра больший экрана, это фрагмент, для CV просто увеличение изображения
  8. Сдвиг вправо-влево увеличенного изображения
  9. Выбор камеры. L -левая стереокамера, R- правая. При выборе камеры кнопка меняет цвет на красный
  10. Предпросмотр средствами CV всей картинки и два варианта вывода стерео изображения. ST- последовательно делаются два снимка, объединяются в одно изображение и выводятся на экран, ST1 - снимки выводятся сразу после съемки по отдельности
  11. Частота кадров. Определяет максимальную по длительности выдержку
  12. Чувствительность. 0(автомат), 100, 200, 400, 800
  13.  Компенсация экспозиции
  14. Контраст
  15.  Dynamic Range Compression - сжатие динамического диапазона
  16. Управление диафрагмой объективов Canon. Закрыть на пол деления (cd+), открыть (cd-), полностью открыть (cdo)
  17. Управление фокусировкой объективов Canon. inf к бесконечности, macro к ближнему плану, AF - автоматическая фокусировка. Когда выбраны не управляемые объективы, кнопки сереют
  18. Кнопка производит замер локального контраста  и выводит его в поле рядом. Этот параметр служит для оценки качества фокусировки и используется при автофокусировке. В процессе автофокусировки выводятся текущие значения. Последняя в ряду кнопка, запускающая другой алгоритм фокусировки. Более быстрый чем на предыдущей строке, но все равно около 15 секунд
  19. Кнопки разных режимов съемки. F1- без исправления искажений, но с записью параметров в EXIF, F2 с исправлениями, если стоит соответствующая галочка в строке 3, но без EXIF, SF - съемка стереопары и запись единым кадром, DP - съемка и запись стереопары с коррекцией вне зависимости от галочки и вычисление и запись карты глубин.

Исправление искажений при постановке галочки в поле DP. Срабатывает на лету.

Raspberry Pi
Raspberry Pi

Для коррекции искажений за основу взят пример 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 есть специальные функции, однако я не разобрался с синтаксисом и просто вращаю и сдвигаю одно из изображений.

Вычисление карты глубин:

Raspberry Pi

Для вычисления карты глубин в 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 мм.

Canon 135
135 мм

Кстати, не сложно объединить данную конструкцию с кубиком вращающим камеру и реализовать слежение.

Для настройки баланса белого возможен как выбор предустановленных настроек, так и регулировка усиления в красном и синем каналах. На нижеприведенном снимке усиление в красном канале увеличено примерно в два раза, а в синем уменьшено.

Цвет

Просмотр стереопары через телефон:

Стерео 

Просмотр стерео картинок на компьютере подробнее описан в статьях Стереокамера на 2-х Raspberry Pi и Стереосъемка.

Управлять камерой можно с телефона или компьютера через VNC, а если к камере подключен монитор, то можно воспользоваться Bluetooth клавиатурой с сенсорной панелью.


клавиатура

В программе много невычищенного мусора, тем не менее для желающих публикую ее полный текст.


 
10.10.2016
Установите проигрыватель Flash

Облако тегов:
3D печать
Arduino
Raspberry Pi
Аэрофотосъемка
Байдарки
Геомеханика
История
Камеры
Макросъемка
Объективы
Освещение
Панорамы
Принадлежности
Принтеры
Программы
Сканеры
Стереосъемка
Фильтры
Фокусировка
Фотокубики
...
rss