树莓派上的摄像头与OpenCV

树莓派上都有OpenCV了,是不是再装个摄像头玩[奸笑]

参考:Accessing the Raspberry Pi Camera with OpenCV and Python

1 摄像头连接及配置

摄像头安装及配置我就不写了,懒的翻译原文……

1.1 安装依赖

安装带array子模块的picamera,用以将图像Numpy的array传给OpenCV处理:

1
2
source activate cv
pip install "picamera[array]"

1.2 摄像头图像预览

需要注意的是,树莓派上的vnc-server默认配置是不能将传输摄像头预览图像传送到vnc-client上的,需要修改配置:在树莓派上打开vnc-server的菜单 -> Options...-> TroubleShooting -> 选择Enable experimental direct capture mode。(参考Sending Raspberry Pi Camera preview to a laptop running VNC Viewer

2 使用Python+OpenCV访问摄像头输出的图片

通过Python将摄像头输出的图片送入OpenCV进行显示,显示时用到了OpenCV的highGUI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import cv2

# 初始化摄像头
camera = PiCamera()
# 新建原始摄像头数据的引用
rawCapture = PiRGBArray(camera)

# 等一小段时间让摄像头启动
time.sleep(0.1)

# 从摄像头获取一张图片
camera.capture(rawCapture, format="bgr")
image = rawCapture.array

# 在屏幕上显示,按下任意键退出
cv2.imshow("Image", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

需要注意的是获取图片时使用的矩阵格式为format="bgr",参见4.2. Capturing to an OpenCV object

使用PiRGBArray对象的好处就是省去了将原始的RGB矩阵封装成各种格式(如JPEG)的步骤,这样读入OpenCV时也就省去了OpenCV将各种格式解码为RGP矩阵的步骤,尤其是在树莓派这种资源受限的设备上能够节省很多算力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import cv2

# 初始化摄像头并设置分辨率和帧率
camera = PiCamera()
camera.resolution = (640, 480)
camera.framerate = 32
# 新建原始摄像头数据的引用,指定分辨率
rawCapture = PiRGBArray(camera, size=(640, 480))

# 等一小段时间让摄像头启动
time.sleep(0.1)

# 从摄像头中获取视频帧
for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):
# 直接从摄像头获取使用Numpy矩阵表达的原始图片数据,
# 其中`camera.capture_continuous`返回一个`frame`对象,
# `frame`对象的`array`属性中存储的就是该帧Numpy矩阵
image = frame.array

# 在highGUI中显示该帧
cv2.imshow("Frame", image)
key = cv2.waitKey(1) & 0xFF

# 注意,在读取下一帧前需要清空当前帧,否则代码报错
rawCapture.truncate(0)

# 在highGUI中按q键退出循环
if key == ord("q"):
cv2.destroyAllWindows()
break

3 试一试图像的透视变换

搞事情之前:我想在Python模块搜索路径中加入我的工作环境~/pyworkspace,以后的事情都在这里搞。
因此,在~/berryconda3/envs/cv/lib/python3.5/site-packages下新建workspace.pth文件,写入内容:

1
/home/pi/pyworkspace

搞一个小事情先,试一下OpenCV的性能,比如4 Point OpenCV getPerspective Transform Example,矩形的四点透视变换。

1
2
3
4
mkdir cvpi && cd cvpi
touch __init__.py
mkdir transform && cd transform
touch perspective_transform.py

透视变换的代码思路:

  1. 通过Python代码测量图片中目标矩形区域的长宽;
  2. 计算输出直角坐标系中的矩形顶点坐标
  3. 利用cv2.getPerspectiveTransform()函数计算变换矩阵并应用,输出图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import numpy as np
import cv2

# /home/pi/cvpi/transform/perspective_transform.py
# 使用透视变换还原矩形

def order_points(pts):
"""
调整矩形四个点的排列顺序,用以维护有序的坐标列表。

接受一个包含四个坐标的列表,按照左上、右上、右下、左下的顺序重新排列坐标。

Args:
pts: 一个shape为(4, 2)的nparray。
Returns:
顺序调整后的nparray,shape依然是(4, 2)。
"""
# 初始化包含四个坐标的坐标列表(cv用坐标系,x轴向右,y轴向下),分别对应矩形的左上、右上、右下、左下坐标
rect = np.zeros((4, 2), dtype = "float32")

# 计算每个坐标的x+y,左上应当具有最小的和,而右下则应当具有最大的和
s = pts.sum(axis = 1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]

# 计算每个坐标的y-x,右上应具有最小差,左下应具有最大差
diff = np.diff(pts, axis = 1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]

return rect

def four_point_transform(image, pts):
"""
用透视变换,将图片中使用`pts`标注的透视图矩形区域,变换为鸟瞰图中的矩形区域。

接受一个图片,以及图片中一个矩形的顶点坐标列表,通过透视变换输出该区域的鸟瞰图。

Args:
image: 图片RGB矩阵。
pts: 目标矩形区域的顶点坐标列表。
Returns:
透视变换后的矩形区域鸟瞰图片。
"""
# 重新排列矩形顶点坐标,初始化左上、右上、右下、左下坐标
rect = order_points(pts)
(tl, tr, br, bl) = rect

# 计算矩形宽度:取 右上与左上顶点间的距离 与 右下与左下顶点间的距离 之中的最大值
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))

# 计算矩形高度:取 右上与右下顶点间的距离 与 左上与左下顶点间的距离 之中的最大值
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB))

# 按照左上(0, 0)、右上(0, H)、右下(W, H)、左下(W, 0)的顺序构造变换后的鸟瞰图坐标列表
dst = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]
], dtype = "float32")

# 使用源坐标和目标坐标计算透视变换矩阵
M = cv2.getPerspectiveTransform(rect, dst)
# 在源图片上应用透视变换
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))

return warped

测试变换效果:

1
2
3
cd ../..
mkdir test && cd test
touch pptf_test.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from cvpi.transform.perspective_transform import four_point_transform
import numpy as np
import argparse
import cv2

# /home/pi/cvpi/test/pptf_test.py
# 测试透视变换:
# python pptf_test.py --image images/example_01.png --coords "[(73, 239), (356, 117), (475, 265), (187, 443)]"
# python pptf_test.py --image images/example_02.png --coords "[(101, 185), (393, 151), (479, 323), (187, 441)]"
# python pptf_test.py --image images/example_03.png --coords "[(63, 242), (291, 110), (361, 252), (78, 386)]"

# 构造命令行工具
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", help = "path to the image file")
ap.add_argument("-c", "--coords",
help = "comma seperated list of source points")
args = vars(ap.parse_args())

# load the image and grab the source coordinates (i.e. the list of
# of (x, y) points)
# 从文件读取图片RGB矩阵,从命令行中读取目标矩形区域的顶点坐标。
# NOTE: 出于安全考虑,不鼓励使用`eval`函数,但在此仅为测试代码功能。
image = cv2.imread(args["image"])
pts = np.array(eval(args["coords"]), dtype = "float32")

# 测试`four_point_transform`的功能。
warped = four_point_transform(image, pts)

# 显示图片
cv2.imshow("Original", image)
cv2.imshow("Warped", warped)
cv2.waitKey(0)
cv2.destroyAllWindows()

矩形透视变换测试

4 试一试OpenCV文档扫描器

在搞事情之前:

1
2
3
4
# pwd: ~./pyworkspace/cvpi
mkdir tool && cd tool
touch __init__.py scan.py
conda install -y scikit-image

作者还开源了自己封装的OpenCV常用函数,比如上面的透视变换就包含在内,安装imutils工具包:

1
pip install --upgrade imutils

另外,我发现16G的卡已经用了很多空间了,所以推荐删点空间出来:

1
2
3
4
sudo apt-get purge wolfram-engine
sudo apt-get purge libreoffice*
sudo apt-get clean
sudo apt-get autoremove
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from cvpi.transform.perspective_transform import four_point_transform
from skimage.filters import threshold_local
import numpy as np
import argparse
import cv2
import imutils

# /home/pi/cvpi/tool/scan.py

# 构造命令行工具
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required = True,
help = "Path to the image to be scanned")
args = vars(ap.parse_args())

################################################################################
# 1 边缘检测
################################################################################
# 读取图片,计算图片原始高度与目标高度的缩放比例
# 备份图片,然后以500为高度成比例缩放图片
image = cv2.imread(args["image"])
ratio = image.shape[0] / 500.0
orig = image.copy()
image = imutils.resize(image, height = 500)

# 将图片处理为灰阶图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 使用高斯模糊去掉较为繁杂的边界,目的是仅保留明显的轮廓,有助于轮廓检查
gray = cv2.GaussianBlur(gray, (5, 5), 0)
edged = cv2.Canny(gray, 75, 200)

# show the original image and the edge detected image
# 显示原图与边缘检测的结果对比
print("STEP 1: Edge Detection")
cv2.imshow("Image", image)
cv2.imshow("Edged", edged)
cv2.waitKey(0)
cv2.destroyAllWindows()

################################################################################
# 2 轮廓查找
# 文档扫描的对象通常是一页矩形的文档,按照经验矩形有四个顶点。
# 因此我们假设,边界检查结果中最大的且具有四个顶点的轮廓即为文档轮廓。
################################################################################
# 获得轮廓列表:
# 第一个参数:传入要进行轮廓查找的图片,由于函数会直接操作该图片,这里使用边缘检测后的图片备份。
# 第二个参数:表示使用仅普通列表组织返回的轮廓,
# 若指定为RETR_TREE则还会返回为有层级结构的列表(即表示出轮廓之间的包含关系)。
# 第三参数表示简单压缩轮廓点集,如将共线段的点简化为线段两端点,节省内存。
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# 由于OpenCV2与3返回轮廓检查返回值结构不一样,因此为了代码通用性需要判断OpenCV版本。
cnts = cnts[0] if imutils.is_cv2() else cnts[1]
# 按面积将轮廓从大到小排序,保留面积最大的5个轮廓。
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:5]

for c in cnts:
# 测量轮廓周长。
peri = cv2.arcLength(c, True)
# 使用Douglas-Peucker曲线近似算法,允许2%的周长误差,返回近似后的顶点列表。
approx = cv2.approxPolyDP(c, 0.02 * peri, True)

# 如果近似结果仅包含4个顶点,则假设找到原图片中目标文件的轮廓
if len(approx) == 4:
screenCnt = approx
break

# 显示目标文件的轮廓
print("STEP 2: Find contours of paper")
# 参数分别为:要绘制的原图,轮廓点集列表,需要绘制的轮廓(-1表示画出所有),轮廓颜色,轮廓粗细
cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2)
cv2.imshow("Outline", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

################################################################################
# 3 透视变换
# 将获得的文档轮廓进行透视变换,输出文档的鸟瞰图。
################################################################################
# 将轮廓按比例缩放,并应用透视变换。
warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)

# 将变换结果灰阶处理,设置阈值以制造出类似扫描仪的黑白效果。
warped = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
T = threshold_local(warped, 11, offset = 10, method = "gaussian")
warped = (warped > T).astype("uint8") * 255

# 显示原图与处理后效果对比
print("STEP 3: Apply perspective transform")
cv2.imshow("Original", imutils.resize(orig, height = 650))
cv2.imshow("Scanned", imutils.resize(warped, height = 650))
cv2.waitKey(0)
cv2.destroyAllWindows()

边缘检测

边缘检测

轮廓查找

轮廓查找

透视变换

透视变换

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×