4、picamera基本使用方法
如果你是一个python程序员,那么你将轻松的掌握以下实例,请随时提出改进或新的实例。
4.1、捕捉一个图像输出至文件
使用capture方法可以轻松将捕捉到的图像输出至指定文件。
下面这个实例是捕捉一个分辨率为1024*768的图像,并将之输出到foo.jpg中:
import time
import picamera
with picamera.PiCamera() as camera:
camera.resolution = (1024, 768)
camera.start_preview()
#摄像头预热2秒
time.sleep(2)
camera.capture('foo.jpg')
需要注意的是,若目录中有同名文件,picamera将会重置该图片。
4.2、捕捉一个流
这个实例是通过capture()方法将捕捉的一个图像转成一个以jpeg解码的流(bytes流):
import io
import time
import picamera
# 创建一个流
my_stream = io.BytesIO()
with picamera.PiCamera() as camera:
camera.start_preview()
# 摄像头预热
time.sleep(2)
camera.capture(my_stream, 'jpeg')
需要注意的是,这不像输出到一个文件,该流捕捉以后会自动关闭脚本,因为在这个实例中没有对流进行其他操作。若对象有flush方法的话,在捕捉完毕后,会调用对象的flush方法,如下:
import time
import picamera
# 打开一个扩展名为jpg的文件
my_file = open('my_image.jpg', 'wb')
with picamera.PiCamera() as camera:
camera.start_preview()
time.sleep(2)
camera.capture(my_file)
# 在此时capture会调用my_file的flush方法,完成流封装。
# 但并未关闭文件,需要调用close进行关闭
my_file.close()
上边这个实例需要注意的是,我们并没有指定捕捉文件的解码格式,所以capture会获取输出的文件名的扩展名来寻找解码属性。
4.3、捕捉一个图像转化为PIL imag对象
首先我们要捕捉一个图像流,然后通过流读取将图像的数据转换成PIL image对象。
import io
import time
import picamera
from PIL import Image
# 创建一个流
stream = io.BytesIO()
with picamera.PiCamera() as camera:
camera.start_preview()
time.sleep(2)
camera.capture(stream, format='jpeg')
# 将指针指向流的开始
stream.seek(0)
image = Image.open(stream)
4.4、捕捉一个图像转化为open cv对象
首先我们捕捉一个图像流,然后将图像数据转换为open cv对象:
import io
import time
import picamera
import cv2
import numpy as np
# 创建一个流
stream = io.BytesIO()
with picamera.PiCamera() as camera:
camera.start_preview()
time.sleep(2)
camera.capture(stream, format='jpeg')
# 从流构建numpy
data = np.fromstring(stream.getvalue(), dtype=np.uint8)
# 通过opencv解码numpy
image = cv2.imdecode(data, 1)
# opencv解码后返回以RGB解码的图像数据
image = image[:, :, ::-1]
如果你不想使用有损JPEG编码,并希望加快这一解码过程的话,可以使用picamera自带的picamera.array模块。可以使用PiRGBArray类简单的捕获’brg’格式的数据。(假定RGB与BGR是分辨率相同的数据,只是具有相反的颜色)
import time
import picamera
import picamera.array
import cv2
with picamera.PiCamera() as camera:
camera.start_preview()
time.sleep(2)
with picamera.array.PiRGBArray(camera) as stream:
camera.capture(stream, format='bgr')
# 此时就可以获取到bgr的数据流了
image = stream.array
4.5、调整捕捉图像的分辨率
有时,我们需要将图片进行某种分析或处理,你希望尽可能的捕获比较小分辨率的图像,虽然这种分辨率的调整可以交给PIL或者opencv来完成,但是通过picamera可以更搞笑的完成这个操作:
import time
import picamera
with picamera.PiCamera() as camera:
camera.resolution = (1024, 768)
camera.start_preview()
# 摄像头预热
time.sleep(2)
camera.capture('foo.jpg', resize=(320, 240))
同时调整分辨率的参数也可以适用于捕获视频的start_recording()方法。
4.6、捕获连续图像
您可能希望连续捕捉到的图像的亮度、色彩与对比度上保持一致(例如,这个可能在延迟摄影中非常有用)。可以设置一些属性,确保在多个镜头中保持画面的一致。具体来说,您需要保证摄像头的曝光时间,白平衡和画面增益都是固定的。
设置这些属性是非常专业的,并且非常难调整到合适的范围值。在白天,对于iso,100~200是一个简单的合理范围,而在低光环境下,需要调整到400~800。对于shutter_speed如果需要确定一个合理的范围值可以直接查询exposure_speed属性。对于曝光增益,通常只需要将analog_gain设置为大于1的值(analog_gain的默认值为1,但这将产生一个全黑的图像帧)。
下面的这个例子提供了一些简单配置的实例:
import time
import picamera
with picamera.PiCamera() as camera:
camera.resolution = (1280, 720)
camera.framerate = 30
# 等待摄像头预热,适应光线环境
time.sleep(2)
# 现在设置图像修正值
camera.shutter_speed = camera.exposure_speed
camera.exposure_mode = 'off'
g = camera.awb_gains
camera.awb_mode = 'off'
camera.awb_gains = g
# 最后将采取修正值的图像输出到文件
camera.capture_sequence(['image%02d.jpg' % i for i in range(10)])
4.7、捕获延时拍摄序列
有一种需求就是,每若干分钟捕获一张图像,并存储起来,若像实现这种需求最简单的方法就是capture_continuous()函数。调用这个函数时,会不断捕捉摄像头图像,知道调用停止函数。这样你可以很轻易的控制定时捕获两张图片之间的时间。
接下来这个例子就是演示每5分钟定时抓拍一张图片:
import time
import picamera
with picamera.PiCamera() as camera:
camera.start_preview()
time.sleep(2)
for filename in camera.capture_continuous('img{counter:03d}.jpg'):
print('Captured %s' % filename)
time.sleep(300) # 休眠5分钟
除了延时捕获图像以外,还可以通过datetime来计算时间,在特定的时间开启摄像头捕获图像。
import time
import picamera
from datetime import datetime, timedelta
def wait():
# 计算时间,将从下一个小时开始捕获图像
next_hour = (datetime.now() + timedelta(hour=1)).replace(
minute=0, second=0, microsecond=0)
delay = (next_hour - datetime.now()).seconds
time.sleep(delay)
with picamera.PiCamera() as camera:
camera.start_preview()
wait()
for filename in camera.capture_continuous('img{timestamp:%Y-%m-%d-%H-%M}.jpg'):
print('Captured %s' % filename)
wait()
4.8、在光线比较弱的环境下捕获图像
树莓派的摄像头可以在光纤比较弱的环境下捕获图像,其主要的实现方式是通过设置捕获属性,给摄像头设置一个比较高的光线增益,以及增加曝光的时间以让摄像头接收尽可能多的光。我们可以通过设置shutter_speed[设置曝光时间]与framerate[帧率]两个参数。首先我们要设置一个比较慢的帧率,然后将曝光时间设置为6秒来捕获图像:
import picamera
from time import sleep
from fractions import Fraction
with picamera.PiCamera() as camera:
camera.resolution = (1280, 720)
# 设置帧率为1/6fps,然后将曝光时间设置为6秒
# 最后将iso参数设置为800
camera.framerate = Fraction(1, 6)
camera.shutter_speed = 6000000
camera.exposure_mode = 'off'
camera.iso = 800
# 给摄像头一个比较长的预热时间,让摄像头尽可能的适应周围的光线
# 你也可以试试开启AWB【自动白平衡】来代替长时间的预热
sleep(10)
# 最后捕捉图像,因为我们将曝光时间设置为6秒,所以拍摄的时间比较长
camera.capture('dark.jpg')
如果在一个光线不是特别暗的地方使用以上实例,则你会得到一个严重曝光以至于可能是全白的图像文件。
因为我们设置了一个比较长的曝光时间,如果这时移动摄像头,那么我们将会得到一个严重图像扭曲的照片。
4.9、将捕获的流转换为网络流
这是一个将捕获的图片通过网络传给服务器的例子。在这个例子中我们有两个脚本:服务器端,树莓派端。然后通过socket进行连接。我们使用一种简单的数据包协议。首先我们数据包的前4个字节是类型为int值的数据,代表图像数据的长度,然后是图像的数据。若收到的数据长度为0,则代表picamera已经停止捕获图像。
数据包格式如下:
首先在服务器运行一下脚本(这个脚本是基于PIL包来解析jpeg文件,你也可以使用其他的图形库,比如opencv或graphicsMagick等来替代PIL):
import io
import socket
import struct
from PIL import Image
# 启动socket,设置监听端口为8000,接受所有ip的连接
server_socket = socket.socket()
server_socket.bind(('0.0.0.0', 8000))
server_socket.listen(0)
# 接受一个客户端连接
connection = server_socket.accept()[0].makefile('rb')
try:
while True:
# 读取我们的包头,也就是图片的长度
# 如果长度为0则退出循环
image_len = struct.unpack(', connection.read(struct.calcsize(')))[0]
if not image_len:
break
# 构造一个流来接受客户端传输的数据
# 开始接收数据并写入流
image_stream = io.BytesIO()
image_stream.write(connection.read(image_len))
# 将流的指针指向开始处,并通过PIL来读入流
# 并将之存储到文件
image_stream.seek(0)
image = Image.open(image_stream)
print('Image is %dx%d' % image.size)
image.verify()
print('Image is verified')
finally:
connection.close()
server_socket.close()
树莓派脚本如下:
import io
import socket
import struct
import time
import picamera
# 连接之前创建的服务器端
# 将my_server替换成服务器的ip
client_socket = socket.socket()
client_socket.connect(('my_server', 8000))
# 创建一个连接
connection = client_socket.makefile('wb')
try:
with picamera.PiCamera() as camera:
camera.resolution = (640, 480)
# 摄像头预热
camera.start_preview()
time.sleep(2)
# 记录一个开始时间,并构建一个流来存储捕获的图片数据
# 我们可以直接讲捕获的数据流传给服务器,但为了捕获我们的图像长度,
# 我们暂且阻碍传输,并等待捕获完成,并获取图长度组建数据包
start = time.time()
stream = io.BytesIO()
for foo in camera.capture_continuous(stream, 'jpeg'):
# 将数据写入流中
connection.write(struct.pack(', stream.tell()))
connection.flush()
# 将数据流的指针指向起始位置
connection.write(stream.read())
# 如果我们的等待的连接时间大于30秒则退出循环
if time.time() - start > 30:
break
# 重置捕获流,等待下一次捕获图像。
stream.seek(0)
stream.truncate()
# 循环结束,写一个长度为0的数据包,告知服务器我们已经完成了整个操作。
connection.write(struct.pack(', 0))
finally:
connection.close()
client_socket.close()
4.10、录制视频文件
录制一个视频文件很简单:
import picamera
with picamera.PiCamera() as camera:
camera.resolution = (640, 480)
camera.start_recording('my_video.h264')
camera.wait_recording(60)
camera.stop_recording()
请注意,我们在这里使用的方法是wait_recording,而不是time.sleep(),以下的例子也是使用这个方法来演示,虽然功能上与time.sleep相同,但是wait_recording会不断的监听错误的抛出(比如磁盘空间不足),一旦出现错误会立即暂停捕获图像,来处理当前的错误,而不是在sleep以后才进行处理。所以如果我们用time.sleep来替代wait_recording方法的话,这种错误只能在stop_recording()后才能被处理,这可能导致了我们未能及时处理错误,导致图像数据捕获失败。
4.11、将录制的视频转换为数据流
这个例子与将录制的视频存储到文件很像:
import io
import picamera
stream = io.BytesIO()
with picamera.PiCamera() as camera:
camera.resolution = (640, 480)
camera.start_recording(stream, format='h264', quality=23)
camera.wait_recording(15)
camera.stop_recording()
在这里,我们设置了一个视频质量参数quality,指示解码器将图像质量维持在23左右。H.264视频编码主要取决于两个参数:
4.12、录制多个视频文件
如果你希望将视频文件分割多个进行录制,可以使用split_recording()方法来实现这个功能:
import picamera
with picamera.PiCamera() as camera:
camera.resolution = (640, 480)
camera.start_recording('1.h264')
camera.wait_recording(5)
for i in range(2, 11):
camera.split_recording('%d.h264' % i)
camera.wait_recording(5)
camera.stop_recording()
在实例中,我们通过split_recording函数来分割录制10个视频文件,每个视频文件的时间为5秒。
也可以利用record_sequence()函数来实现这个功能,而且代码更加简洁:
import picamera
with picamera.PiCamera() as camera:
camera.resolution = (640, 480)
for filename in camera.record_sequence(
'%d.h264' % i for i in range(1, 11)):
camera.wait_recording(5)
record_sequence这个方法适用于1.3以后的版本。
4.13、循环录制
这个功能类似于将录制的视频转换成文件流,区别是,picamera将产生一个loop缓冲区,如果在缓冲区已满,则picamera会将最开头的视频替换掉,以保证在缓冲区的视频是最新的。
一个典型的例子,就是安全监控实例,在这个实例中,我们希望检测到视频内有移动的物体,然后将这段视频记录起来。本例中我们先进行录制20秒的视频,并将其存储到文件流中,直到write_now返回true才会将这个缓存区域存储起来(这段实现比较多样,我们可以使用任意一个检测图像运动的算法来实现这个例子)。如果我们检测到有移动的物体,那么我们将刷新缓存中录制10秒的视频,并将该视频保存至磁盘:
import io
import random
import picamera
def write_now():
# 在这我们做一个假的例子,产生一个随机的true来代替检测到运动的物体
return random.randint(0, 10) == 0
def write_video(stream):
print('Writing video!')
with stream.lock:
# 找到视频的起始端
for frame in stream.frames:
if frame.frame_type == picamera.PiVideoFrameType.sps_header:
stream.seek(frame.position)
break
# 将流输出到磁盘
with io.open('motion.h264', 'wb') as output:
output.write(stream.read())
with picamera.PiCamera() as camera:
stream = picamera.PiCameraCircularIO(camera, seconds=20)
camera.start_recording(stream, format='h264')
try:
while True:
camera.wait_recording(1)
if write_now():
# 检测到移动的物体
# 进行10秒的录制,并将录制的视频存放至缓存区
camera.wait_recording(10)
write_video(stream)
finally:
camera.stop_recording()
在上面的示例中,我们使用了线程锁,以防止我们在保存视频时缓存区的录像被修改(因为我们创建的缓存流是一个loop缓冲区,是可以读写覆盖的)。如果没有使用with语句块,那么我们在写入完成时,应该取消对缓存的stream.lock锁。
需要注意的是,缓存区最少要有20秒的视频,因为使用H.264解码的话,最小码率为17Mbps,所以超过或等于20秒的视频流才是可用的视频。
这功能支持1.0及以后版本。
4.14、将录制的视频用于网络传输
这个功能类似于录制视频流,但区别在于,我们将创建一个socket对象,这不像我们传输图像的数据协议那么复杂,我们并不需要对视频流进行数据包的封装。这一次,我们将持续发送视频的帧组成的数据流,以便简化我们的socket视频通讯。
首先我们开始编写服务器端脚本:
import socket
import subprocess
# 首先我们创建一个socket监听,端口为8000,接受所有的ip连接
server_socket = socket.socket()
server_socket.bind(('0.0.0.0', 8000))
server_socket.listen(0)
# 然后我们创建一个socket文件流
connection = server_socket.accept()[0].makefile('rb')
try:
# 如果接受到一个客户端连接,那么我们将通过终端来打开一个播放器。
# 如果你使用的是mplayer,则需要注释掉vlc这段。
cmdline = ['vlc', '--demux', 'h264', '-']
#cmdline = ['mplayer', '-fps', '25', '-cache', '1024', '-']
player = subprocess.Popen(cmdline, stdin=subprocess.PIPE)
while True:
# 创建一个死循环,每次读取1k的数据
# 然后将数据传送到播放器
data = connection.read(1024)
if not data:
break
player.stdin.write(data)
finally:
connection.close()
server_socket.close()
player.terminate()
注意,如果你是在Windows上运行此代码,则需要提供完成播放器exe文件路径。
你可能会注意到,播放的视频有几秒钟的延迟,不用担心,这是正常现象,因为媒体播放器为了能够流畅的播放视频,会事先换成几秒的视频流,以防止网络卡顿。但是一些播放器(比如mplayer)允许用户跳过缓存或减少缓冲区直接播放视频,虽然这将提高了播放的卡顿和中断播放的现象,但是视频的延迟将大大减少。
现在我们来编写树莓派端代码:
import socket
import time
import picamera
# 创建一个socket连接,来连接我们的服务器,这里需要将my_server替换成服务器的ip
client_socket = socket.socket()
client_socket.connect(('my_server', 8000))
# 创建一个socket文件流
connection = client_socket.makefile('wb')
try:
with picamera.PiCamera() as camera:
camera.resolution = (640, 480)
camera.framerate = 24
# 开启摄像头,并预热两秒
camera.start_preview()
time.sleep(2)
# 开始录制并传输,录制视频总长度为60秒
camera.start_recording(connection, format='h264')
camera.wait_recording(60)
camera.stop_recording()
finally:
connection.close()
client_socket.close()
还可以指出的是,我们能够利用netcat和raspivid命令来快速实现这段脚本,在终端上运行以下代码:
server-side: nc -l 8000 | vlc --demux h264 -
client-side: raspivid -w 640 -h 480 -t 60000 -o - | nc my_server 8000
然而,我们可以将连接反过来,通过树莓派简历一个视频服务器,等待播放器的连接。当连接简历时,我们会录制60秒的视频,并实时传输过去。这样我们可以先初始化摄像头的连接,等待播放器的接入:
import socket
import time
import picamera
with picamera.PiCamera() as camera:
camera.resolution = (640, 480)
camera.framerate = 24
server_socket = socket.socket()
server_socket.bind(('0.0.0.0', 8000))
server_socket.listen(0)
# 创建一个socket文件流
connection = server_socket.accept()[0].makefile('wb')
try:
camera.start_recording(connection, format='h264')
camera.wait_recording(60)
camera.stop_recording()
finally:
connection.close()
server_socket.close()
然后我们可以通过播放器来打开这个网络地址,这里演示的是利用vlc来打开摄像头:
vlc tcp/h264://my_pi_address:8000/
注意的是,目前,VLC或mplayer都不会对视频流进行GPU解码,他们会先尝试利用CPU进行解码,但这种软解码的效率不够强大。所以为了能够顺利播放,你需要将播放器运行在一个性能比较好的设备上(废话。。。)
4.15、在捕捉的图像上叠加图片
在摄像头进行捕捉的同时我们可以运行多层渲染器,然而picamera只允许加载单个渲染器到相机端,所以如果想在捕获的图像上叠加图片,我可以需要创建一个附加的渲染器来显示我们捕捉到的静态图像。
注意,picamera不支持在捕捉图像或拍摄视频的同时来叠加图像信息,如果需要嵌入一些文字信息请参阅下面的“在捕捉的图像上叠加文字信息”
叠加图片工作是一个比较复杂的操作,因为摄像头模块的大小是32X16,所以渲染的图片的宽度必须是32的倍数,其高度必须是RGB格式所规定的字节。不过虽然听上去很复杂,但是实现起来却很简单。
下面这个实例演示的是,加载任意一个图像或PIL,让他扩展到RGB允许的尺寸,这将调用add_overlay()函数:
import picamera
from PIL import Image
from time import sleep
with picamera.PiCamera() as camera:
camera.resolution = (1280, 720)
camera.framerate = 24
camera.start_preview()
# 读取任意一张图片
img = Image.open('overlay.png')
# 创建一个新的RGB渲染器,并指定渲染器的格式
pad = Image.new('RGB', (
((img.size[0] + 31) // 32) * 32,
((img.size[1] + 15) // 16) * 16,
))
# 将原始图片加载到渲染器中
pad.paste(img, (0, 0))
# 在这里我们使用原始图片的尺寸来进行叠加
o = camera.add_overlay(pad.tostring(), size=img.size)
# 在默认情况下,图片将叠加在第0层上,下方是我们的摄像头图片层(第2层)
# 这里我们将图片设置为盘透明并覆盖在捕捉的图像上
o.alpha = 128
o.layer = 3
# 创建一个死循环,等待用户终止脚本
while True:
sleep(1)
我们可以不使用一个图片文件作为叠加源,我们可以直接从numpy中产生叠加图片数据。在下面的例子中,我们构建了一个numpy 以捕捉图片相同的分辨率覆盖在图片上,并在重心画一个白色的十字以标示出我们的覆盖物。
import time
import picamera
import numpy as np
# 创建一个numpy空间,指定分辨率为1280*720,并在画面中间画一个十字
a = np.zeros((720, 1280, 3), dtype=np.uint8)
a[360, :, :] = 0xff
a[:, 640, :] = 0xff
with picamera.PiCamera() as camera:
camera.resolution = (1280, 720)
camera.framerate = 24
camera.start_preview()
# 我们通过add_overlay直接将覆盖物叠加在第3层上,这里我们可以省略覆盖物的尺寸
# 默认为捕获图片的尺寸
o = camera.add_overlay(np.getbuffer(a), layer=3, alpha=64)
try:
# 等待用户终止脚本
while True:
time.sleep(1)
finally:
camera.remove_overlay(o)
我们可以调成layer参数以隐藏渲染器(layer默认为2),或者可以通过alpha参数设置渲染器的透明度和调整渲染器的大小,使渲染器不用拉伸显示在界面上。我们也可以通过叠加图片来构建简单的用户界面。
这功能支持1.8及以后版本。
4.16、在捕捉的图像上叠加文字
相机模块包含一个基本的注释工具,它最多可允许在图片上叠加255个ASCII字符(包括在图片或视频的捕捉中或捕捉后进行叠加)。想要实现这一功能,只需要将需要叠加的文字填在annotate_text参数上:
import picamera
import time
with picamera.PiCamera() as camera:
camera.resolution = (640, 480)
camera.framerate = 24
camera.start_preview()
camera.annotate_text = 'Hello world!'
time.sleep(2)
# 将图片输出到文件
camera.capture('foo.jpg')
我们也可以通过一点技巧来显示比较长的字符串:
import picamera
import time
import itertools
s = "This message would be far too long to display normally..."
with picamera.PiCamera() as camera:
camera.resolution = (640, 480)
camera.framerate = 24
camera.start_preview()
camera.annotate_text = ' ' * 31
for c in itertools.cycle(s):
camera.annotate_text = camera.annotate_text[1:31] + c
time.sleep(0.1)
当然,这个功能也可以用在显示或嵌入录像的时间戳(该实例还演示了在绘制的时间戳下利用annotate_background属性填充背景色):
import picamera
import datetime as dt
with picamera.PiCamera() as camera:
camera.resolution = (1280, 720)
camera.framerate = 24
camera.start_preview()
camera.annotate_background = picamera.Color('black')
camera.annotate_text = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
camera.start_recording('timestamped.h264')
start = dt.datetime.now()
while (dt.datetime.now() - start).seconds < 30:
camera.annotate_text = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
camera.wait_recording(0.2)
camera.stop_recording()
该功能支持1.7及以后版本。
4.17、控制摄像头的led指示灯
摄像头的led指示灯指示了该摄像头模块在开启中,但是在某种情况下,这可能会是一个障碍,比如你在拍摄野外动物的情况下,这个led灯可能会吓跑动物,而且在近距离拍摄的时候,可能会将led的灯光反射到摄像头上。
你可以利用一些简单的方式来解决,比如用胶带或者一些遮挡物来挡住led灯,也可以通过picamera来设置disable_camera_led属性关闭led指示灯。
但是这种操作需要有RPi.GPIO库的支持,以及提供运行该python脚本足够的权限(这意味着你将需要通过root身份来运行这段脚本)才可以控制Led灯的属性。
import picamera
with picamera.PiCamera() as camera:
# 将led指示灯设置为关闭
camera.led = False
# 这时拍照时指示灯不会亮起
camera.capture('foo.jpg')
这里需要注意的是,当你第一次使用led属性来控制指示灯时,GPIO库将设置为BCM模式[GPIO.setmode(GPIO.BCM)]和禁止使用警示灯[GPIO.setwarnings(False)],所以这将使你无法控制板载led灯,如果想控制则需要手动开启。