Picamera и дистанционная съемка с живой картинкой

raspberry piZero

Статья Дистанционное управление по сети была написана в 2014 году, и описанные в ней методы позволяли организовать как процесс съемки, так и вывод изображения с камеры в реальном времени на удаленный компьютер. В 2016 году вышла статья 3 камеры на один Raspberry Pi3, в которой была представлена программа, написанная на Python 3 с использованием  Tkinter и OpenCV, которая позволяла на локальном компьютере практически полностью использовать настройки камеры, представляемые библиотекой picamera. В качестве видоискателя могла использоваться как картинка формируемая графическим процессором, так и изображение в окне графического интерфейса Tkinter. Последняя имела меньшее разрешение и частоту кадров, но позволяла исправлять на лету дисторсию с помощью opencv и, что самое главное, эта картинка была видна на удаленном рабочем столе, получаемом с помощью программ VNC (Virtual Network Computing). Исходный код можно скачать здесь.

pi3tkcv.py:
 # This Python file uses the following encoding: utf-8
import os, sys
# rwpbb.ru zero
# Python 3
import RPi.GPIO as gp
#import serial
import time
import picamera
import numpy as np
from tkinter import *
from picamera.array import PiRGBArray
from PIL import Image
from PIL import ImageTk
#from tkinter import filedialog
import cv2

#assert float(cv2.__version__.rsplit('.', 1)[0]) >= 3, 'OpenCV version 3 or newer required.'


fr=0 # номер снимка в текущем сеансе
frs=0 # номер стереоснимка в текущем сеансе
fram = 10
fcam0=295 # фокусное расстояние в пикселях
fcams=305 # фокусное расстояние в стерео режиме

# fcam0 Фокусное расстояние в пикселях. Т.е. если у нас
# рыбий глаз с фокусным расстоянием 18 мм, матрица размером 36х24 мм
# и кадр 640х480 пикселей, то фокусное расстояние будет равно 320
# угол обзора и фокусное расстояние в мм можно получить командой
# finfo=cv2.calibrationMatrixValues(K,(640,480),36,24)
z1=100
xt=0
var = 256 # длительность выдержки
flag =1 # если 0 то включен предпросмотр, блокируются другие включения
flagdp=0 # если 1 то включена коррекция
w, h = 2592, 1944
gp.setmode(gp.BCM)
gp.setup(18, gp.IN, pull_up_down = gp.PUD_UP)
gp.setup(19, gp.OUT)
gp.setup(21, gp.OUT)
gp.output(19, False)
gp.output(21, False)
# Присвоение очередного номера новым снимкам
fn=0
namef="/home/pi/fotopicam/foto1.txt"
try:
f = open(namef, 'r')
fs = f.read()
print(fs)
f.close()
except IOError:
ff = open(namef, 'w')
fs ="0"
ff.write(fs)
ff.close()

f = open(namef, 'r')
fs = f.read()
f.close()
fn=int(fs)
ff = open(namef, 'w')
fn=fn+1
ffs=str(fn)
ff.write(ffs)
ff.close()



# Предпросмотр с использованием CV, если flagdp=1, то с коррекцией
#За основу взят пример http://www.pyimagesearch.com/2016/05/23/opencv-with-tkinter/
#Для коррекции искажений использован пример https://github.com/smidm/opencv-python-fisheye-example
def sinxv1dp():
global flag, image0

if flag==0:
sels()
else:
flag=0
panelB = None
panelA = None
camera.resolution = (640, 480)
camsetcv()
rawCapture = PiRGBArray(camera, size=(640, 480))
time.sleep(0.1)
for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):
image0 = frame.array
if flagdp.get() == 1:
image0 = cv2.fisheye.undistortImage(image0, K, D=D, Knew=Knew)
image = cv2.cvtColor(image0, 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

rawCapture.truncate(0)
root.update()
if flag == 1:
break

panelA.grid_forget()
panelB = None
panelA = None
flag=1



def g19():
if CheckVar4.get() == 1:
gp.output(19, True)
else:
gp.output(19, False)
def g21():
if CheckVar5.get() == 1:
gp.output(21, True)
else:
gp.output(21, False)

# Пересчет матрицы при масштабировании
def camz(z):
global fcam, K, D, Knew, z1
z1=float(z)
cx=320-6*xt*(1-z1/100)
cy=240
fcam=100*fcam0/(z1+0)
fsc=0.85 +(100-z1)/(500-2*z1)
K = np.array([[ fcam, 0. , cx],
[ 0. , fcam, cy],
[ 0. , 0. , 1. ]])

D = np.array([0., 0., 0., 0.])

Knew = K.copy()
Knew[(0,1), (0,1)] = fsc * Knew[(0,1), (0,1)]
x=(1-z1/100)/2 +xt*(1-z1/100)/100
y=(1-z1/100)/2
w=z1/100
h=z1/100
camera.zoom = (x,y,w,h)
finfo=cv2.calibrationMatrixValues(K,(640,480),36,24)
print(finfo)
print(z1,x,y,w,h)

# Пересчет матрицы при сдвиге
def tilt (x0):
global xt,fcam, K, D, Knew
xt=int(x0)
cx=320-6*xt*(1-z1/100)
cy=240
fcam=100*fcam0/(z1+00)
fsc=0.85 +(100-z1)/(500-2*z1)
K = np.array([[ fcam, 0. , cx],
[ 0. , fcam, cy],
[ 0. , 0. , 1. ]])

D = np.array([0., 0., 0., 0.])

Knew = K.copy()
Knew[(0,1), (0,1)] = fsc * Knew[(0,1), (0,1)]
x=(1-z1/100)/2 +xt*(1-z1/100)/100
y=(1-z1/100)/2
w=z1/100
h=z1/100
camera.zoom = (x,y,w,h)

# Параметры баланса белого
def swb():
camera.awb_mode = Spinboxwb.get()
r=int(Spinboxwbr.get())/10
b=int(Spinboxwbb.get())/10
camera.awb_gains =(r,b)

# Параметры баланса белого при awb_mode = 'off'
def wbr():
r=int(Spinboxwbr.get())/10
b=int(Spinboxwbb.get())/10
camera.awb_gains =(r,b)

def wbb():
b=int(Spinboxwbb.get())/10
r=int(Spinboxwbr.get())/10
camera.awb_gains =(r,b)

def iso():
iso=int(Spinbox2.get())
camera.iso = iso
print(camera.iso)

# Параметры камеры при CV
def camsetcv():
global var
#camera.hflip = 1
camera.rotation = 0
fram=int(Spinbox1.get())
camera.framerate = fram
iso=int(Spinbox2.get())
expcomp=int(Spinbox3.get())
camera.exposure_compensation = expcomp
contr=int(Spinbox5.get())
camera.contrast = contr

camera.awb_mode = Spinboxwb.get()
r=int(Spinboxwbr.get())/10
b=int(Spinboxwbb.get())/10
camera.awb_gains =(r,b)

if CheckVar2.get() == 1:
camera.color_effects = (128,128)
else:
camera.color_effects = None
camera.drc_strength = Spinbox4.get()
camera.contrast = contr

if exp.get() == "auto":
camera.iso = 0
camera.shutter_speed = 0
camera.exposure_mode = "auto"
Spinbox2.delete(0,3)
Spinbox2.insert(0,'0')

elif exp.get() == "off":
camera.shutter_speed = var
camera.exposure_mode = "auto"
time.sleep(0.5)
camera.exposure_mode = "off"

else:
camera.iso = iso
camera.shutter_speed = var
camera.exposure_mode = "auto"
time.sleep(0.5)
var=int(camera.exposure_speed)
label.config(text= "1/" + str(int(1000000/var)))

# Параметры камеры при HDMI. При разрешении большем 1296 частота кадров ограничена 15
def camset():
if Spinbox6.get() == "2592x1944":
w, h = 2592, 1944
elif Spinbox6.get() == "1620x1232":
w, h = 1620, 1232
elif Spinbox6.get() == "1296x972":
w, h = 1296, 972
else:
w, h = 640, 480

camera.resolution = (w, h)
#camera.hflip = 1
camera.rotation = 0
global fram, var
fram=int(Spinbox1.get())
iso=int(Spinbox2.get())
expcomp=int(Spinbox3.get())
contr=int(Spinbox5.get())
camera.framerate = fram
camera.preview_fullscreen=CheckVar1.get()
#camera.preview_window = (0,0,1600,1200)
camera.preview_window = (0,0,1024,768)
camera.awb_mode = Spinboxwb.get()
r=int(Spinboxwbr.get())/10
b=int(Spinboxwbb.get())/10
camera.awb_gains =(r,b)
if CheckVar2.get() == 1:
camera.color_effects = (128,128)
else:
camera.color_effects = None
camera.drc_strength = Spinbox4.get()
camera.exposure_compensation = expcomp
#camera.brightness = 40
camera.contrast = contr
if exp.get() == "auto":
camera.iso = 0
camera.shutter_speed = 0
camera.exposure_mode = "auto"
Spinbox2.delete(0,3)
Spinbox2.insert(0,'0')

elif exp.get() == "off":
camera.shutter_speed = var
camera.exposure_mode = "auto"
time.sleep(0.5)
camera.exposure_mode = "off"

else:
camera.iso = iso
camera.shutter_speed = var
camera.exposure_mode = "auto"
time.sleep(0.5)
var=int(camera.exposure_speed)
label.config(text= "1/" + str(int(1000000/var)))

# Выдержка
def selp():
global var
var=int(var*2)
camera.shutter_speed = var
# Проверяем, что частота кадров позволяет увеличить выдержку
time.sleep(0.5)
var=int(camera.exposure_speed)
label.config(text= "1/" + str(int(1000000/var)))

def seln():
global var
var=int(var/2)
label.config(text= "1/" + str(int(1000000/var)))
camera.shutter_speed = var

# Снимок
def selcam1():
global flag,fr

if flag==0:
sels()
else:
fr=fr+1
#camera.crop = (0,0,1,1)
camset()
camera.exif_tags['IFD0.Artist'] = "RWPBB"
camera.exif_tags['EXIF.FocalLength'] = '50'
camera.capture("/home/pi/fotopicam/"+ffs+"pi%03d.jpg" % fr)

# Снимок 2
def selcam2():
global flag,fr

if flag==0:
sels()
else:
fr=fr+1
if Spinbox6.get() == "2592x1944":
w, h = 2592, 1944
elif Spinbox6.get() == "1620x1232":
w, h = 1620, 1232
elif Spinbox6.get() == "1296x972":
w, h = 1296, 972
else:
w, h = 640, 480
camsetcv()
camera.resolution = (w, h)
rawCapture = PiRGBArray(camera, size=(w, h))
time.sleep(0.1)
camera.capture(rawCapture, format="bgr")
image = rawCapture.array
if flagdp.get() == 1:
fcam1=w*fcam0/640
cx=w/2-6*xt*(1-z1/100)
cy=h/2
fcam=100*fcam1/(z1+00)
fsc=0.85 +(100-z1)/(500-2*z1)
K = np.array([[ fcam, 0. , cx],
[ 0. , fcam, cy],
[ 0. , 0. , 1. ]])

D = np.array([0., 0., 0., 0.])

Knew = K.copy()
Knew[(0,1), (0,1)] = fsc * Knew[(0,1), (0,1)]
image = cv2.fisheye.undistortImage(image, K, D=D, Knew=Knew)
cv2.imwrite("/home/pi/fotopicam/"+ffs+"pifcv%03d.jpg" % fr, image)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = Image.fromarray(image)
image = ImageTk.PhotoImage(image)
flag = 0
panelA = None
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
rawCapture.truncate(0)
root.update()

time.sleep(10)
panelA.grid_forget()
panelB = None
panelA = None
flag=1

# Компенсация экспозиции
def expc():
expcomp=int(Spinbox3.get())
camera.exposure_compensation = expcomp

# Контраст
def contr():
contr=int(Spinbox5.get())
camera.contrast = contr

# Индикация текущей выдержки в автоматическом режиме
def info():
label.config(text= "1/" + str(int(1000000/camera.exposure_speed)))
print(camera.iso)

# Вычисление и индикация коэффициента локального контраста
def infof():
gray = cv2.cvtColor(image0, cv2.COLOR_BGR2GRAY)
fm = cv2.Laplacian(gray, cv2.CV_64F).var()
labelf.config(text= str(int(fm)))



# Остановка проедпросмотра
def sels():
global flag
flag = 1
if camera.preview:
camera.stop_preview()

# Установка параметров и включение предпросмотра через HDMI
def preview():
global flag
if flag==0:
sels()
else:
camset()
#camera.crop = (0,0,1,1)
camera.start_preview()
flag = 0
def piexit():
camera.close()
gp.cleanup()
exit()
# Реакция на кнопку 18
def finFunction(can):

sels()
gp.output(21, True)
time.sleep(0.2)
gp.output(21, False)
time.sleep(3.2)
selcam2()


gp.add_event_detect(18, gp.FALLING, callback=finFunction, bouncetime=1000)
with picamera.PiCamera() as camera:
root = Tk()
root.title("PiCamera")
root.wm_geometry("1280x760+0+0")
panelA = None
panelB = None
exp = Spinbox(values =('auto','P','off'),width =4, font=('monospace',16))
exp.grid(row=0,column=2, sticky='w')
Spinbox6 = Spinbox(values =('1296x972','2592x1944','1620x1232','640x480'),width =9, font=('monospace',16))
Spinbox6.grid(row=0,column=2, sticky='e', columnspan=2)
Spinboxwb = Spinbox(values =('auto','sunlight','cloudy','tungsten','shade','off'),width =8, font=('monospace',14), command=swb)
Spinboxwb.grid(row=2,column=2,columnspan=2)
Spinboxwbr = Spinbox(from_=5, to=29,width =2, font=('monospace',16), command=wbr)
Spinboxwbr.grid(row=2,column=2, sticky='w')
Spinboxwbr.delete(0)
Spinboxwbr.insert(0,10)
Spinboxwbb = Spinbox(from_=5, to=29,width =2, font=('monospace',16), command=wbb)
Spinboxwbb.grid(row=2,column=3, sticky='e')
Spinboxwbb.delete(0)
Spinboxwbb.insert(0,10)

CheckVar1 = IntVar()
CheckVar1.set(0)
C1 = Checkbutton(text = "full", variable = CheckVar1, \
onvalue = 1, offvalue = 0, height=1, \
width =4, font=('monospace',16))
C1.grid(row=3,column=2,columnspan=2)

CheckVar2 = IntVar()
CheckVar2.set(0)
C2 = Checkbutton(text = "B&W", variable = CheckVar2, \
onvalue = 1, offvalue = 0, height=1, \
width = 3, font=('monospace',16))
C2.grid(row=3,column=2, sticky='w')

flagdp = IntVar()
flagdp.set(0)
C3 = Checkbutton(text = "DP", variable = flagdp, \
onvalue = 1, offvalue = 0, height=1, \
width =2, font=('monospace',16))
C3.grid(row=3,column=3, sticky='e')

button = Button(text="shutter_speed", command=info, font=('monospace',16))
button.grid(row=4,column=2,columnspan=2)

buttonp = Button(text="+", command=selp, font=('monospace',16), width = 2)
buttonp.grid(row=5,column=2, sticky='w',columnspan=2)

buttonn = Button(text="-", command=seln, font=('monospace',16), width = 2)
buttonn.grid(row=5,column=2, sticky='e',columnspan=2)

label = Label(text= "1/" + str(var), font=('monospace',16))
label.grid(row=5,column=2,columnspan=2)

button = Button(text="preview", command=preview, font=('monospace',16), height=1, width = 6)
button.grid(row=8,column=2, sticky='w')
buttonpstop = Button(text="Stop", command=sels, font=('monospace',16), height=1, width = 6)
buttonpstop.grid(row=8,column=3, sticky='e')
buttonpstopl = Button(text="Stop", command=sels, font=('monospace',16), height=25, width = 78)
buttonpstopl.grid(row=1,column=0, sticky='w', rowspan=23)
scalezoom = Scale(root, from_=20, to=100, orient=HORIZONTAL, resolution=5, command=camz)
scalezoom.grid(row=9,column=2, columnspan=2, sticky='e')
scaletilt = Scale(root, from_=-50, to=50, orient=HORIZONTAL, resolution=1, command=tilt)
scaletilt.grid(row=10,column=2, columnspan=2)
scalezoom.set(100)
labelzoom = Label(text= "Zoom", font=('monospace',16))
labelzoom.grid(row=9,column=2,sticky='w')
labelzooml = Label(text= "L", font=('monospace',16))
labelzooml.grid(row=10,column=2,sticky='w')
labelzoomr = Label(text= "R", font=('monospace',16))
labelzoomr.grid(row=10,column=3,sticky='e')


buttoncv1 = Button(text="CV", command=sinxv1dp, font=('monospace',16), height=1, width = 2)
buttoncv1.grid(row=13,column=2, sticky='w')


Spinbox1 = Spinbox(from_=3, to=32,width =3, font=('monospace',16))
Spinbox1.grid(row=15,column=3)
Spinbox1.delete(0)
Spinbox1.insert(0,12)
labelframe = Label( root, text="Frame", font=('monospace',16))
labelframe.grid(row=15,column=2)

Spinbox2 = Spinbox(values =('0','100','200','400','800'),width =4, font=('monospace',16), command=iso)
Spinbox2.grid(row=16,column=3)
labeliso = Label( root, text="ISO", font=('monospace',16))
labeliso.grid(row=16,column=2)

Spinbox3 = Spinbox(from_=-25, to=25,width =4, font=('monospace',16), command=expc)
Spinbox3.grid(row=17,column=3)
Spinbox3.delete(0,"end")
Spinbox3.insert(0, 0)
labelexp = Label( root, text="EXP", font=('monospace',16))
labelexp.grid(row=17,column=2)

Spinbox4 = Spinbox(values =('high','low','off'),width =4, font=('monospace',16))
Spinbox4.grid(row=19,column=3)
labeldrc = Label( root, text="DRC", font=('monospace',16))
labeldrc.grid(row=19,column=2)

Spinbox5 = Spinbox(from_=-25, to=25,width =4, font=('monospace',16), command=contr)
Spinbox5.grid(row=18,column=3)
Spinbox5.delete(0,"end")
Spinbox5.insert(0,-1)
labelcontr = Label( root, text="Contr", font=('monospace',16))
labelcontr.grid(row=18,column=2)
CheckVar4 = IntVar()
CheckVar4.set(0)
C4 = Checkbutton(text = "19",variable = CheckVar4, command=g19,\
onvalue = 1, offvalue = 0, height=1, \
width =2, font=('monospace',16))
C4.grid(row=20,column=3, sticky='e')
CheckVar5 = IntVar()
CheckVar5.set(0)
C5 = Checkbutton(text = "21", variable = CheckVar5, command=g21,\
onvalue = 1, offvalue = 0, height=1, \
width =2, font=('monospace',16))
C5.grid(row=20,column=2, sticky='w')

labelf = Label(text= "0", font=('monospace',16))
labelf.grid(row=22,column=2,columnspan=2)
buttoncani = Button(text="LC", command=infof, font=('monospace',16),width =3)
buttoncani.grid(row=22,column=2, sticky='w')
buttoncanf4 = Button(text="exit", command=piexit, font=('monospace',16),width =3)
buttoncanf4.grid(row=22,column=3, sticky='e')


buttonpcam = Button(text="F1", command=selcam1, font=('monospace',16), height=1, width = 2)
buttonpcam.grid(row=13,column=2, columnspan=2)
buttonpcam2 = Button(text="F2", command=selcam2, font=('monospace',16), height=1, width = 2)
buttonpcam2.grid(row=13,column=3, sticky='e')
root.mainloop()


camera.close()
gp.cleanup()



Tkinter

В последующие четыре года я собрал довольно много камер и все стоящие передо мной задачи их настройки прекрасно этой комбинацией программ решались. Причем использование удаленного рабочего стола  позволяло не только запускать, но и налету изменять программу и просматривать полученные фотографии с помощью продвинутых программ просмотра отображающих не только изображение, но и сопутствующие параметры необходимые для детального анализа.  На компьютере работа была практически так же комфортна, как и работа с компьютером Raspberry Pi с подключенным монитором и клавиатурой, на планшете или телефоне нажать некоторые кнопки было сложнее, но вполне возможно. Идея использовать для управления Веб-интерфейс и таким образом обойтись без программ удаленного рабочего стола и, возможно сэкономить ресурсы возникла исключительно из-за возникшего свободного времени, которое надо было чем-то занять.

picamerapicamerapicamera

В начале я полагал, что задача давно решена, и если поискать в сети, то можно найти почти готовое устраивающее меня решение. Однако оказалось, что хотя задача была сформулирована и за нее брались и обсуждали, однако готовое нужное мне решение либо не было найдено за ненадобностью, либо, что более вероятно, не опубликовано в виде статей, доступных поисковикам, а лежит в каких то недокументированных репозиториях.  Таким образом, передо мной встала задача создать собственную программу, объединяющую потоковое видео с кнопками и ползунками управления. В результате я написал две программы, использующие Веб-интерфейс на базе   socketserver и flask, и сравнил их со связкой  программы  на Tkinter c VNC при запуске на компьютере Pi3+ и ZeroW. 

Примеров, реализующих либо только видео, либо только управление, достаточно много, но задача несколько осложнялась тем, что мне хотелось отображать изображение, которое использовало те же настройки яркости, контраста, чувствительности , выдержки,  баланса белого, что и  изображение, которое будет сделано после нажатия спусковой кнопки. Т.е. видео поток я должен был формировать из изображений, полученных с помощью функций из библиотеки picamera.  Первое решение, которое приходит в голову, это взять пример из picamera (4.10.Web streaming), использующий socketserver,  и добавить к нему кнопки и opencv для исправления дисторсии вызываемой объективами типа рыбий глаз. Получается и все работает , но использование только GET  сильно обедняет возможности создать красивый интерфейс. И хотя можно создать программу, изменяющую все настройки, она будет громоздкой и не очень удобной.   Ниже приведена программа с 5 кнопками, позволяющая делать снимок, менять компенсацию экспозиции и включать и выключать коррекцию дисторсии при предпросмотре. В отличие от программы 2016 года, где была кнопка съемки с исправлением дисторсии и с полным разрешением, я сегодня считаю, что при необходимости эти преобразования надо делать на мощном настольном компьютере, ресурсы  малины на это не тратить. Исходный текст программы для скачивания здесь.

streamer8z.py:
import picamera
from datetime import datetime
import io
import logging
import socketserver
from threading import Condition
from http import server
from time import sleep
from PIL import ImageFont, ImageDraw, Image
import cv2
import traceback
from picamera.array import PiRGBArray
import time
import numpy as np
import datetime as dt

fcam0=250 # фокусное расстояние в пикселях
z1=100
xt=0
flag=0
ev=0 # компенсация экспозиции
def camz(z):
global fcam, K, D, Knew, z1
z1=float(z)
cx=320-6*xt*(1-z1/100)
cy=240
fcam=100*fcam0/(z1+0)
fsc=0.85 +(100-z1)/(500-2*z1)
K = np.array([[ fcam, 0. , cx],
[ 0. , fcam, cy],
[ 0. , 0. , 1. ]])

D = np.array([0., 0., 0., 0.])

Knew = K.copy()
Knew[(0,1), (0,1)] = fsc * Knew[(0,1), (0,1)]
#x=(1-z1/100)/2 +xt*(1-z1/100)/100
#y=(1-z1/100)/2
#w=z1/100
#h=z1/100
#self.camera.zoom = (x,y,w,h)
#finfo=cv2.calibrationMatrixValues(K,(640,480),36,24)
#print(finfo)
#print(z1,x,y,w,h)


PAGE="""\
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Raspberry Pi</title>
<style>
body {
background: #999999;
margin: 10px;
font-family: Arial, sans-serif;
font-size: 40px;
text-align: center;
}
.button {
width: 100px;
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 10px;
text-align: center;
margin: 10px 10px;
border-radius: 15px;
font-size: 20px;
opacity: 1;
transition: 0.5s;
}
.button:hover {opacity: 0.6}
</style>
</head>
<body>
<center><h1>Raspberry Pi</h1>
<img src="stream.mjpg" width="640" height="480">
<div class="button"><a href="/capture/">Снимок</a></div> </center>
<form>
<p><button name="hid0" type="submit" class="button"> -- </button>
<button name="hid1" type="submit" class="button"> Выкл </button>
<button name="hid" type="submit" class="button"> Вкл </button>
<button name="hid2" type="submit" class="button"> ++ </button></p>
</form>

</body>
</html>
"""
Redirection="""<html><head><meta http-equiv="refresh" content="0;URL=/index.html"></head></html>"""

class StreamingOutput(object):
def __init__(self):
self.frame = None
self.buffer = io.BytesIO()
self.condition = Condition()
self.camera=picamera.PiCamera(resolution='640x480', framerate=12)

def write(self, buf):
if buf.startswith(b'\xff\xd8'):
# New frame, copy the existing buffer's content and notify all
# clients it's available
self.buffer.truncate()
with self.condition:
self.frame = self.buffer.getvalue()
self.condition.notify_all()
self.buffer.seek(0)
return self.buffer.write(buf)
def shot_camera(self):
self.camera.wait_recording(0.5)
self.camera.stop_recording()
sleep(0.5)
self.camera.resolution=(2592, 1944)
#self.camera.start_preview()
sleep(0.5)
self.camera.capture("/home/pi/" + datetime.now().strftime("%d-%b-%Y.(%H_%M_%S_%f)") + ".jpg")
self.camera.resolution=(640,480)
self.camera.start_recording(self, format='mjpeg')
def shot_camera3(self):
global flag
flag=1
def shot_camera2(self):
global flag
flag=0
def shot_camera0(self):
global ev
ev=ev-2
self.camera.exposure_compensation =ev
def shot_camera4(self):
global ev
ev=ev+2
self.camera.exposure_compensation =ev
def start_recording(self):
#Uncomment the next line to change your Pi's Camera rotation (in degrees)
#camera.rotation = 90
self.camera.start_recording(self, format='mjpeg')
def stop_camera(self):
self.camera.stop_recording()



class StreamingHandler(server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
self.send_response(301)
self.send_header('Location', '/index.html')
self.end_headers()
elif self.path == '/index.html':
content = PAGE.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
elif self.path == '/stream.mjpg':
self.send_response(200)
self.send_header('Age', 0)
self.send_header('Cache-Control', 'no-cache, private')
self.send_header('Pragma', 'no-cache')
self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
self.end_headers()
try:
while True:
with output.condition:
output.condition.wait()
frame = output.frame
if flag == 1:
# Convert to PIL Image
npframe = np.fromstring(frame, dtype=np.uint8)
pil_frame = cv2.imdecode(npframe,1)

pil_frame = cv2.fisheye.undistortImage(pil_frame, K, D=D, Knew=Knew)
cv2_im_rgb = cv2.cvtColor(pil_frame, cv2.COLOR_BGR2RGB)
pil_im = Image.fromarray(cv2_im_rgb)

buf= io.BytesIO()
pil_im.save(buf, format= 'JPEG')
frame = buf.getvalue()

self.wfile.write(b'--FRAME\r\n')
self.send_header('Content-Type', 'image/jpeg')
self.send_header('Content-Length', len(frame))
self.end_headers()
self.wfile.write(frame)
self.wfile.write(b'\r\n')
except Exception as e:
logging.warning(
'Removed streaming client %s: %s',
self.client_address, str(e))
elif self.path == '/capture/':
output.shot_camera()
content = Redirection.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
elif self.path == '/index.html?hid1=':
output.shot_camera2()
content = Redirection.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
elif self.path == '/index.html?hid=':
output.shot_camera3()
content = Redirection.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
elif self.path == '/index.html?hid0=':
output.shot_camera0()
content = Redirection.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
elif self.path == '/index.html?hid2=':
output.shot_camera4()
content = Redirection.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
else:
self.send_error(404)
self.end_headers()

class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
allow_reuse_address = True
daemon_threads = True
camz(100)
output = StreamingOutput()
output.start_recording()
try:
address = ('', 8000)
serveur = StreamingServer(address, StreamingHandler)
serveur.serve_forever()
finally:
output.stop_camera()

streaming

Другой, более продвинутый вариант - использовать flask. За основу взят проект 2014 года от  Miguel Grinberg Video Streaming with Flask. Поскольку наши задачи существенно различались, то из его проекта осталась в неизменном виде программа  base_camera.py, а остальное сильно изменено. Оставлена поддержка только камер Raspberry Pi и добавлена возможность менять практически все доступные настройки камеры. В результате в проекте осталось всего 3 файла: app9.py, base_camera.py и index.html. Архив с проектом можно скачать здесь.

app9.py:
#!/usr/bin/env python
from datetime import datetime
from importlib import import_module
import os, sys
from flask import Flask, render_template, Response, request,jsonify
import io
import time
import picamera
from base_camera import BaseCamera
import numpy as np
from PIL import ImageFont, ImageDraw, Image
import cv2
from picamera.array import PiRGBArray
rot=0
z=85
rbc="checked='checked'"
rbc6=""
dpp="DP:ON"
prr="Preview:ON"
mdd="Mode:M"
option="auto"
sh1="off"
iso=0
con=0
bri=50
sat=0
sharp=0
wb8=15 # баланс белого красный
wb9=15 # синий
sl1=0 # Значение для slider1
sl5=0
sl6=0
sl7=0
sl10=0
sl11=0
sl13=50
wh1="3280x2464"
fcam0=250 # фокусное расстояние в пикселях для матрицы 640х480 пикселей
#z1=100
#xt=0
flag=1
flag1=1 # 0 выйти из цикла трансляции видео
fr=12 # частота кадров
camera=picamera.PiCamera()
camera.exposure_mode = "auto"
camera.framerate = fr
ev=int(sl1)-25
camera.exposure_compensation =ev
def infof():
gray = cv2.cvtColor(image0, cv2.COLOR_BGR2GRAY)
fm = cv2.Laplacian(gray, cv2.CV_64F).var()
text= str(int(fm))
return text
def camz(z):
global fcam, K, D, Knew, z1
z1=100
cx=320
cy=240
fcam=100*fcam0/z1
fsc=z/100
K = np.array([[ fcam, 0. , cx],
[ 0. , fcam, cy],
[ 0. , 0. , 1. ]])

D = np.array([0., 0., 0., 0.])

Knew = K.copy()
Knew[(0,1), (0,1)] = fsc * Knew[(0,1), (0,1)]
class Camera(BaseCamera):
camz(z)
#camera.resolution=(640,480)
@staticmethod
def frames():
global image0
if flag1 == 1:
if rot == 0 or rot == 180:
w, h = 640, 480
else :
w, h = 480, 640
camera.resolution=(w, h)
camera.rotation = rot
# let camera warm up
time.sleep(1)
rawCapture = PiRGBArray(camera, size=(w, h))

for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):

image0 = frame.array
if flag == 2:
image0 = cv2.fisheye.undistortImage(image0, K, D=D, Knew=Knew)
image = cv2.cvtColor(image0, cv2.COLOR_BGR2RGB)
pil_im = Image.fromarray(image)
stream= io.BytesIO()
pil_im.save(stream, format= 'JPEG')

# return current frame
stream.seek(0)
yield stream.read()

# reset stream for next frame
rawCapture.truncate(0)
if flag1 == 0:
break

app = Flask(__name__)

@app.route('/')
def index():
global spa
#"""Video streaming home page."""
var=int(camera.exposure_speed)
spa=str(int(1000000/var))
return render_template('index.html',val15=z, opt3=wh1, opt=option, opt2=sh1, rb6=rbc6, val14=rot,val12=fcam0, val11=sl11, val10=sl10, val13=sl13, val=sl1, val5=sl5, val6=sl6, val7=sl7, val7a=spa, dp=dpp, pr=prr,md=mdd, val8=wb8, val9=wb9)

@app.route('/process_data/', methods=['POST'])
def doit():
global flag, flag1, dpp, prr, mdd, spa, sl5, sl6
index = request.form['index']
# ... обработать данные ...
var=int(camera.exposure_speed)
if index == "1":
if flag <2:
flag =2
dpp="DP:OFF"
else:
flag=1
dpp="DP:ON"
if index == "2":
if prr == "Preview:ON":
flag1=0
time.sleep(0.2)
camera.resolution=(1920, 1080)
camera.start_preview()
prr="Preview:OFF"
else:
camera.stop_preview()
flag1=1
prr="Preview:ON"
if index == "3":
if mdd == "Mode:M":
camera.shutter_speed = var
camera.exposure_mode = "auto"
time.sleep(0.5)
camera.exposure_mode = "off"
mdd="Mode:A"
sl6=str(int(var/1000))
else:
camera.iso = 0
camera.shutter_speed = 0
camera.exposure_mode = "auto"
mdd="Mode:M"
sl5="0"
sl6="0"
if index == "4":
if wh1 =="3280x2464":
w, h =3280, 2464
elif wh1 =="1920x1080":
w, h =1920, 1080
elif wh1 =="1640x1232":
w, h =1640, 1232
elif wh1 =="2592x1944":
w, h =2592, 1944
elif wh1 =="1296x972":
w, h =1296, 972
else:
w,h=640,480
flag1=0
time.sleep(0.2)
camera.resolution=(w, h)
camera.capture("/home/pi/" + datetime.now().strftime("%d-%b-%Y.(%H_%M_%S_%f)") + ".jpg")
#camera.resolution=(640,480)
flag1=1
spa=str(int(1000000/var))
return render_template('index.html',val15=z, opt3=wh1, opt=option, opt2=sh1, rb6=rbc6, val14=rot,val12=fcam0, val11=sl11, val10=sl10, val13=sl13, val=sl1, val5=sl5, val6=sl6, val7=sl7, val7a=spa, dp=dpp, pr=prr,md=mdd, val8=wb8, val9=wb9)

@app.route('/process_data2/', methods=['POST'])
def slid():
global sl1, sl5, sl6, sl7, sl10, sl11, fcam0, sl13, spa,z
sl1 = request.form['slider1']
ev=int(sl1)
camera.exposure_compensation =ev
sl5 = request.form['slider5']
iso=int(sl5)*100
camera.iso = iso
sl6 = request.form['slider6']
sp=int(sl6)*1000
camera.shutter_speed =sp
sl7 = request.form['slider7']
con=int(sl7)
camera.contrast = con
sl13 = request.form['slider13']
bri=int(sl13)
camera.brightness = bri
sl10 = request.form['slider10']
sat=int(sl10)
camera.saturation = sat
sl11 = request.form['slider11']
sharp=int(sl11)
camera.sharpness = sharp
infsh=infof()
print(infsh)
sl12 = request.form['slider12']
fcam0=int(sl12)
sl15 = request.form['slider15']
z=int(sl15)
camz(z)
time.sleep(0.1)
var=int(camera.exposure_speed)
spa=str(int(1000000/var))
return render_template('index.html',val15=z, val11a=infsh, opt3=wh1, opt=option, opt2=sh1, rb6=rbc6, val14=rot,val12=fcam0, val11=sl11, val10=sl10, val13=sl13, val=sl1, val5=sl5, val6=sl6, val7=sl7, val7a=spa, dp=dpp, pr=prr,md=mdd, val8=wb8, val9=wb9)
@app.route('/process_data14/', methods=['POST'])
def rotcam():
global rot, flag1
sl14 = request.form['slider14']
rot=int(sl14)
flag1=0
time.sleep(0.2)
flag1=1
return render_template('index.html',val15=z, opt3=wh1, opt=option, opt2=sh1, rb6=rbc6, val14=rot,val12=fcam0, val11=sl11, val10=sl10, val13=sl13, val=sl1, val5=sl5, val6=sl6, val7=sl7, val7a=spa, dp=dpp, pr=prr,md=mdd, val8=wb8, val9=wb9)
def gen(camera):
"""Video streaming generator function."""
while True:
frame = camera.get_frame()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
@app.route('/process_data8/', methods=['POST'])
def rad():
global spa, option,wb8, wb9, rbc6
option = request.form['options']
wb8 = request.form['slider8']
wb9 = request.form['slider9']
# ... обработать данные ...
camera.awb_mode = option
if option =="off":
r=int(wb8)/10
b=int(wb9)/10
camera.awb_gains =(r,b)
rbc6=rbc
else:
rbc6=""

print(option,wb8,wb9)


return render_template('index.html',opt3=wh1, opt=option, opt2=sh1, rb6=rbc6, val14=rot,val12=fcam0, val11=sl11, val10=sl10, val13=sl13, val=sl1, val5=sl5, val6=sl6, val7=sl7, val7a=spa, dp=dpp, pr=prr,md=mdd, val8=wb8, val9=wb9)
@app.route('/process_data3/', methods=['POST'])
def sh():
global sh1
sh1 = request.form['opt']
# ... обработать данные ...
camera.drc_strength = sh1
print(sh1)

return render_template('index.html',opt3=wh1, opt=option, opt2=sh1, rb6=rbc6, val14=rot,val12=fcam0, val11=sl11, val10=sl10, val13=sl13, val=sl1, val5=sl5, val6=sl6, val7=sl7, val7a=spa, dp=dpp, pr=prr,md=mdd, val8=wb8, val9=wb9)
@app.route('/process_data5/', methods=['POST'])
def wh():
global wh1
wh1 = request.form['opti']

print(wh1)

return render_template('index.html',opt3=wh1, opt=option, opt2=sh1, rb6=rbc6, val14=rot,val12=fcam0, val11=sl11, val10=sl10, val13=sl13, val=sl1, val5=sl5, val6=sl6, val7=sl7, val7a=spa, dp=dpp, pr=prr,md=mdd, val8=wb8, val9=wb9)
@app.route('/video_feed')
def video_feed():
"""Video streaming route. Put this in the src attribute of an img tag."""
return Response(gen(Camera()),
mimetype='multipart/x-mixed-replace; boundary=frame')


if __name__ == '__main__':
app.run(host='0.0.0.0', threaded=True)


index.html:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>PiCamera</title>

<style>
.slidecontainer {
width: 640px;
}
.slidecontainer1 {
width: 630px;
}
.slider {
-webkit-appearance: none;
width: 95%;
height: 15px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
}

.slider:hover {
opacity: 1;
}

.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 30px;
background: #4CAF50;
cursor: pointer;
}

.slider::-moz-range-thumb {
width: 20px;
height: 30px;
background: #4CAF50;
cursor: pointer;
}
.slider1::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 10px;
height: 20px;
background: #5050AF;
cursor: pointer;
}

.slider1::-moz-range-thumb {
width: 10px;
height: 20px;
background: #5050AF;
cursor: pointer;
}
.slider1 {
-webkit-appearance: none;
width: 45%;
height: 10px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
}
.slider1:hover {
opacity: 1;
}
.button {
width: 120px;
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 5px;
text-align: center;
margin: 2px 5px;
border-radius: 15px;
font-size: 18px;
opacity: 0.7;
transition: 0.5s;
}
.button:hover {opacity: 0.9}
</style>
</head>
<body>
<table width="640" border="0" align="center">
<tr>
<td>
<center>
<img src="{{ url_for('video_feed') }}">
<form action="/process_data/" method="POST">
<button name="index" value="1" type="submit" class="button"> {{dp}} </button>
<button name="index" value="2" type="submit" class="button"> {{pr}} </button>
<button name="index" value="3" type="submit" class="button"> {{md}} </button>
<button name="index" value="4" type="submit" class="button"> Снимок </button>
</form>

<form action="/process_data2/" method="POST">
<div class="slidecontainer">
<table width="640" border="0" align="center">
<tr>
<td><p align="left">
Компенсация экспозиции: <span id="demo"></span></p></td><td><p align="right"> <input type="submit" class="button" value="Enter"></p></td></tr></table>
<input type="range" min="-25" max="25" value={{val}} class="slider" id="myRange" name="slider1">

<p align="left">
ISO: <span id="demo5"></span>00</p>
<input type="range" min="0" max="8" value={{val5}} class="slider" id="myRange5" name="slider5">

<p align="left">
Выдержка: <span id="demo6"></span>мс 1/{{val7a}}с</p>
<input type="range" min="0" max="100" value={{val6}} class="slider" id="myRange6" name="slider6">

<p align="left">
Контраст: <span id="demo7"></span></p>
<input type="range" min="-100" max="100" value={{val7}} class="slider" id="myRange7" name="slider7">

<p align="left">
Яркость: <span id="demo13"></span></p>
<input type="range" min="0" max="100" value={{val13}} class="slider" id="myRange13" name="slider13">

<p align="left">
Насыщенность: <span id="demo10"></span></p>
<input type="range" min="-100" max="100" value={{val10}} class="slider" id="myRange10" name="slider10">

<p align="left">
Резкость: <span id="demo11"></span>({{val11a}})</p>
<input type="range" min="-100" max="100" value={{val11}} class="slider" id="myRange11" name="slider11">

<p align="left">
Фокусное расстояние: <span id="demo12"></span></p>
<input type="range" min="100" max="500" value={{val12}} class="slider" id="myRange12" name="slider12">
<p align="left">
Масштаб: <span id="demo15"></span></p>
<input type="range" min="0" max="100" value={{val15}} class="slider" id="myRange15" name="slider15">

</div>
</form>
<form action="/process_data14/" method="POST">
<div class="slidecontainer">
<table width="640" border="0" align="center"><tr>
<td><p align="left">Вращение: <span id="demo14"></span></p></td>
<td><p align="right"> <input type="submit" class="button" value="Enter"></p></td></tr></table>
<input type="range" min="0" max="270" step="90" value={{val14}} class="slider" id="myRange14" name="slider14">
</div>
</form>
<form name="myForm" action="/process_data8/" method="POST" >


<input type="radio" name="options" id="option1" value="auto" {{rb1}}> auto </input>
<input type="radio" name="options" id="option2" value="sunlight" {{rb2}}> sunlight </input>
<input type="radio" name="options" id="option3" value="cloudy" {{rb3}}> cloudy </input>
<input type="radio" name="options" id="option3" value="tungsten" {{rb4}}> tungsten </input>
<input type="radio" name="options" id="option3" value="shade" {{rb5}}> shade </input>
<input type="radio" name="options" id="option3" value="off" {{rb6}}> off </input><br>
<div class="slidecontainer1">
<input type="range" min="1" max="25" value={{val8}} class="slider1" id="myRange8" name="slider8">
<input type="range" min="1" max="25" value={{val9}} class="slider1" id="myRange9" name="slider9"><br>
Красный:<span id="demo8"></span><input type=submit class="button" value={{opt}}>Синий:<span id="demo9"></span>
</div>
</form>
<form name="myForm2" action="/process_data3/" method="POST" >
<input type="radio" name="opt" id="opt1" value="off" {{cb1}}> off </input>
<input type="radio" name="opt" id="opt2" value="low" {{cb2}}> low </input>
<input type="radio" name="opt" id="opt3" value="medium" {{cb3}}> medium </input>
<input type="radio" name="opt" id="opt4" value="high" {{cb4}}> high </input>
<input type=submit class="button" value={{opt2}}>
</form>
<form name="myForm3" action="/process_data5/" method="POST" >
<input type="radio" name="opti" id="opti1" value="3280x2464" {{wh1}}> 3280x2464 </input>
<input type="radio" name="opti" id="opti2" value="2592x1944" {{wh2}}> 2592x1944</input>
<input type="radio" name="opti" id="opti3" value="1920x1080" {{wh3}}> 1920x1080 </input>
<input type="radio" name="opti" id="opti4" value="1640x1232" {{wh4}}> 1640x1232 </input><br>
<input type="radio" name="opti" id="opti5" value="1296x972" {{wh5}}> 1296x972 </input>
<input type="radio" name="opti" id="opti6" value="640x480" {{wh7}}> 640x480 </input>
<input type=submit class="button" value={{opt3}}>
</form>
</center>

<script>
var slider = document.getElementById("myRange");
var output = document.getElementById("demo");
output.innerHTML = slider.value;

slider.oninput = function() {
output.innerHTML = this.value;
}
</script>
<script>
var slider5 = document.getElementById("myRange5");
var output5 = document.getElementById("demo5");
output5.innerHTML = slider5.value;

slider5.oninput = function() {
output5.innerHTML = this.value;
}
</script>
<script>
var slider6 = document.getElementById("myRange6");
var output6 = document.getElementById("demo6");
output6.innerHTML = slider6.value;

slider6.oninput = function() {
output6.innerHTML = this.value;
}
</script>
<script>
var slider7 = document.getElementById("myRange7");
var output7 = document.getElementById("demo7");
output7.innerHTML = slider7.value;

slider7.oninput = function() {
output7.innerHTML = this.value;
}
</script>
<script>
var slider8 = document.getElementById("myRange8");
var output8 = document.getElementById("demo8");
output8.innerHTML = slider8.value;

slider8.oninput = function() {
output8.innerHTML = this.value;
}
</script>
<script>
var slider9 = document.getElementById("myRange9");
var output9 = document.getElementById("demo9");
output9.innerHTML = slider9.value;

slider9.oninput = function() {
output9.innerHTML = this.value;
}
</script>

<script>
var slider10 = document.getElementById("myRange10");
var output10 = document.getElementById("demo10");
output10.innerHTML = slider10.value;

slider10.oninput = function() {
output10.innerHTML = this.value;
}
</script>

<script>
var slider11 = document.getElementById("myRange11");
var output11 = document.getElementById("demo11");
output11.innerHTML = slider11.value;

slider11.oninput = function() {
output11.innerHTML = this.value;
}
</script>

<script>
var slider12 = document.getElementById("myRange12");
var output12 = document.getElementById("demo12");
output12.innerHTML = slider12.value;

slider12.oninput = function() {
output12.innerHTML = this.value;
}
</script>

<script>
var slider13 = document.getElementById("myRange13");
var output13 = document.getElementById("demo13");
output13.innerHTML = slider13.value;

slider13.oninput = function() {
output13.innerHTML = this.value;
}
</script>

<script>
var slider14 = document.getElementById("myRange14");
var output14 = document.getElementById("demo14");
output14.innerHTML = slider14.value;

slider14.oninput = function() {
output14.innerHTML = this.value;
}
</script>
<script>
var slider15 = document.getElementById("myRange15");
var output15 = document.getElementById("demo15");
output15.innerHTML = slider15.value;

slider15.oninput = function() {
output15.innerHTML = this.value;
}
</script>
</td>
</tr>
</table>

</body>
</html>

Ниже снимок экрана телефона при съемке в ИК диапазоне камерой на базе компьютера ZeroW.

flask

Первые три кнопки верхнего ряда меняют название при нажатии. Отображется название режима, который включится при нажатии на кнопку. DP ON/OFF включает и выключает коррекцию дисторсии. Preview ON/OFF включает и выключает предпросмотр силами GPU на мониторе, подключенном через HDMI. Поскольку этот режим не является дистанционным, то в нем используется единственное разрешение 1920х1080, вырезающее центральную часть 1х1 и являющееся оптимальным для фокусировки.  Mode M/A переключает между ручным и автоматическим режимами. В режиме M камера будет реагировать только на изменения выдержки, контраста, яркости, насыщенности, резкости.  Кнопка Снимок запускает режим съемки с выбранными параметрами и разрешением, выбранным ниже.

Далее расположены ползунки, позволяющие менять настройки. Изменения значения параметров отображаются в реальном времени за счет JavaScript и передаются камере при нажетии кнопки Enter, находящейся над ними. После этого страница перезагружается. В графе выдержка отображается ее значение в мс. Если задан 0, то включается автоматический режим. Скорость затвора в долях секунды отображает выбранное автоматикой значение выдержки. Замер производится при нажатии кнопки Enter. Фокусное расстояние объектива для исправления дисторсии задается в условных пикселях, где длина пикселя равна длине матрицы, деленной на 640. Ползунок масштаб относится только к преобразованному при коррекции изображению.

Далее идет выбор предустановок баланса белого. Если выбрано off, то баланс осуществляется изменением уровня красного и синего ползунками, расположенными справа и слева от кнопки.

Ползунок Вращение имеет 4 значения: 0, 90, 180, 270 и служит для правильной ориентации предпросмотра при портретной и ландшафтной ориентации камеры.

Строка с чекбоксами: off, low, medium, hight передает  при нажатии желаемое значение аппаратного сжатия тонов камере.

В самом низу находится выбор разрешения снимка. Значения 3280х2464 и 1640х1232 относятся к 8 Мп камере, а 2592х1944 и 1296х972 к % Мп камере. Формат 640х480 одинаков для обеих камер, а формат 1920х1080 вырезает 2 Мп фрагмент из центра обеих матриц. 

Надпись на кнопке отображает текущее значение. Нажатие на кнопку без выбора приведет к ошибке и потребует перезагрузки страницы.

Сравнение

Сравнение всех трех программ проводилось на двух компьютерах: Raspberry Pi 3+ и ZeroW.


Pi3+
Pi3+ дист.
ZeroW
ZeroW дист.
Tkinter 15%
48% (VNC)
55%
100% (VNC)
Tkinter + DP
17%
33% (VNC) 80%
100% (VNC)
Flask
28% (Chromium)
15%
80% (Midori)
55%
Flask + DP
22% (Chromium)
17%
90% (Midori) 75%
Socketserver
17% (Chromium)
3%
100% (Midori) 60%
Socketserver + DP
32% (Chromium)
25%
100% (Midori) 100%

Первая колонка: компьютер Pi3+ запущен с живой картинкой, выведенной на подключенный к нему по hdmi монитор, в случае с веб-интерфейсом на этом же компьютере запущен браузер Chromium. Вторая колонка: отображение и управление на удаленном компьютере. Мощности Raspberry Pi используются для передачи данных. В этом случае при программе с Tkinter идут затраты ресурсов на работу VNC сервера. Значения загрузки взяты общие для компьютера и гуляют на 20% от приведенных значений. На загрузку влиет, в частности, изменение картинки или движение в кадре.  Таким образом если работать локально, то абсолютный лидер программа с Tkinter поскольку работает одна программа на питоне и не надо тратить ресурсы ни на браузер, ни на удаленный рабочий стол. При работе с удаленного компьютера или телефона точно такую же загрузку демонстрирует программа, использующая Flask, а программа с Tkinter демонстрирует меньшую загрузку процессора при использовании коррекции дисторсии (DP). Это, вероятно, связано с тем, что падает передаваемый поток со 100 Кб/с до 50 Кб/с за счет меньшей частоты кадров и большего сжатия за счет меньшей детализации. Программа, написанная с использованием Socketserver, похоже, перекладывает кодирование потока с камеры на графический процессор и практически не загружает ЦПУ, при необходимости исправлять дисторсию с помощью opencv мы это преимущество теряем и производительность сравнима с программой на Tkinter. Для Pi3 нагрузка, создаваемая любой из этих программ, не критична, запаса хватает, так как в большинстве случаев это единственная работающая на компьютере программа. Глядя на эти результаты, можно подумать, что для слабенькой Zero лучшим вариантом будет Socketserver без коррекции, но не тут-то было: графический процессор у Zero на себя работу не берет и использование Flask представляется предпочтительным. При локальном использовании запуск браузера Chromium приводит к появлению изображения с крайне низкой частотой кадров, как в случае Flask  так и  Socketserver,  и далее машина виснет и не позволяет не только управлять камерой, но и перезапустить компьютер. Таким образом, для ZeroW  Tkinter является самым универсальным решением, так как работает и локально и позволяет управлять дистанционно, хотя загрузка процессора при этом достигает 100%. Если вместо Chromium запустить Midori, то удается работать с Flask и локально с загрузкой 80% без корректировки дисторсии и 90% с ней. Для Flask на телефоне можно запустить простенький браузер, написанный на De Re BASIC!:

 HTML.OPEN
start:
! Load the file
onerror:
HTML.LOAD.URL "http://192.168.0.6:5000/"

! The user now sees the html actions

! loop until data$ is not ""

DO
HTML.GET.DATALINK data$
UNTIL data$ <> ""
goto start

В строке HTML.LOAD.URL указываете адрес компьютера, на котором запущена программа на Python 3, и заданный порт. С  socketserver придется использовать полноценный браузер.

Для автозапуска надо поместить файл picam.desktop в папку /home/pi/.config/autostart/ со следующим содержанием:

[Desktop Entry]
Type=Application
Name=picam
Comment=start
Exec=python3 /home/pi/picamera9/app9.py
Terminal=true

04.05.2020

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

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