ubuntu 18.04 下 OpenCV 3.2.0 的 opencv_example 运行闪退问题探究。

1. 问题说明

在之前的ORB-SLAM3项目于ROS运行的博客中,提到过安装ROS时会自己安装一个OpenCV 3.2.0版本,所以最好不要安装其他版本的OpenCV,避免和OpenCV 3.2.0产生冲突,导致ROS链接失败。

今天尝试在ubuntu18.04的虚拟机里面安装OpenCV 3.2.0时,先是遇到了CMake构建问题,又遇到了OpenCV 3.2.0提供的demo代码无法正常运行的问题。

1
2
3
4
5
# 编译demo文件并运行
cd opencv-3.2.0/samples/cpp/example_cmake
mkdir build && cd build
cmake .. && make
./opencv_example

出现的状况就是,当我执行./opencv_example的时候,程序没有任何反应就直接终止了

1
2
3
4
king@ubuntu:~/slam/pkg/opencv-3.2.0/samples/cpp/example_cmake/build$ ./opencv_example 
Built with OpenCV 3.2.0
Capture is opened
king@ubuntu:~/slam/pkg/opencv-3.2.0/samples/cpp/example_cmake/build$

理论上执行./opencv_example的时候,应该弹出一个摄像头画面的窗口,且该窗口应一直存在。如下图所示

image.png

所以,窗口没有弹出程序就终止了,肯定是有问题的。我们就要看看问题到底在哪里。

2. 问题探究

2.1. 是否识别了摄像头设备

首先是确认我们的usb摄像头到底有没有连到主机上。因为我使用的是虚拟机,所以需要在VMware里面设置让摄像头连接到虚拟机上而不是连到宿主机上。

在VMware的顶栏上,选择虚拟机-可移动设备,在可移动设备的列表里面应该能看到你的USB摄像头。如果看不到,说明windows上都没认出来这个摄像头,请检查USB链接或确认摄像头自身是否损坏。

如下图所示,这里出现了我的USB Camera,且前面有一个勾勾代表摄像头已经练到了虚拟机上。如果没有连接到虚拟机而是宿主机上,这里的弹出菜单会显示“连接(断开与 主机 的链接)”,点击它即可让USB设备连接到虚拟机。

image.png

另外,如果你在虚拟机开启的情况下将摄像头插入电脑,VMware会主动弹窗咨询你新的usb设备是要插入到虚拟机还是宿主机。选择虚拟机即可。

现在VMware里面已经配置好了USB摄像头连到虚拟机,我们就可以进虚拟机内部检查一下是否有摄像头设备了。首先执行ls /dev/video* -l命令,可以看到有两个video设备。说明至少系统是认到了视频输入设备的。

1
2
3
king@ubuntu:~$ ls /dev/video* -l
crw-rw----+ 1 root video 81, 0 Feb 28 18:08 /dev/video0
crw-rw----+ 1 root video 81, 1 Feb 28 18:08 /dev/video1

随后安装一下v4l-utils软件包,它提供了一些摄像头相关的工具,可供我们检测当前主机上的摄像头设备。

1
sudo apt-get install -y v4l-utils

我们可以通过v4l2-ctl -d /dev/video0 --all命令查看某个设备的详细信息,这里可以看到/dev/video0就是我插入的USB摄像头,分辨率是640*480。说明linux系统已经成功识别了摄像头。

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
king@ubuntu:~$ v4l2-ctl -d  /dev/video0 --all
Driver Info (not using libv4l2):
Driver name : uvcvideo
Card type : USB Camera: USB Camera
Bus info : usb-0000:02:03.0-1
Driver version: 5.4.233
Capabilities : 0x84A00001
Video Capture
Metadata Capture
Streaming
Extended Pix Format
Device Capabilities
Device Caps : 0x04200001
Video Capture
Streaming
Extended Pix Format
Priority: 2
Video input : 0 (Camera 1: ok)
Format Video Capture:
Width/Height : 640/480
Pixel Format : 'MJPG'
Field : None
Bytes per Line : 0
Size Image : 614400
Colorspace : sRGB
Transfer Function : Default (maps to sRGB)
YCbCr/HSV Encoding: Default (maps to ITU-R 601)
Quantization : Default (maps to Full Range)
Flags :
Crop Capability Video Capture:
Bounds : Left 0, Top 0, Width 640, Height 480
Default : Left 0, Top 0, Width 640, Height 480
Pixel Aspect: 1/1
Selection: crop_default, Left 0, Top 0, Width 640, Height 480
Selection: crop_bounds, Left 0, Top 0, Width 640, Height 480
Streaming Parameters Video Capture:
Capabilities : timeperframe
Frames per second: 30.000 (30/1)
Read buffers : 0
brightness 0x00980900 (int) : min=-64 max=64 step=1 default=0 value=0
contrast 0x00980901 (int) : min=0 max=95 step=1 default=34 value=34
saturation 0x00980902 (int) : min=0 max=100 step=1 default=56 value=56
hue 0x00980903 (int) : min=-2000 max=2000 step=100 default=0 value=0
white_balance_temperature_auto 0x0098090c (bool) : default=1 value=1
gamma 0x00980910 (int) : min=100 max=300 step=1 default=120 value=120
power_line_frequency 0x00980918 (menu) : min=0 max=2 default=1 value=1
white_balance_temperature 0x0098091a (int) : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive
sharpness 0x0098091b (int) : min=0 max=7 step=1 default=0 value=0
backlight_compensation 0x0098091c (int) : min=0 max=3 step=1 default=2 value=2
exposure_auto 0x009a0901 (menu) : min=0 max=3 default=3 value=3
exposure_absolute 0x009a0902 (int) : min=78 max=10000 step=1 default=312 value=312 flags=inactive
focus_absolute 0x009a090a (int) : min=0 max=15 step=1 default=0 value=0 flags=inactive
focus_auto 0x009a090c (bool) : default=1 value=1

2.2. 摄像头驱动问题?

既然摄像头设备存在,则需要确认摄像头本身是否能被linux正常调用。

测试的方法很简单,下载ubuntu的cheese茄子软件(其实就是windows的相机软件),看看是否能正常打开摄像头。

1
sudo apt-get install -y cheese

安装完毕后,使用cheese命令即可打开摄像头。结果发现,弹窗是黑的。

image.png

这里我拔除了摄像头,可以看到cheese会显示“no device found”(找不到设备),这就说明,上图中黑屏的情况并不是正常情况。如果摄像头不存在,会直接提示我们找不到设备,而不会黑屏。

image.png

结合之前的信息,可知我们现在linux系统已经识别到了USB摄像头,但没有办法调用它来拍照

ubuntu下摄像头启动与黑屏问题解决博客里面的方式解决了,在VMware的虚拟机设置里面,把usb兼容性改成3.1就可以了(原本是usb2.0)。修改了之后vm会自动重置虚拟机。

image.png

虚拟机重置了之后,重新连接一下usb摄像头到虚拟机就ok了。此时cheese已经能识别到我们的摄像头并拍摄画面了。

image.png

到这里,说明摄像头能被linux成功识别并调用,摄像头本身并没有问题。

2.3. opencv_example代码探究

既然摄像头本身没有问题,那问题就是在OpenCV3.2.0或者说是在示例代码上了。示例代码samples/cpp/example_cmake/exmaple.cpp的主体并不长,这里直接给出。

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
// opencv-3.2.0/samples/cpp/example_cmake/exmaple.cpp
int main()
{
cout << "Built with OpenCV " << CV_VERSION << endl;
Mat image;
VideoCapture capture;
capture.open(0);
if(capture.isOpened())
{
cout << "Capture is opened" << endl;
for(;;)
{
capture >> image;
if(image.empty())
break;
drawText(image);
imshow("Sample", image);
if(waitKey(10) >= 0)
break;
}
}
else
{
cout << "No capture" << endl;
image = Mat::zeros(480, 640, CV_8UC1);
drawText(image);
imshow("Sample", image);
waitKey(0);
}
return 0;
}

首先尝试将代码中的capture.open(0)改成capture.open("/dev/video0");,会发现问题依旧,启动之后未出现任何弹窗程序就终止了。

尝试将其改成capture.open(1);,可以看到这个设备是不存在的,open函数失败了并打印了错误信息,随后程序走到了No capture并正常显示了黑屏的Hello OpenCV页面。

image.png

这里我发现了一个问题,其实在没有修改代码之前,程序已经打印了Capture is opened,说明摄像头其实已经被open成功了,进入了if(capture.isOpened())为真的分支中。

1
2
3
4
king@ubuntu:~/slam/pkg/opencv-3.2.0/samples/cpp/example_cmake/build$ ./opencv_example 
Built with OpenCV 3.2.0
Capture is opened
king@ubuntu:~/slam/pkg/opencv-3.2.0/samples/cpp/example_cmake/build$

那么我们就来看看这个分支里面有啥问题会导致程序没有任何报错就退出吧。其中for(;;)是一个死循环,退出条件是内部的两个break。程序没有任何报错就终止,只能是这两个break导致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(capture.isOpened())
{
cout << "Capture is opened" << endl;
for(;;)
{
capture >> image;
if(image.empty())
break;
drawText(image);
imshow("Sample", image);
if(waitKey(10) >= 0)
break;
}
}

所以我们可以修改代码,给这两个break之前加上打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cout << "Capture is opened" << endl;
for(;;)
{
capture >> image;
if(image.empty()){
cout << "Image is empty! break." << endl;
break;
}
drawText(image);
imshow("Sample", image);
if(waitKey(10) >= 0){
cout << "waitKey() >= 0! break." << endl;
break;
}
}

再次运行,会发现是第二个break导致程序终止了。

1
2
3
4
5
king@ubuntu:~/slam/pkg/opencv-3.2.0/samples/cpp/example_cmake/build$ ./opencv_example 
Built with OpenCV 3.2.0
Capture is opened
waitKey() >= 0! break.
king@ubuntu:~/slam/pkg/opencv-3.2.0/samples/cpp/example_cmake/build$

那肯定是waitKey的返回值大于等于0命中了判断导致的。该函数的原型如下

1
int cv::waitKey(int delay = 0)

这个函数是干啥用的呢?官方的说明如下:

Waits for a pressed key.

The function waitKey waits for a key event infinitely (when delay = 0) or for delay milliseconds, when it is positive. Since the OS has a minimum time between switching threads, the function will not wait exactly delay ms, it will wait at least delay ms, depending on what else is running on your computer at that time. It returns the code of the pressed key or -1 if no key was pressed before the specified time had elapsed.

这个函数会至少等待delay毫秒的键盘输入,并返回键盘输入的ASCII值。当没有键盘被按下的时候,会返回-1。如果delay传入0则代表永久等待。

既然返回值是按键的ASCII码,我们可以打印一下这个函数的返回值,看看它收到了什么

1
2
3
4
5
6
int key = waitKey(10);
cout << "waitKey() return: " << key << endl;
if(key >= 0){
cout << "waitKey() >= 0! break." << endl;
break;
}

打印的结果是255,但是ASCII码的最大值只到127,这里的255已经超出键盘上按键可能返回的值了,肯定是个无意义的返回值!

1
2
waitKey() return: 255
waitKey() >= 0! break.

bing一搜,就找到了类似的帖子:opencv中waitkey()函数返回值为255_以下哪个选项是使用opencv的waitkey函数的返回值的正确描述?-CSDN博客

说白了就是,因为操作系统缘故,原本在没有任何键盘输入时应该返回的-1函数,被当作了一字节的无符号整数处理,返回成了255。

3. 问题解决

现在已经知道了问题的来源了,解决方案就是在判断中过滤掉255这个值,或者直接把判断写死成键盘上的某个按键,只有按下键盘上的特定按键的时候才会退出。

1
2
3
4
5
6
int key = waitKey(10);
cout << "waitKey() return: " << key << endl;
if(key >= 0 && key != 255){
cout << "waitKey() >= 0! break." << endl;
break;
}

过滤掉返回值255之后重新编译,demo就能正常显示摄像头的画面了。左侧可以看到waitKey一直在返回255。

image.png

此时按下键盘上的数字1,可以看到它识别到了ASCII 49并成功退出程序了。

image.png

4. The end

不管咋样,这个困扰了我大半个上午的问题总算是解决啦!

最开始我没想起来去看源码,以为一直是OpenCV没办法正常打开摄像头的问题,搜了一圈都没有找到解决方案。最后给代码加上了打印调试大法才找到了问题所在。感觉前面的折腾有点钻牛角尖了。