eboer's Boat

Neboer's Blog isn't Only About Technique

别看我的屏幕!
OpenCVCUDAC++ 31

为了应对生活中的一个问题,Neboer捣鼓了一些高科技……使用OpenCV实时检测开门,可以在100ms内极速响应!

别看我的屏幕!

生活中的问题

我们都可能会有这样一些经历:自己在做一些事情,比如用电脑玩游戏,不希望被别人看见。但是家里并不只有自己一个人,虽然自己关上了门,但是任何人都可以推门进来。如果直接给门上锁,就显得太心怀鬼胎——所以你竖起耳朵,变成了敏锐的捕食者,仔细的倾听着环境的一切声音。这不但让你玩不痛快,而且如果你家里有一些来回走动的人的话,你会疲于切换屏幕,真的是苦不堪言。有没有什么方法可以让电脑自动检测门是否被推开,然后在门被推开的那一刹那,自己切换屏幕成正常的工作屏幕呢?

你一定会说,我要找的不就是老板键嘛!你说对了,不过老板键归根到底还是一种快捷键,实际还是需要你自己来判断是否有人要进门,你一样需要费心判断。如果可以让电脑自己拥有一双眼睛,时刻盯着门该多好!这并不是天方夜谭,在现代计算机视觉和图形加速技术日益发达的今天,这件事情是完全可以交给计算机来操作的。

Neboer的奇思妙想

Neboer就是以上这种现象的受害者。Neboer的笔记本有一块亮度不高的IPS屏幕,虽然整个屏幕不是很大,但感人的是IPS的视角自由的观察特性使得其他人在远处看我屏幕上的内容十分容易。为了能够随心所欲的玩游戏、写文章、看电影,同时又在有人进门之后立刻切换屏幕,Neboer进行了许多的奇思妙想——比如,制作一个开发板,自带距离检测装置,自动检测前方障碍物的距离,如果有人推门,那么这个距离就会增加,依靠不断检测这个距离就可以在距离达到危险值之前用Zigbee/蓝牙/Wifi等协议向电脑发送告警信号,再结合电脑端的动作程序切换桌面,实现自动切屏。这个想法看起来需要的技术力稍高,维护的成本也非常高,有限的条件下无法使用。而且超声检测距离的方法在精度上是一个问题,后面我们会谈到这套系统对精度的变态要求,我们只能拒绝这个nb的设想。那么,能不能用激光测距呢?这个想法比之前那个好一些,但是电脑也不会发激光(),感觉也差不太多。检测声音?听到有人站在门口,就切换屏幕?这个有点不靠谱吧,而且电脑平时运行的时候噪音就不小,怎么保证接受的声音质量是一个问题。经历了许多内心戏,我们就不卖关子了,Neboer最终想到的解决方案是,用笔记本的摄像头来做这件事情。(早说你是笔记本有摄像头啊!)

OpenCV Python

每个笔记本都自带一个摄像头,这个摄像头本来是方便你进行视频会议用的,不过感觉正常人的笔记本的摄像头基本都不太能用得上(社恐)(不是恐怖分子!),正好拿来用做开门检测。.

开始的开始。我们在门上贴了一张纸片。并用透明胶,把这张纸片和门牢牢的固定在一起,使得一旦门发生了运动,纸片也会跟着运动。由于纸片是白色的,而门是黑色的,我们就可以通过检测纸片的边缘来检测门的运动,这二者是同步且等价的。由于纸片的大小更确定,且有明确的形状(在镜头里看去,纸片接近一个平行四边形)四角,最重要的是纸片呈明亮的白色,所以检测纸片比检测门要好得多。

这里需要用到图像处理技术了,我们当然想到了本科时期的老本行,OpenCV!于是话不多说,直接 pip install opencv-python,干起来!

很快,第一版的代码就写好了。我相信任何一个对Python有基本了解的人都能快速写好这个简单的程序。

import time
from sys import stderr, argv
import cv2
import imutils as imutils
import keyboard
import numpy as np
from cv2 import EVENT_LBUTTONDBLCLK
from datetime import datetime, timedelta
from playsound import playsound

vid = cv2.VideoCapture(0)
ret0, frame0 = vid.read()
cut_range = [None, None]

def get_coor(event, x, y, flags, param):
    global cut_range
    if event == 4:
        if not cut_range[0]:
            cut_range[0] = (x, y)
        elif not cut_range[1]:
            cut_range[1] = (x, y)
            cv2.destroyAllWindows()
    cv2.setMouseCallback('frame', get_coor, [0])
def canny_edge(src):
    gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
    denoised_src = cv2.fastNlMeansDenoising(gray, None, 5)
    edges = cv2.Canny(denoised_src, 100, 200)
    return edges
while cut_range[1] is None:
    ret0, frame0 = vid.read()
    cv2.imshow('frame', frame0)
    cv2.waitKey(1)
good_position = None
counters = 0
while True:
    _, frame = vid.read()
    cropped_frame = frame[cut_range[0][1]:cut_range[1][1], cut_range[0][0]:cut_range[1][0]]
    height, width, channels = cropped_frame.shape
    canny_result = canny_edge(cropped_frame)
    contours, _ = cv2.findContours(canny_result, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    minimal_distance = 0xFFFF
    best_point = None
    for cont in contours:
        for line in cont:
            for point in line:
                dis = point[0] ** 2 + point[1] ** 2
                if dis < minimal_distance:
                    best_point = point
                    minimal_distance = dis
    if len(contours) > 0:
#        cv2.circle(canny_result, (int(best_point[0]), int(best_point[1])), 3, (255, 255, 255), 2)
        if good_position is None:
            good_position = best_point
            print(good_position)
            continue
        else:
            if (best_point[0] - good_position[0]) ** 2 + (best_point[1] - good_position[1]) ** 2 < 3:
                continue
    keyboard.press_and_release('ctrl+win+left')
    print(datetime.now())
    break
#    cv2.imshow('frame', canny_result)
    # if cv2.waitKey(1) & 0xFF == ord('q'):
    #     break

这个程序的结构还是很明确的:首先打开一个VideoCapture,然后读入一帧完整的图像。然后显示这个图像,等待用户点击鼠标两次,裁剪,然后开始正式的图像处理循环:

  1. read,从摄像头里读取一张画面。
  2. cvtColor处理画面,留下黑白色通道
  3. FastNlDenoise,对满是噪点的WebCam捕捉到的原始画面进行降噪处理
  4. CannyEdgeDetect,检测降噪后物体的边缘,得到一个二进制的细细的边缘线。
  5. findContours找到图中最大的边界
  6. 找到边界点的四个角(边界上距离截取矩形的四顶点最近的四点),判断他们的距离与采样得到的平均距离是否差异过大,如果距离的差值大于某个值(3),那么就说明门开了,切换桌面!

以上代码确实能够检测开门,但是……好吧,检测的速度够慢的。我把主要的循环写进了一个函数loop里,然后对整个Python程序进行性能分析,结果当然是让人十分恼火的: 这个程序有一半的时间花在CPU的降噪上,然后有一半的时间在从videoCapture里read frame。而且这个程序的灵敏度不高,稳定度也不高,经常用着用着就误报了,然后就被强行切换桌面了。这个程序还有一个问题是,动作太慢!实际检测的结果表明,边界检测并不能很好的代表这个形状的所有细节,与其边界检测,不如不检测,直接对canny之后的结果进行分析。

经过了大量的实验,实际的响应时间还是不尽人意,根据profile的结果,我对Python版本的程序提出了以下优化建议:

  1. FastNIDenoise简直fast过了头,速度太慢。
  2. CannyEdge不能够完美反映出边缘的特点,因为门上的纸有四个边,在门运动的时候,只有竖边才最能够反映出门的运动效果。
  3. FindContours是没有必要的,程序可以用图上的边缘点进行帧比对,没必要用contour去拟合一个经过旋转的矩形。
  4. 程序占用了太多CPU,玩原神卡的一批!而且在游戏的时候,响应速度显著下降,根本不能实时检测开门,这简直是舍本逐末!
  5. 摄像头只有30fps,这个是硬件限制,我没有办法。

(我超,O)

针对上面四个问题,我的解决方案是,用C++保证执行速度,用cuda保证程序的性能,然后重写判断门是否开了的逻辑。

OpenCV C++

用C++做这个程序是一个很不容易的决定,因为我们的平台是Windows,而Windows最著名的就是它特有的装软件的方式:手动包管理——这简直是一场让你三天吃不好饭的恶心至极的经历。

编译OpenCV:噩梦的开始

OpenCV放出的Release是可以直接拿来用的,但是别忘了这个问题:官方放出的二进制包里没有对cuda的支持。事实上这个二进制包里的内容少的可怜,可能只有一少部分的核心的功能被实现了,而且缺乏对CPU并行化等诸多优化技术的支持——别忘了,OpenCV是Intel主导开发的。还有谁比Intel更了解自己家的CPU呢?于是,如果要追求极致性能,在Windows上编译OpenCV是一个无法避免的选择。

我不知道屏幕前的你有没有过在Windows上开发软件的经验。可能对于大多数人来说,人生编写的第一个C++程序就是在本科或者更早的时候,在Windows上编译、运行的。准确的说,Windows上的C++有许多成熟的环境,如果选择自由的MinGW,可能会好很多,但是如果用MSVC的C++——Visual C++——的话,可能会让大多数人痛不欲生。VC++和linux下的GCC C++环境很不一样,而且Visual Studio提供的解决方案十分死板——如果你不安装生成工具,你甚至必须开IDE才能完成编译和构建过程。这对于熟悉了Linux C++开发的你我来说是不可以接受的。可以说,Microsoft的傲慢在Windows的开发体验中表现得淋漓尽致。

所以,你下载了OpenCV的源代码,用CMake打开了文件夹,满怀欣喜的点击了“configure”,然后拉动CMake的选项菜单——然后石化——整整3页的依赖项,这些真的都得自己一个一个装好?年轻人,不要躺平!小小困难怎么能击倒你!站起来,继续奋斗(编译),用你勤劳的双手,去填满一个又一个的依赖选项,装好Qt、ffmpeg,如果有不能通过的,不会安装的软件,就去百度、去谷歌、去CSDN,看看大佬们是怎么装好各个依赖的!如果你什么都不做,你只会一事无成,可是你只要努力去解决依赖的问题,你就很快会成功(得到编译完成的二进制库)

我可去你的吧()

经过了一上午的研究,我很快放弃了自行构建OpenCV的打算。

  1. CMake是一个很先进的构建系统,学起来 十分简便,行为十分确定,在Windows下表现十分强悍,可以让你用 最少的配置 完成软件的构建过程。()
  2. Windows是一个很先进的操作系统,使用了不同于linux的,最先进的包管理系统—— 完全不管理 ,MSI/zip/exe等多样的安装方式让许多库的安装只需要轻轻的 无数次 点击就可以完成。
  3. Visual Studio的构建系统十分先进,每次编译都 尽可能少 的占用系统资源——尤其是宝贵的CPU资源,用 无关紧要 的编译时间来换取更少的CPU占用……对于像Qt这种需要编译许多内容的项目,你只需要在早上起来的时候开始编译,然后就可以用电脑去做任何别的事情,编译不会占用太多的电脑性能,等明天一觉醒来的时候,可能就编译成功了(当然,也可能看到一些 机器翻译 的中文报错)……
  4. 网上对编译安装OpenCV的教程十分 先进,OpenCV自己的“在Windows下构建OpenCV指南”里,竟然用的是 Windows 7 SP1 系统来演示的。对于更新版本的Windows,这个教程里早就做出了保证:“it should also work on any other relatively modern version of Windows OS”,看得出来,OpenCV开发者对于自己写的软件的环境适配性还是十分自信的。我的评价是,其傲慢程度和微软有一拼。

不行我编不下去了,Windows上的开发体验实在是太糟糕了。我举个例子吧,编译Qt,挺简单的吧,好,我问你,编译Qt的哪个大版本,哪个小版本?我翻遍了整个OpenCV的文档和网上的各种资料,也没有人讲过这个问题。我选择了最新的Qt6,并选择了qt-everywhere-6.3.0版本进行构建。然后,configure失败。算了,说这些太没有说服力了,我把我在构建中为了方便自己所记录的笔记放在这里供大家参考。如果你们知道我在写什么,或许你可以感受到我内心的绝望。

git clone git://code.qt.io/qt/qt5.git --depth=1 --recursive --shallow-submodules
git clone git://code.qt.io/qt/qt5.git --recurse-submodules -j8
configure -debug -nomake examples -nomake tests -skip qtwebengine,qtwebsockets,qtwebglplugin,qtlocation -opensource -confirm-license -mp -cmake-generator "NMake Makefiles"
AppData\Roaming\Tencent
# pyenv set
set Python_ROOT_DIR=C:\Users\username\.pyenv\pyenv-win\versions\3.9.7
# faster compile
set CL=/MP
nmake
- Qt is now configured for building. Just run 'cmake --build . --parallel'
Once everything is built, you must run 'cmake --install .'
Qt will be installed into 'C:/Qt/Qt-6.4.0'
To configure and build other Qt modules, you can use the following convenience script:
        C:/Qt/Qt-6.4.0/bin/qt-configure-module.bat
If reconfiguration fails for some reason, try removing 'CMakeCache.txt' from the build directory
# no use!
 C:\WINDOWS\system32\cmd.exe /k "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"
C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -noe -c "&{Import-Module """C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"""; Enter-VsDevShell 8b3a0a1c}"
C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64
set PATH=%PATH%;C:\your\path\here\
C:\Windows\System32\chcp.com 65001
clear all CMakeCache.txt CMakeFiles under the folder.
configure -release -nomake examples -nomake tests -skip qtwebengine,qtwebsockets,qtwebglplugin,qtlocation -opensource -confirm-license -mp -cmake-generator "Visual Studio 17 2022"  -DCMAKE_CXX_FLAGS_RELEASE="" -- -A "Win64"
C:\WINDOWS\system32\cmd.exe /k "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
# clear cache
CMakeCache.txt|CMakeFiles nowholeword:C:\projects\other\qt5_6.3.0\ 
configure -release -nomake examples -nomake tests -skip qtwebengine,qtwebsockets,qtwebglplugin,qtlocation -opensource -platform win32-msvc -cmake-generator "Visual Studio 17 2022"
# cl需要搜索这里
C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\um\x64

总之,毁灭吧,累了。我经过了两天的配置和实验性编译之后,决定放弃在Windows上构建OpenCV的过程。这个软件就应该在linux上编译嘛!我这个Windows用户凑什么热闹。

cuda:唤醒源自显卡的神奇力量

好吧,这个标题起得有点中二,不过味对了就行。

关于cuda环境的安装,许多教程都给出了详细的讲述,但是我还是决定在这里不厌其烦地再说一遍,因为cuda环境的配置还是很重要的。我们先明确一下,这个单词“cuda”的英语发音是“窟哒”而不是别的,请在一目十行地阅读本节之前纠正错误读音(哦原来你一直读的是对的啊,那没事了/呲牙)。

安装cuda

  1. 开始的开始,由于Nvidia官网在中国访问速度比较慢,所以请自行准备加速方法。而且整个cuda环境需要占用大约5GB的空间?请提前预留大约10G左右。建议安装cuda到系统盘。
  2. 首先,需要准备环境。我知道作为资深程序猿的你,电脑里一定装过cuda,但是为了性能和稳定,还是建议换成最新的cuda版本。在安装之前一定要把旧版cuda卸载掉,防止冲突。 在软件与功能里卸载就可以了。
  3. 其次,安装cuda。访问Nvidia官网下载cuda,选择合适的exe,然后点击下载。这里我建议选择网络包。让英伟达用自己的CDN下载总比你消耗节点流量下载来得快。并且网络安装包可以选择性下载组件,不会浪费时间。
  4. 下载网络安装包完成之后,双击打开。如果你下载的是本地安装包,它会提示“选择一个提取的临时文件夹”。这个并不是真正的软件安装路径,这个路径仅仅是解压安装程序到哪里,实际上的软件安装还没有开始。这个路径完全可以在安装之后卸磨杀驴地删掉(笑),所以建议选择固态硬盘,这样安装的速度会快一些,当然需要提前占用一点空间。
  5. 在真正的安装程序运行之后,会弹出一个对话框:“注意,你即将安装一个旧版本的驱动程序……”。这是什么?我明明安装的是最新版本的cuda,为什么会提示我在安装旧版的驱动程序呢?不要担心,正常安装就可以了,注意在选择安装模块的那个位置里, 取消勾选 图形驱动程序和PhysicX等选项,只保留cuda toolkit。这是什么原因呢?因为你的电脑上已经安装了更新版本的Nvidia图形驱动程序,而cuda安装包里除了cuda还包括了一些旧版的图形驱动,所以产生了冲突。Nvidia的这个安装包在这里做的有一点问题:它应该在“选择安装”页面里,当你选择图形驱动程序等组件之后,再提醒你系统中已经存在更新版本的驱动,而不是在开始之后就直接提醒,我也是费了一些力气才理解这个软件的逻辑,真的是……
  6. 选择一个合适的位置安装cuda,这个位置一般建议保留默认。

安装cuDNN(可选)

好了,你现在成功的装好了cuda的基本环境,接下来我们安装cuDNN。 虽然你不需要跑深度学习,但是为了以防万一你将来有一天可能需要跑,所以还是装一下(草

安装cuDNN,需要一个已经提交申请的Nvidia账号,也就是说,你不再能像下载cuda一样容易的获得cuDNN了。所以你需要访问cuDNN的下载页面,根据提示注册一个账号,然后填写一个表格,回到下载页面刷新,然后点击cuDNN for cuda 11.X,下载。注意别忘了选中“I Agree ...“

下载好了cuDNN之后,你会发现你得到了一个压缩包。什么?不是安装程序了吗?没错,这次需要你手动把cuDNN的文件复制到指定的位置。首先你需要解压cuDNN的安装包到一个固定的文件夹,这里就称之为,然后 复制 bin\cudnn.dll 到 C:\Program Files\NVIDIA\CUDNN\v8.x\bin 复制 include\cudnn.h 到 C:\Program Files\NVIDIA\CUDNN\v8.x\include 复制 lib\x64\cudnn*.lib 到 C:\Program Files\NVIDIA\CUDNN\v8.x\lib\x64 注意我默认了你的cuda安装位置是默认位置,如果不是请自行修改前缀路径。

好了,现在你只需要把cuDNN的library添加进PATH就可以了。将整个C:\Program Files\NVIDIA\CUDNN\v8.x添加到用户的PATH里,就OK了。 实际上,如果你需要开发基于cuDNN的程序,那么你只需要在Visual Studio里指定链接目标:cudnn.lib就可以了。

安装cuBLAS(装不上)

cuBLAS是Nvidia推出的一款线性代数库,可以用cuda加速基本的线性代数运算。其实BLAS的实现有很多,不过cuBLAS是在GPU上实现的,所以性能好很多,openCV很显然可以从中获益。

好的,你根据指引一路走,终于来到了HPC sdk的下载页面,然后怀着激动的心情,点开了Windows x64一栏……好吧, The NVIDIA HPC SDK for Windows will be available at a later date.,这可真够令人扫兴的。

所以,别想了,Windows是没有这个功能的。

所以至此,你已经装好了cuda,为顺利编译openCV GPU打下了基础。

Visual Studio:宇宙第一、地表最强IDE

Visual Studio无疑是微软最nb的产品之一了。不管你如何评价,它始终都是最强大的IDE。它的强大在于它的垄断,在于它的闭源,在于微软的傲慢。Visual Studio延续了微软的一贯传统:试图控制并解决你可能出现的所有问题。你可能会问,为什么我们要用这样一个令人讨厌的IDE来开发?因为这是最省心的路径。你当然可以选择其他的Windows C++环境来开发这个程序,但那就意味着,你必须自己解决依赖、编译、链接、构建等问题。微软虽然尝试安排你开发方面的一切,但是他安排得也很好,用现代人的话来说就是“真香”。最重要的是,Nvidia CUDA对微软的Visual C++支持的是最好的,或者说,cuda压根就没考虑过你在Windows上还用其他的IDE。所以,用Visual Studio来解决这个问题,我认为是比较合适的方案,最省心,可以让你把最多的精力放在你的程序本身,而不是环境上。

安装Visual Studio 2022

好了,闲话少说,不管你之前用Visual Studio的哪个版本,我都强烈推荐你用Visual Studio 2022。在之前一段时间里VS 2022还都只有preview版放出,但是现如今2022早已称为官方发布的最新稳定版了。请登录Visual Studio官网下载Visual Studio Installer,然后打开。这个程序会引导你完成Visual Studio的安装。

勾选哪个组件?当然是“通用Windows平台开发”和“C/C++桌面开发”了!注意安装的路径,为了整个IDE的性能和你的使用体验,建议安装在固态硬盘里。事实上,Visual Studio有的时候会卡的一批,所以硬盘一定要用最好的,还是那句话,专注于开发过程,尽量减少其他的、与开发无关的错误。所以什么事情尽可能不要为难自己,挑简单的来,以解决问题为主。

整个下载过程需要花费不少时间。在Visual Studio下载和安装组件的过程中,你可以继续安装其他的组件,比如说和Visual Studio对应版本配套的生成工具。

安装Visual Studio 2022 生成工具

注意,Visual Studio是一个IDE,可以帮助你开发软件。但是有的时候我们不需要开发软件,我们只需要把软件从源代码构建成可以使用的二进制程序。这个时候再开IDE就显得非常不合适了。于是我们需要生成工具来帮助我们配合构建脚本一起完成build from source的问题。于是我们访问Visual Studio中文下载专区,点击“适用于Visual Studio 2022的工具”, 就可以找到Visual Studio 2022 生成工具了。下载然后根据指导安装即可。不像Nvidia,Visual Studio的安装还是蛮容易的。毕竟傲慢的微软把你当傻瓜(,希望你不会像微软想的一样。

vcpkg:听我说谢谢你

前面说到,openCV的编译过程让我十分痛苦,不断有新的错误从CMake里被抛出来。就在我觉得一切都失去意义,编译无望,程序在没有开始就已经结束的时候,一个念头突然在我的脑海里冒出来——Windows真的没有包管理吗?

我也知道Windows没有包管理,但是我知道有许多开发者为了能在Windows上有更好的开发体验而努力。比如Chocolatey,比如Windows自家的nuget,这些都可以起到包管理的作用。OpenCV虽然编译选项繁多,构建过程复杂无比,但是他所依赖的所有子库都可以通过CMake编译。事实上,CMake不知道在什么时候,已经成为了不成标准的标准:在我的印象里,除了GNU家族的软件还在坚持autotools工具链生成Makefile,CMake已经成为了开源界应用最广泛的高级构建描述语言。既然他们都基于CMake,那么即使是在Windows上,通过适当组织路径,传递参数,也是可以实现不依赖于绝对路径的、标准化的构建过程的。想到这里,我便开始搜索,有没有可以自动构建OpenCV(带所有编译选项)的工具。

真的让我找到了!那就是vcpkg。在这里简单描述一下vcpkg是什么:vcpkg是微软爸爸(气节呢气节呢气节呢)(爸爸饿饿饭饭)在2016年建立的一个开源项目,直到2020年才发布了第一个Release。vcpkg旨在为Windows开发者构建一个统一的C/C++开发环境,方便依赖库的管理。vcpkg和Visual Studio是深度集成的,刚刚装好的软件包就可以立即在Visual Studio中使用。(什么?还有这种好事)(这和apt不是一样吗?激动个啥),如果要形象一点理解,就可以把vcpkg理解成开发者的chocolately,类似Ubuntu系列系统中的apt包管理器可以安装-dev包(不太一样的地方在于:vcpkg全是从源码自己编译的软件,而Ubuntu里的-dev包只是带了一份头文件或源代码而已)。

你以为事情会进展得非常顺利吗?是的,事情就是进展得非常顺利……怎么可能!vcpkg也存在许多问题,看起来如此简单的一条命令也是经过试错的。只能说,微软的文档水平一向都很高,但是在开源产品上就拉跨了。首先,如果我想编译整个、最新的OpenCV,我应该选择哪个软件包?带着这样的问题,你把如下关键词输入了谷歌的搜索框:"vcpkg install OpenCV ",你一定会为从灵魂深处感到气愤:OpenCV在Windows下的编译指南里,只字未提“vcpkg”的字样;反而是在General Install上介绍了8种不同的包管理安装openCV的方式,这算是什么?明明Windows下编译OpenCV最方便省力的方法就是通过vcpkg,为什么这么好的包管理安装方法只在General页面上提一嘴,并且在Windows专门的编译页面里还用这个陈旧复杂的CMake方式……感觉对OpenCV文档编写组的吐槽永远也说不完。

好吧,你终于抵达了目标:openCV的vcpkg info,然后你看到了openCV这个包支持的所有feature。在Windwos下编译整个openCV需要打开哪些呢?为了保险起见,你还是决定搜索一下,结果终于在一篇博客里看到了完整的命令: /vcpkg install opencv4[contrib,core,cuda,dnn,eigen,ffmpeg,jpeg,nonfree,opengl,png,qt,tiff,vtk,webp]

那么接下来就只有等待了。实际上,vcpkg的编译速度是非常快的,因为vcpkg在我这台电脑上默认的构建工具是ninja,而ninja不同于蜗牛速度的Visual Studio直接构建,它可以最大化利用电脑的总性能,竭尽全力地压榨CPU,争取以最快的速度完成构建任务。你看,Windows自己的包管理都不使用自己的Visual Studio的工具链,这说明了什么?()

经过漫长的等待,漫长的等待,好吧,一共需要编译72个target,我等了整整半天,才最终完成编译。我的笔记本实在是不堪重负,CPU核心的温度一度飙升到了90。不过结果还是好的,没有什么大问题 ,顺利完成了编译。

编译需要注意这两点:

  1. 不得不说,一定要用固态硬盘保存vcpkg的工作环境,性能的提升不是一星半点。
  2. 一定要预留出足够的硬盘空间。vcpkg在编译安装软件的过程中会下载大量的源码包,这些东西非常占用硬盘空间。而且vcpkg默认把你的工作目录做为下载缓存,不会主动删除已经下载的包,这就导致了硬盘空间会在安装过程中会以肉眼可见的速度减少,而这需要引起你的警惕,作为系统盘的固态硬盘的空间是十分宝贵的,直接影响系统的总体性能,整个安装大概需要40G左右,一定要提前预留出足够的空间。
  3. 用以上命令安装的软件,包括了debug和release两个版本。debug版本的软件比release大一些。整个安装完成之后,你可以在root\vcpkg\installed\x64-windows文件夹下找到装好的软件。
  4. 在整个安装完成之后,可以把vcpkg的缓存文件夹(buildtrees和downloads)清空,不会影响软件的使用。唯一的问题就是,如果你用vcpkg安装其他的用到这些包的软件,那么vcpkg还需要重新下载这些源码包。其次就是buildtrees里包含一些日志信息,如果安装顺利完成不需要可以直接删除。
  5. vcpkg在安装软件的过程中需要和一些在中国大陆直接访问速度很慢的网站进行通信(rawgithubcontent),vcpkg支持系统代理,别忘了选择一个相对稳定的代理工具。

好了,安装顺利完成。不需要过多的校验,很快就可以体验到openCV的强大功能啦!

coding with OpenCV C++

C++是一个很nb的语言,我就不需要多说了。本来以为C++版本的OpenCV在使用上会更加麻烦,但是事实证明:我多虑了。OpenCV C++延续了Python一贯的传统,不但写起来依旧浅显易懂,而且性能提高了很多,最重要的是,可以和CUDA在一起使用了! 所以,我并不建议使用Python来写OpenCV,因为用C++来开发的成本并不会比Python高很多,反而会获得极大的性能收益,我觉得这是一个很赚的事情。因此我们接下来就着重讲如何用openCV C++来编写一个基于GPU的、能够实时检测开门的程序。

首先,当你编译完成之后,你可以选择先清理一下C盘里vcpkg的临时文件,来释放一些宝贵的硬盘空间。然后你需要执行 vcpkg integrate install,这个命令会让vcpkg和Visual Studio集成起来,使得可以直接从Visual Studio里使用来自vcpkg的库。然后,打开Visual Studio,新建一个C++控制台项目。

你可以输入 #include <opencv2/opencv.hpp>来检查是否安装顺利。不过我觉得不应该不顺利,毕竟都已经自动化到这种程度了(笑),如果没有报错,那就恭喜你编译成功了。我们可以正式进行程序的开发了。

在正式开始openCV的C++编程之前,我觉得有必要为你介绍一些前置知识。如果你之前没用过openCV,或者只用过OpenCV Python,那么有一些问题是你在开发程序的过程中考虑不到的。而在事无巨细的C++里,如果不考虑到这些点,会遇到十分诡异的bug,这里需要引起注意。

OpenCV的数据类型

如果你用Python,那么你大概率不会注意到OpenCV的数据类型。因为Python不需要你关心一个数到底是整数还是浮点数,是8位的还是32位的,也不需要你关心图像是3个通道还是1个通道。但是事实证明,不关心这些会引起性能的下降,因为这就意味着Python不得不为你考虑好这些事情。频繁的类型转换可能会降低性能,所以有必要先搞清楚OpenCV里的数据类型。

OpenCV的基本数据类型有如下几种,每种类型都有自己的 名字。注意, 名字 不同于类型,类型的名字本质上是一个宏定义。但在定义类型的时候,你需要使用类型来定义。注意对类型和类型的名字加以区分。 OpenCV最小的数据单位是各种数,有整数、浮点数、有符号、无符号等等,这些数据的名字是CV_8U 8S 16U 16S 32S 32F 64F。U表示无符号整数,S表示有符号整数,F表示浮点数。 虽然这些类型的名字有7种,但是实际用到的基本类型只有5种,是uchar(8U) short(16S) int(32S) float(32F) double(64F)

实际使用的时候,视通道数量确定自己使用的数据类型。Vec2b表示由2个Byte(uchar)组成的短数组,类似的类型还有Vec2/3/4|b/s/i/f/d,以及Vec6f和Vec6d。末尾的字母bsifd分别对应uchar/short/int/float/double五个类型。

OpenCV的窗口与图形显示

还有一个需要注意的地方,就是OpenCV的图形显示。如果你需要在开发过程中展示一个Mat里的图像,无论是展示到屏幕上,还是保存成图像文件到硬盘里,都需要用到qt库来进行GUI操作。之前我曾经用CMake在脱离qt库的情况下完成了一次编译,也算是编译成功了,但是因为缺少Qt库,导致cv::imshow函数不能正常链接(就是库里缺少这个符号),但有意思的是,imread函数是正常的,imwrite是不能使用的,可能是因为我在编译的时候还缺少了其他必要的图像编码库?总之如果你是通过编译安装了完整的OpenCV,不太可能遇到这个问题。

虽然你很可能知道这个坑,但是我还是要不厌其烦的提醒你一下(Neboer你是老太婆吗):在C++版本中,OpenCV的imshow需要一个窗口。你需要提前创建一个窗口才能展示图片。在OpenCV中并没有窗口这个元素,OpenCV关心的只是窗口的名字。也就是说,在操作不同的窗口的时候,你需要创造、传入不同的窗口名(也就是一个字符串!)来指定你要操作的窗口。这个操作和大多数的图形化编程的思路都不太一样,需要你转过弯来。

另外,另外,OpenCV的imshow并不会直接显示窗口。换句话说,OpenCV并不是靠imshow命令来绘制图像到屏幕上的。imshow必须用配套的waitKey或者pollKey才能完成绘制的任务。如果你需要不断更新图像里的内容,你需要把这两个函数之一添加到循环体中。如果你忘记了对应的wait/poll,你会看到一个灰白的窗口,然后鼠标在上面转圈。

OpenCV是支持滑动条的!

OpenCV开发的过程并不是一帆风顺的,OpenCV作为一个计算机 视觉库,只是负责给计算机带来“视 ”,并不负责让计算机思考。实际上需要思考的是你,所以在开发的过程中,经常会遇到一些关于参数的问题。调参是一个很痛苦的事情,因为谁也不知道最合适的参数是多少,所以需要不断的改变相关数值的大小,然后实时观测结果,这就需要一个能够灵活改变参数的方式。你第一想到的肯定是命令行,但是OpenCV拥有更高级的图形方法来解决问题——Slider。

通过 cv2.createTrackbar('slider', "window_name", 0, 100, on_change),你可以在window_name这个窗口上创建一个Trackbar。这个范围可以任意调整,一旦Trackbar的值改变了,on_change函数就会被调用,同时被传入一些参数,你可以在那个函数里操作你需要修改的值,实在是非常方便。

OpenCV C++的debug是比较痛苦的

OpenCV C++毕竟是一个C++库,最大的问题就是不易debug。比如如果你在onClick的回调里写了一个有问题的代码,那么你将在运行程序的时候会发现在waitKey函数处抛出了异常,C++ debugger并不会准确识别代码里异常抛出的位置。其实这个也十分易懂:毕竟真正在执行你的回调的是waitKey函数,实际抛出问题的地方也是那里。回调问题只是诸多不易debug的原因之一,此外还有可能出现的谜之段错误、类型不匹配错误等等,甚至一个逻辑错误都需要你debug半天。C++确实是一个不易于debug的语言,所以在实际开发的过程中需要你有一定的C++经验。最起码不能是hello world水平。而且在C++编程的过程中,要时刻注意遵守C++ best practice,比如(从我自身的经验出发写一些注意事项,无来源,不一定正确,仅供参考)

  • 减少指针的使用
  • 尽量用stl(现代debugger对stl支持得都很不错)
  • 时刻留意变量的生命周期
  • 能用栈解决的问题就不要用堆(小心泄露)
  • 使用引用减少不必要的内存拷贝(尤其是在操作指针的时候,不拷贝这个性质很重要)
  • 减少使用名字空间防止污染命名环境(这个很重要,不要总是using namespace,这句话的副作用比你想象的多得多)
  • 使用结构体来代替不必要的类的定义。只有明确需要复杂的结构的时候,再面向对象(笑)
  • 其他Visual C++的注意事项,比如用printf_s代替printf之类,还有就是注意wchar的处理和char的区别等等(Windows只有在你开发的时候才会如此重视utf8(笑))
  • 使用C++的最新规范(向先进设计思路学习嘛,不过也要注意MSVC对C++不同版本的支持情况)

等等。只要你小心谨慎,bug一般不会找上门。而且Visual Studio对C++的debug体验还是不错的,遇到问题一般可以很快的解决。C++是一个非常复杂的语言,但只要尊重客观事实,保持谦虚态度,你的C++水平可以在实践中得到长足的进步。进而反过来让你写出更好的C++程序。

OpenCV的文档质量是很不错的

实际开发的时候,只需要看手册就好。openCV官方的文档还是比较全面的。许多关键函数等的定义、使用,每个参数的意义等等都可以在官方文档里清楚的查到,但是……

但是,GPU部分除外。

OpenCV CUDA:无人区

好,现在你已经用OpenCV写出了一个不错的hello world,并且运行成功,屏幕上显示的内容让你十分高兴,于是你怀着激动的心情,开始了OpenCV CUDA之旅。

你很快就发现这趟旅途并不顺利,因为你发现OpenCV根本没有好好做cuda方面的文档,里面的内容支离破碎,相关的描述含糊不清,很多概念缺乏例子,上来就是API文档,让人摸不着头脑。

这个现象的原因是可以理解的。因为OpenCV一开始并不支持cuda,OpenCV的对OpenCL/cuda等的支持都是分散在社区里各处的志愿者维护的。这些缺乏组织的代码虽然没有什么质量问题,但是没有形成良好的文档,主要还是没人贡献。

那是不是就意味着,OpenCV CUDA不能用呢?当然不是,OpenCV热衷于把经典的算法用cuda去实现,现在cuda支持的操作非常多,OpenCV中几乎所有的CPU算法都可以找到对应的cuda替代版本,但是二者不一定在使用上完全相同。这就给开发工作带来了一定的困难——但只是一定困难,又不是不可能,我们都挺过了编译OpenCV的过程,为什么在这里反而停滞不前了(),所以我在这里给出一些相对来说可以提供参考的地方,帮助你快速掌握OpenCV cuda。

  • OpenCV官方文档。虽然OpenCV官方文档在GPU上描述得含糊不清,但这依旧是你能接触到的最准确的说明类文字。就算上面什么也没写,你也可以看看都有哪些API可以用。然后根据API的命名可以对这些API的用法进行一个大概的猜测,结合其他的实验或者例子,可以迅速掌握它的用法。
  • Github。在OpenCV官方的Github中,OpenCV相关的开发者编写了一些(其实并不多)例子,其中就有一组例子专门讲GPU编程的。这些例子覆盖的范围很广,而且代码质量很高,完全可以拿来学习各种API的用法。可以起到辅助文档帮助你掌握OpenCV GPU编程的作用。
  • 还是Github。除了OpenCV官方,还有许多人在Github上分享自己使用OpenCV的方法,这其中就包括大量的示例代码。这些代码将会称为你学习OpenCV过程中重要的参考。但是对于其他的代码也必须保持一个警惕的态度,需要仔细研究它到底讲了什么内容,和你的研究目标是否一致。
  • Google 查询单独的一个API。这个不需要多说,如果你有一个API的用法不太了解,看文档、看示例都没有弄清,那么直接Google是最好的方法。因为如果文档里记载的不清楚,不仅仅只有你一个人会觉得有问题,很多人都会,所以有问题的人就会去Stackoverflow/Github issue之类的地方提问,这样你就有机会看到大佬是如何回答他的问题的,进而得到启示,结合本地的运行结果,弄懂这个API的用法。

这里我举几个例子,说明一下如何在缺乏文档描述的情况下,掌握OpenCV CUDA的几个API的用法。你可以通过我的研究过程学习到掌握这个模块的一般思路,从而更好的操作OpenCV。

简单:CannyEdgeDetect (cuda)

虽然最后我们程序里并没有用到CannyEdgeDetector,但是仅仅作为了解还是不错的。我们假设现在需要对已经仅剩灰度、经过降噪处理的一张图进行Canny边缘检测,来看看一般的研究流程:

  1. 明确需求,需要Canny边缘检测,首先你需要了解CPU版的Canny边缘检测是如何调用的,这个在网上有许多说明,了解起来相当容易,不再赘述。
  2. Google搜索“opencv cuda canny”
  3. 第一个结果: cv::cuda::CannyEdgeDetector Class Reference - OpenCV ...,这就是我们要找的,直接点击
  4. 你看到了“cv::cuda::CannyEdgeDetector Class Reference”,看到CannyEdgeDetector在cuda里竟然是一个abstract class!然后就开始迷茫:这是一个抽象类,里面定义了一些抽象方法,那么我真正可以实例化的类在哪里?这个detect方法看起来和CPU版的没什么不同,但是我应该如何调用呢?这个方法并不是静态的方法,所以我需要实例化一个实现了cv::cuda::CannyEdgeDetector的类,然后调用它的detect吗?但是抽象类也没有构造函数……
  5. 你完全搞不清楚,于是点开了下面的“opencv2/cudaimgproc.hpp”头文件,试图在头文件的定义里看到一些蛛丝马迹
  6. 你看到了 Ptr< CannyEdgeDetector > cv::cuda::createCannyEdgeDetector,于是你成功了,你一下就明白了OpenCV里cuda方法的逻辑:先用确定的参数create一个xxxor,然后再用这个xxxor类的detect之类的主方法去探测你待检测的图片,这样一来,对于参数相同的图像处理过程,一些重复性的工作可以移动到创建类的时候完成,不需要反复执行,提高了效率。
  7. 于是,你写出了如下的代码:
    auto CannyEdgeDetector = cv::cuda::createCannyEdgeDetector(10,100);
    while (true) {
     ...
     cv::cuda::GpuMat edges;
     CannyEdgeDetector.detect(input, edges);
     ...
    }
    
    恭喜,你成功掌握了CannyEdgeDetector(cuda)的用法。

意外:SobelFilter(cuda)

有了CannyEdgeDetector的经验,你很快就想当然地想到:对于Sobel,有cv::Sobel方法,那么在GPU里一定有一个createSobelDetector之类的方法了!然后你十分兴奋地在Google里输入:opencv cuda SobelDetector,然后回车: 等等,不对!为什么排名第一的是一个StackOverFlow的回答???点进去一看:完全没有用

  1. 发现Google搜索并没有想要的答案,瞬间慌了,因为你害怕OpenCV cuda不支持Sobel方法。
  2. 你调整了关键词,搜索“opencv cuda Sobel”,这次搜索的结果依然不令人满意。
  3. 你想到了之前在CannyEdgeDetect里,曾经看过一个叫做“opencv2/cudaimgproc.hpp”的头文件,现在它依然在你的include列表中静静地躺着。你点开了这个文件,在里面尝试全文搜索“sobel”,没有结果。
  4. 你更换不同的关键词进行搜索,尝试在github中找到更多的示例代码。你在github中搜索“opencv cuda sobel”之类的字样,终于得到了满意的答案:getting-started-with-cuda-opencv中有类似的代码。
  5. 你点开getting-started-with-cuda-opencv的例子: cv::Ptr<cv::cuda::Filter> sobelx,还有 sobelx : cv::cuda::createSobelFilter(CV_8UC1, CV_32FC1, 1, 0, 3, 1),你终于理解了自己在找什么。原来OpenCV的cuda模块把sobel视作一个filter对象。
  6. 一切都明朗起来了。你重新调整了搜索关键词,“createSobelFilter”,直接搜索。在第一个结果里你就看到了OpenCV的文档:“Image Filtering - CUDA-accelerated Computer Vision”
  7. 你点进去,然后看到了真正的Sobel方法:cv::cuda::createSobelFilter。但是等等,这个createSobelFilter的方法怎么和Sobel的CPU方法差这么多?原来在cuda版本的函数里需要多填入一个srcType和dstType,就是说需要你指定输入的图像和输出的图像分别是什么类型。根据我们前文提到的基础知识,你很快就明白了输入的灰度图像是CV_8U类型,然后输出也是CV_8U。事实上,这个操作是支持彩色通道图像的!所以你可以把输入类型调整成CV_8UC3,这也是你从摄像头中读取的一帧原始图像的存储方式。
  8. 最终你的代码变成了这个样子:
    auto sobel_filter = cv::cuda::createSobelFilter(CV_8UC3, CV_8U, 1, 0);
    ...
    sobel_filter->apply(g_buffer1, g_buffer2);
    cv::cuda::cvtColor(g_buffer2, g_buffer1, cv::COLOR_BGR2GRAY); // 实际上,经过转换的g_buffer_1并不是CV_8U类型的,所以需要转换一下。至于原因不是很清楚。
    
    恭喜,虽然经过了一些曲折,但你最终还是掌握了OpenCV cuda SobelFilter的用法。

照猫画虎:houghlines(cuda)

在第一版的程序里,我们曾经走过一段弯路:我们尝试用houghline来描述门上纸的四边的斜率和顶点位置。houghline函数和之前的函数的最大不同之处就在于:这个函数返回的是一个列表,而不是一个图像的集合。在CPU里,返回列表本身无可厚非,只要传给函数一个vector之类的对象就可以无障碍地获得执行结果了,参考CPU houghlines的文档中的内容。 其中关键的代码是:

vector<Vec2f> lines; // will hold the results of the detection
HoughLines(dst, lines, 1, CV_PI/180, 150, 0, 0 ); // runs the actual detection

可以看到,这个例子非常明确的把函数的输出导入到了一个空白的vector里。然后执行HoughLines方法,整个过程非常易懂。 但GPU呢?我们用相似的方法找到了GPU版本的文档,然后顺利的发现了 cv::cuda::createHoughLinesDetector 到目前为止都十分顺利。但是,这个 createHoughLinesDetector的文档中并没有什么例子来教你如何使用它!所以,OpenCV又一次让你抓瞎了,这怎么办?

为什么GPU和CPU如此不同呢?因为我们都知道,在CPU函数里,直接把一个vector传入函数中是没有任何问题的——因为都在内存上嘛!不过GPU的函数就不同了——GPU函数一定工作在GPU上,直接操作的只有显存,而你的Vector定义在内存上,自然不能直接传入GPU函数里。在OpenCV cuda里只有一种(常用)的GPU存储结构:GpuMat。而GpuMat又不是Vector,是不是要download成Vector之后再操作呢?你还真的这么操作了!然后遗憾的发现:直到你download这个结果到一个CPU的vector之后,才发现OutputArray lines和你想象中的并不一样:它并不是一个vector,而还是一个GpuMat!需要再次经过HoughLinesDetector的方法downloadResults的处理……好好,我知道你已经快睡着了。这些内容是在一个Stackoverflow回答里看到的,但是我尝试之后发现并不能使用(实际数组是空的),不知道为什么。这里缺乏文档,而且任何地方都找不到,工作一度陷入了绝境。

就好像vcpkg一样,有没有现成的例子(解决方案)呢?

当然。你打开了Github,看到了opencv/samples/gpu/houghlines.cpp。这可是救星啊! 等等,这个例子里用的不是houghlines,而是houghlinesP!这说明什么,说明写OpenCV的开发者也不愿意用houghlines的方法(草),所以你仔细观察他的代码,终于学会了houghlinesP的正确方法:createHoughSegmentDetector

你在Google上查询houghlinesP和houghlines有什么区别,你终于知道了:houghlinesP可以直接返回目标的两点,而houghlines只能返回直线的k和b,这些直线无限延伸,并没有端点信息。 既然houghlinesP更优秀,那么就用createHoughSegmentDetector吧!参考代码:

GpuMat d_src(mask);
GpuMat d_lines;
Ptr<cuda::HoughSegmentDetector> hough = cuda::createHoughSegmentDetector(1.0f, (float) (CV_PI / 180.0f), 50, 5);
hough->detect(d_src, d_lines);
vector<Vec4i> lines_gpu;
if (!d_lines.empty())
{
    lines_gpu.resize(d_lines.cols);
    Mat h_lines(1, d_lines.cols, CV_32SC4, &lines_gpu[0]);
    d_lines.download(h_lines);
}
// 至此 lines_gpu就是我们想要得到的线的目标数组了。只需要遍历这个数组,就可以得到所有检测出来的 **线段** 了。

很好。在经过了上面的3个例子的洗礼之后,我觉得你对OpenCV cuda的编程过程会更多一些理解。这就足够了。反复进行学习-编程的过程,最终我们就可以把我们一直想要的 开门检测程序 给做出来了。

最终的程序

当一切尘埃落定,你开始着手编写这个“科技改变生活”的程序了。 实际的流程如下:

  1. 准备阶段:
    1. 打开摄像头
    2. 展示画面,让用户标出剪切范围
    3. 准备SobelFilter。
  2. 主循环

    获取边缘的方法:

     1. 对画面进行裁剪
     2. FastNlDenoise彩色降噪
     3. SobelFilter检测边缘
     4. cvtColor转换为单通道
    
    1. 采集一些数据:反复读取纸张的左边缘边界,取所有边界中亮点的交集,作为基本识别依据
    2. 开始侦测!反复从摄像头里读取画面并获取边缘,与基本识别依据进行比对(取交)。一旦发现匹配的像素少于规定值,就提出报警。
    3. 一旦连续报警次数超过临界值,说明门真的被推开了!这个时候需要进行动作。这里执行的动作是模拟键盘按下CtrlWin+左键,快速切换桌面到左侧。
    4. 切换桌面后,程序继续保持运行,用stdin阻塞,以防假警报等造成不方便。用户可以随时回车解除这个等待,让程序重新采样,继续运行。

然后对程序进行一些必要的性能测试

  1. 运行程序,对程序的性能进行分析:程序在运行中,对于检测22*82=1804个像素点的区域,程序的fps可以保持在28.61(摄像头fps是30左右),占用CPU约5%(八代i7),占用内存320MB,占用GPU不足2%,占用显存不足300MB。
  2. 在运行大型游戏(好吧,原神)的时候,程序的fps稳定在22左右,由于程序对显存进行大量读写,所以导致加载地图等需要大量读写显存的操作变得缓慢,不过也不是不可接受的速度。程序本身不消耗过多GPU运算资源,所以不会影响游戏的fps。
  3. 即使游戏死机,主窗口卡住等关键情况出现,程序也可以完美切换桌面,因为切换桌面并不涉及到窗口的加载卸载,不同桌面用不同的窗口环境,切换的速度很快。
  4. 进行实际的测试:运行游戏,运行程序,提前告知程序需要裁剪的区域,然后离开房间。在等待足够长时间后,猛然推开门,从记录的日志可以看到程序在100ms内快速完成了切换桌面的动作。如果提高程序的灵敏度或者环境光线强度,实际响应速度还会更快。
  5. 程序经过大量的测试,在各种光线条件下都可以保证100ms以内的响应时间。在运行大型游戏的时候也不会造成检测精度的丢失。如果担心大型游戏占用太多系统资源,导致程序不能实时检测开门动作,可以考虑提高程序的优先级,优先保证检测程序的fps。

程序为什么这么快:

  1. 所有的矩阵变换操作完全在GPU中完成,就连“匹配像素点”这种操作也可以直接调用OpenCV cuda函数来完成,执行的效率奇高,完美利用了cuda多核心的优势。
  2. 由于整个程序不存在从显卡下载数据的过程(仅有开始需要上传裁剪后的图像到显存),上传图像后所有的操作都在显卡内部完成,CPU仅仅需要知道一个值就是比对结果(有多少相同的像素),已经最大化的降低了内存拷贝带来的大量延时。
  3. 程序使用C++编写,使用MSVC套件进行编译,整个过程符合Windows高性能程序开发规范,在主循环内部没有额外的操作,保证高效运行。

我是如何测试程序的反应时间的:

首先我们先明确一个事情:程序的反应时间应该是从门被推动到“准备切换桌面”的时间。这段时间被我称为“反应时间”,而从推门到看到电脑屏幕的最短时间被我称为“允许反应时间”。在实际场景中,允许反应时间大约在200ms以内,也就是说,程序必须在有人推门200ms内做出反应,实际的反应的时间还应该低于这个数字,因为切换桌面需要的时间是固定的。程序必须保证在200ms过去后,切换桌面的过程应该已经完成大半。

实际测试程序的反应速度,需要保证我们有一个和电脑时间同步的手持设备(智能手机)。我这里用到了atom clock这个应用程序。在电脑上开启ntp服务,然后用家用局域网连接手机和电脑,在atom clock里与电脑同步ntp时间,然后设置完成!设置一个倒计时,定时开启这个检测程序,然后马上退出房间,等待一段时间程序启动后,立刻推门。推门的同时,另一只手按下atom clock的暂停按钮,然后看2件事:1. 能否明显看到电脑的屏幕发生变化,如果不能,说明反应速度已经很快;2. 回到电脑附近的时候,查看软件的日志,看软件打印的切换桌面的发起时间和实际推门的时间相差了多少。用这个方法可以以毫秒级精度检测出软件的反应时间,这对于Windows操作系统和这个开门检测的需求来说,已经足够了。当然,在进行测试的时候不要让别人看到,要不然解释起来很麻烦(2333)。

后记

其实这个程序的编写全过程真的超乎了我的想象。起初我还觉得一切不能这么顺利,无论是C++,还是MSVC,还是OpenCV,都是我很久没有接触过的东西了。这次开发的经历虽然没什么特别大的技术含量,但是让我重新认识了C++这个古老又强大的语言,同时让我对MSVC的Visual C++有了更加深刻的认识。可以说是Windows开发入门之旅了。微软的东西就是让人又爱又恨,和Visual Studio斗智斗勇的过程,也是只有Windows开发才能体会到的独特的体验了。

很多人会问我为什么选择Windows,其实主要的问题还是Linux上没有游戏可以玩。你想想,在Linux下开发一个这样的程序肯定会更加容易,但是有什么意义呢?一方面Linux又没有许多游戏可以玩,另一方面Linux的图形性能真的不如Windows(当然我不知道cuda性能怎么样,不过我觉得在这一点,二者的差距不会太大)。这个程序凝结了我这半个月以来的探索和实践,中间经过了比文章描述得更多的挫折,限于篇幅,不能全部描述,还请较真的读者们见谅。Neboer会努力提升自己,为大家带来更多的好文章的。

为什么不用wsl?麻烦,而且wsl的硬盘也需要占用大量宝贵的固态硬盘空间。

最后,根据Neboer一贯以来的传统,肯定是要落到“自由开放的OpenCV社区”的。其实我也不用多说了,OpenCV是一个有多么优秀的计算机视觉库——OpenCV里所有强大的算法几乎都是源自相应的论文、专利,然后不同的开发者在各种平台上去实现这些算法,还把这个强大的功能port到了Python环境中,为更多不熟悉C++编程的用户带来了高性能的算法库。OpenCV的cuda部分更是完全的社区贡献,虽然在文档方面还需要进步,但是在对cuda的使用本身已经没有任何的问题了,可以完美的榨干显卡的每一块cuda核心的性能,未来可能还会有更多的算法(比如边界检测等)被移植到cuda环境中,只能说未来很美好,让我们拭目以待了。虽然国内的大环境稍微有点卷,导致一提到“OpenCV”很多人想到的可能是寻线小车等比赛中使用的嵌入式技术,但是我要说的是一个优秀的库从来都是有广泛应用场景的,寻线小车当然很厉害,但是在这里用它来检测开门,不也是很好的一种应用环境吗。

感谢所有致力于开源的人们,是你们的努力让这个世界更美好。虽然我对Intel的牙膏没有很大的好感,但是真的很感谢他们开发了OpenCV。希望未来有更多的优秀算法开源出来,让更多计算机系统看到这个绚丽多彩的世界。