Picamera и дистанционная съемка с живой картинкой
Статья Дистанционное управление по
сети была написана в 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()
В последующие четыре года я собрал довольно много камер и все
стоящие передо мной задачи их настройки прекрасно этой комбинацией
программ решались. Причем использование удаленного рабочего
стола позволяло не только запускать, но и налету изменять
программу и просматривать полученные фотографии с помощью
продвинутых программ просмотра отображающих не только изображение,
но и сопутствующие параметры необходимые для детального
анализа. На компьютере работа была практически так же
комфортна, как и работа с компьютером Raspberry Pi с подключенным
монитором и клавиатурой, на планшете или телефоне нажать некоторые
кнопки было сложнее, но вполне возможно. Идея использовать для
управления Веб-интерфейс и таким образом обойтись без программ
удаленного рабочего стола и, возможно сэкономить ресурсы возникла
исключительно из-за возникшего свободного времени, которое надо
было чем-то занять.
В начале я полагал, что задача давно решена, и если поискать в
сети, то можно найти почти готовое устраивающее меня решение.
Однако оказалось, что хотя задача была сформулирована и за нее
брались и обсуждали, однако готовое нужное мне решение либо не
было найдено за ненадобностью, либо, что более вероятно, не
опубликовано в виде статей, доступных поисковикам, а лежит в каких
то недокументированных репозиториях. Таким образом, передо
мной встала задача создать собственную программу, объединяющую
потоковое видео с кнопками и ползунками управления. В результате я
написал две программы, использующие Веб-интерфейс на базе
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()
Другой, более продвинутый вариант - использовать 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.
Первые три кнопки верхнего ряда меняют название при нажатии.
Отображется название режима, который включится при нажатии на
кнопку. 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 к 5 Мп камере. Формат 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
|
Облако тегов:
...
|