65.9K
CodeProject 正在变化。 阅读更多。
Home

计算机视觉沙箱

starIconstarIconstarIconstarIconstarIcon

5.00/5 (20投票s)

2019年4月4日

GPL3

26分钟阅读

viewsIcon

25120

一个开源应用程序,用于采集和处理来自摄像头的视频

Computer Vision Sandbox

目录

引言

计算机视觉沙盒是一个开源软件包,旨在解决计算机视觉领域的各种问题,例如视频监控、基于视觉的自动化/机器人技术、各种图像/视频处理等。最初,该项目以封闭代码形式启动,花了一些时间来确定其核心架构并开发一套功能,使其能够应用于各种任务。然而,从2.0版本开始,该项目迁移到开源仓库,使其代码对公众开放。

从一开始,该项目就旨在提供高度模块化,并通过开发不同的插件来扩展其功能。主应用程序本身用途不大。它知道如何加载插件并将它们组合在一起以进行一些视频处理。但是,如果缺少这些插件,除了打开/关闭“关于”框之外,几乎没有什么可做的。不过,这不应该是问题,因为官方安装包附带了各种插件,可以将其应用于许多任务。

计算机视觉沙盒中的插件是核心思想,并提供了其大部分功能。这些就像积木一样——你得到的最终结果取决于选择了哪些积木以及如何组合它们。你可能会问,有哪些类型的插件呢?嗯,有几种不同的类型。第一种也是主要类型的插件是视频源——它们生成要处理的视频。视频可以来自USB摄像头(或笔记本电脑的集成摄像头),例如,来自IP视频监控摄像头,来自视频文件或任何其他提供连续图像(视频帧)的源。除此之外,还有不同类型的插件用于图像和视频处理。它可以是图像增强、添加视觉效果、检测特定对象、保存视频存档等。为了让事情变得更有趣并允许更高级的视频处理,有一个脚本插件,允许使用Lua编程语言编写视频处理脚本。最后,还有一些设备插件,允许与一些I/O板、机器人控制器、通过串行端口连接的设备等进行通信——这允许开发更具交互性的应用程序,其中视频处理可以受到现实世界事件的影响,反之亦然。

该项目的代码库已经发展到,现在在一篇文章中描述其所有细节将是一项相当大的工作。因此,本文将只关注几个关键概念和功能,以演示一些可能的用例。为了提供一些想法/方向,这里有一个截图展示了一些应用程序。这些更多地属于计算机视觉方面。但其他领域也将进一步描述。

Computer vision examples

项目特性

当计算机视觉沙盒项目出现时,其想法是制作一种乐高积木拼图。它不只针对视频监控,也不只针对添加不同的图像/视频效果,也不只针对纯粹的计算机视觉应用程序,也不只针对机器人/自动化任务等。相反,它针对上述所有内容及更多。其想法是将每个功能都作为插件,然后让用户将它们组合起来以获得他们想要的任何结果。主应用程序对任何特定的摄像头(视频源)或图像/视频处理例程一无所知。它只知道某些类型的插件。例如,一些插件提供视频,另一些提供图像处理,还有一些可能提供脚本功能等。但实际提供了什么,以及如何提供,纯粹取决于插件。我们从主应用程序所需要的只是知道如何与这些插件进行通信。

在后续章节中,我们将回顾主要的插件类型以及如何在计算机视觉沙盒应用程序中使用它们。

视频源

视频源插件是计算机视觉沙盒的基础。你可能有任意数量的其他插件,但如果没有视频源——就无法完成任何其他操作。什么是视频源?任何持续生成图像的东西。它可以是通过USB或IP接口连接的摄像头,可以是视频文件,屏幕捕获设备,文件夹中的图像文件集合等。主应用程序不关心图像来自何处,只要它们出现即可。

Video source plug-ins

添加视频源时,需要选择一个插件来与特定设备进行通信,然后配置其属性。属性列表特定于所选的插件类型——它可以是摄像头的IP地址、MJPEG流的URL、USB连接摄像头的名称/ID等。配置完成后,视频源即可打开并观看。

Video source properties

配置好多个视频源并确保它们都按预期工作后,我们可以创建一个沙盒,在一个视图中显示多达16个摄像头。任何类型的视频源都可以放入沙盒中,这样不同品牌和型号的摄像头就可以一起打开。

Cameras view

摄像头的视图不需要规则的网格结构。如果想让其中一个单元格比其他单元格更大,我们可以合并几个单元格以创建一个更大的单元格。这允许创建各种形状的视图,并将更大的单元格分配给可能显示更有趣/重要内容的摄像头。

Cameras view

最后,一个沙盒可以定义多个视图,然后可以手动或以一定时间间隔切换这些视图。例如,我们可以有一个默认视图显示所有沙盒视频源,然后一些其他视图以更大尺寸显示特定视频源。

Cameras view

对于每个运行中的视频源(单独或在沙盒内),都可以进行快照,然后可以导出为图像文件或复制到剪贴板。

Camera snapshot

图像/视频处理

观看不同类型的摄像头很不错,但如果能对它们做些什么就更好了。例如,进行一些图像增强/处理,添加不同的效果,实现一些计算机视觉应用程序,将视频保存到文件等。这就是图像和视频处理插件发挥作用的地方。

要为视频源添加视频处理步骤,需要像组合多个视频源到一个视图中一样,将其放入沙盒中。因此,沙盒代表了一种容器,可以运行多个视频源,执行不同的图像/视频处理例程,运行脚本(稍后会详细介绍)等。

完成沙盒的初始配置后,可以通过运行沙盒向导,将视频处理步骤添加到其视频源(摄像头)中,该向导提供可用作视频处理步骤的插件列表。例如,下面的屏幕截图显示了5个图像处理插件作为所选摄像头的视频处理步骤。这些处理步骤共同创造了老式照片的效果——图像首先变成棕褐色(棕色渐变),然后添加垂直纹理噪声,再添加晕影效果使图像边缘变暗,最后添加两个边框:一个模糊边框和一个圆角边框。

Video processing steps

运行与上述配置类似的沙盒,可能会得到如下图所示的结果(前提是插件配置得当)

Video processing steps

在某些情况下,检查配置的视频处理图的性能和/或更改其中某些步骤的属性可能很有用。这可以通过运行摄像头上下文菜单中的视频处理信息表单来完成。此表单显示每个视频处理步骤花费的平均时间以及该图花费的总时间的百分比。此信息可能有助于对配置的图进行故障排除,找出哪些步骤占用大部分CPU时间,并可能导致视频处理延迟(我们通常不希望图的总时间大于视频源的帧间隔时间)。同一个表单还允许更改图像处理插件的属性(如果它们有),并查看其对运行中的视频的影响。

Video processing steps

除了图像处理插件,还有一些视频处理插件可以放入视频处理图中。不过,这两种插件之间的区别可能有点微妙。图像处理插件通常旨在获取输入图像并应用一些图像处理例程,从而改变图像(或提供新图像作为结果)。这些插件通常除了配置的属性外没有状态,并且大部分时间都对所有图像应用相同的例程。相反,视频处理插件可能具有一些内部状态,这可能会影响图像的处理方式。此外,这些插件可能更改或不更改源图像——这取决于插件实现的功能。

视频处理插件的一个很好的例子是视频文件写入器插件,它将传入的图像写入视频文件。此插件可以将所有图像写入单个文件,也可以配置为将视频分割成一定长度(以分钟为单位)的片段。除此之外,它还可以监控目标文件夹的大小并清理旧文件,从而创建视频存档。例如,以下配置告诉视频写入插件将图像写入以“街景”为前缀、以时间戳为后缀的文件。每个视频文件应为10分钟长。目标文件夹的大小不应超过10000Mb(约10Gb)。以这种方式运行带有视频写入插件的沙盒将确保我们始终为我们的摄像头提供10Gb的视频存档。

Video writer properties

另一个值得一提的视频处理插件是图像文件夹写入器插件。与上面提到的视频写入插件不同,该插件以JPEG或PNG文件格式将单个图像保存到指定的文件夹中,并以配置的时间间隔保存。这可以用于创建延时图像。例如,下面的配置将使插件每5秒(5000毫秒)写入一张图像,并忽略/跳过所有其他图像。

Image writer properties

虚拟视频源

尽管我们已经描述了一些视频源插件,但仍然值得一提的是它们的一个子类别,该子类别专门用于虚拟视频源。子类别不会引入新类型的插件。我们仍然处理视频源,它们使用与任何其他此类型插件相同的接口提供新图像。此子类别更多地用于在UI中对类似插件进行分组等。其想法是有一种方法来区分处理由某些摄像头/设备生成的视频源的插件,以及处理文件、图像等“虚拟”视频源的插件。

在此类别中首先要提及的插件是图像文件夹视频源。如上所述,使用图像文件夹写入器插件,我们可以在特定时间间隔保存来自某个视频源的图像。结果,我们得到了一个充满时间戳图像的文件夹。现在,假设我们可能想以不同的间隔播放它们。例如,我们以10秒的间隔保存了一系列延时图像,但之后我们想以每秒30帧的速度播放它们。这就是图像文件夹视频源的作用。配置此插件时,需要指定包含图像文件的源文件夹和帧之间所需的时间间隔。然后,插件将从该文件夹中读取图像文件,并像它们来自某个摄像头一样提供它们作为视频帧。为此视频源添加视频文件写入器作为视频处理步骤将允许将所有这些图像拼接成一个视频文件。现在,这将为我们提供一个适当的延时视频文件!

另一个值得一提的虚拟视频源插件是视频中继器。如果使用得当,这是一个非常有用的插件。所有插件所做的只是简单地重复/转发推入其中的图像。将其与视频中继器推送插件一起使用,可以将视频处理链拆分为多个分支,这些分支可以对原始视频帧应用不同的视频处理步骤。让我们看看如何使用此插件。

首先,我们需要使用视频中继器插件配置几个视频源。配置这些时,需要指定中继器ID——一个唯一的标识符,稍后视频中继器推送插件将使用它来链接视频源。单独打开这些视频源不会产生任何结果,只会显示“正在等待视频源...”消息。这没关系,因为我们还没有任何东西将图像推送到这些源中。下一步是配置一个沙盒,其中包含来自某个摄像头的视频源和几个中继器,例如三个。

Video repeater sandbox configuration

现在,使用沙盒向导,我们将三个视频中继器推送插件作为摄像头视频源的视频处理步骤,每个插件都配置有我们之前在配置视频中继器插件时使用的不同ID。

Video repeater pushing

以这种方式配置的沙盒打开后将显示四个视频源,它们显示完全相同的视频——一个来自摄像头,另外三个转发该视频。但是,由于计算机视觉沙盒将它们视为独立的视频源,我们可以在所有四个视频源上应用我们喜欢的任何视频处理。例如,下面的屏幕截图演示了一个运行中的沙盒,左上方显示了原始摄像头,然后是三个中继器,它们应用不同的视频处理步骤以获得不同的效果。

Video repeater pushing

上面的例子演示了如何利用视频中继器插件的概念实现视频处理分支。但这并非其唯一用途。假设我们为一个视频源配置了一个冗长的视频处理链,包含多个步骤。如果我们在这些步骤之间插入视频中继器推送插件,并将几个视频中继器放入沙盒中,我们就能够看到所执行视频处理的中间结果。在这个用例中,我们只使用视频中继器进行显示,而不是在其之上运行视频处理。这对于调试通过脚本完成的更复杂视频处理将特别有用。

最后,视频中继器可以用来解决处理繁重视频处理链时的一些性能问题。假设我们有一个视频源以每秒30帧的速度提供图像。还假设我们设置了许多视频处理步骤,这些步骤加起来所花费的时间超过了传入帧之间的时间间隔(约33毫秒)。这可能不是我们想要的配置,因为由于耗时的视频处理,最终帧率会下降。解决这个问题的方法是只在原始视频上进行一半的视频处理,然后将目前为止处理完的内容推送到视频中继器,并将视频处理链的其余部分放在那里,这将在不同的线程上运行,因此不会阻塞原始视频源。这样,性能问题就解决了,我们得到了我们想要的帧率!请记住使用前面提到的视频处理信息表单来查找潜在的视频处理瓶颈。

最后要提及的虚拟视频源是屏幕捕获插件。顾名思义,该插件以特定速率捕获屏幕内容,并将其作为来自视频源的图像提供。它可以配置为捕获系统中可用的特定屏幕、特定大小的区域或具有特定标题的窗口,例如“画图”。

Screen capture

脚本插件

正如前面所演示的,将不同的图像/视频处理插件添加到视频源的视频处理图中,可以实现不同的图像效果等。然而,通过顺序预配置的视频处理图可以完成的工作存在一定的限制。它不允许我们在沙盒运行时根据某些逻辑更改插件的配置。此外,它不允许实现更高级的视频处理,即分析图像并根据分析结果执行某些操作。

为了实现更高级的视频处理,计算机视觉沙盒提供了一个Lua脚本插件,使得使用Lua编程语言实现自定义逻辑成为可能。脚本插件可以像其他插件一样添加到视频处理图中,然后进行配置以指定要运行的脚本。

Screen capture

项目网站提供了关于Lua脚本插件提供的API的完整文档,以及许多涵盖不同用例的教程。在这里,我们将简要演示几个脚本示例,以提供一些可以完成的工作的思路。

首先,我们来看一个最简单的脚本,它使用着色图像处理插件来改变图像像素的色相和饱和度。如果该插件直接添加到视频处理图中,那么用户需要手动配置其属性。而且在沙盒运行期间,除非用户回来重新配置,否则这些属性不会改变。然而,使用脚本,我们可以根据我们想要的任何逻辑改变插件的属性。将下面的脚本作为视频处理步骤运行,将持续改变摄像头图像的色相值。

-- Create instance of Colorize plug-in
setHuePlugin = Host.CreatePluginInstance( 'Colorize' )
-- Set Saturation to maximum
setHuePlugin:SetProperty( 'saturation', 100 )
-- Start with Hue set to 0
hue = 0

-- Main function to be executed for every frame
function Main( )
    -- Get image to process
    image = Host.GetImage( )
    -- Set Hue value to set for the image
    setHuePlugin:SetProperty( 'hue', hue )
    -- Process the image
    setHuePlugin:ProcessImageInPlace( image )
    -- Move to the next Hue value
    hue = ( hue + 1 ) % 360
end

从上面的代码可以看出,脚本包含两个部分:全局部分和Main()函数。全局部分旨在执行所有必要的初始化,并且仅在沙盒启动时执行一次。然后,Main()函数为视频源生成的每个新帧执行。使用Host.GetImage() API,我们可以访问当前由视频处理图处理的图像,然后对其应用不同的图像处理例程。

一个稍微大一点的脚本使用了五个不同的插件来创建老电影的效果。之前已经演示了如何通过直接在视频处理图中使用这些插件来创建这种效果。但是现在,我们可能希望使其更具动态性,以便晕影和添加的噪点量在视频帧之间变化。

local math = require "math"

-- Create instances of plug-ins to use
sepiaPlugin      = Host.CreatePluginInstance( 'Sepia' )
vignettingPlugin = Host.CreatePluginInstance( 'Vignetting' )
grainPlugin      = Host.CreatePluginInstance( 'Grain' )
noisePlugin      = Host.CreatePluginInstance( 'UniformAdditiveNoise' )
borderPlugin     = Host.CreatePluginInstance( 'FuzzyBorder' )

-- Start values of some properties
vignettingStartFactor = 80
grainSpacing          = 40
noiseAmplitude        = 20

vignettingPlugin:SetProperty( 'decreaseSaturation', false )
vignettingPlugin:SetProperty( 'startFactor', vignettingStartFactor )
vignettingPlugin:SetProperty( 'endFactor', 150 )
grainPlugin:SetProperty( 'staticSeed', true )
grainPlugin:SetProperty( 'density', 0.5 )
borderPlugin:SetProperty( 'borderColor', '000000' )
borderPlugin:SetProperty( 'borderWidth', 32 )
borderPlugin:SetProperty( 'waviness', 8 )
borderPlugin:SetProperty( 'gradientWidth', 16 )

-- Other variables
seed    = 0
counter = 0

-- Main function to be executed for every frame
function Main( )
    -- Randomize some properties of the plug-ins in use
    RandomizeIt( )
    -- Get image to process
    image = Host.GetImage( )
    -- Apply image processing routines
    sepiaPlugin:ProcessImageInPlace( image )
    vignettingPlugin:ProcessImageInPlace( image )
    grainPlugin:ProcessImageInPlace( image )
    noisePlugin:ProcessImageInPlace( image )
    borderPlugin:ProcessImageInPlace( image )
end

-- Make sure the specified value is in the specified range
function CheckRange( value, min, max )
    if value < min then value = min end
    if value > max then value = max end
    return value
end

-- Modify plug-ins' properties randomly
function RandomizeIt( )
    -- change vignetting start factor
    vignettingStartFactor = CheckRange( vignettingStartFactor +
                                        math.random( 3 ) - 2, 60, 100 )
    vignettingPlugin:SetProperty( 'startFactor', vignettingStartFactor )
    -- change noise level
    noiseAmplitude = CheckRange( noiseAmplitude +
                                 math.random( 5 ) - 3, 10, 30 )
    noisePlugin:SetProperty( 'amplitude', noiseAmplitude )

    -- change grain every 5th frame
    counter = ( counter + 1 ) % 5
    if counter == 0 then
        -- grain's seed value
        seed = seed + 1
        grainPlugin:SetProperty( 'seedValue', seed )
        -- grain's spacing
        grainSpacing = CheckRange( grainSpacing + math.random( 5 ) - 3, 30, 50 )
    end
end

好了,图像效果就到此为止。让我们尝试一些不同的东西。例如,我们来尝试制作一个简单的运动检测器。下面的脚本使用差分图像阈值化插件来查找两个连续图像中相差一定量的像素数量。如果差异量高于某个阈值,则通过突出显示区域并在图像周围添加红色矩形来触发运动。该脚本的逻辑扩展将是在检测到运动时开始写入视频文件,而不是像之前演示的那样将所有内容保存到视频存档中。

-- Create instances of plug-ins to use
diffImages   = Host.CreatePluginInstance( 'DiffImagesThresholded' )
addImages    = Host.CreatePluginInstance( 'AddImages' )
imageDrawing = Host.CreatePluginInstance( 'ImageDrawing' )

-- Since we deal with RGB images, set threshold to 60 for the sum
-- of RGB differences
diffImages:SetProperty( 'threshold', 60 )
-- Highlight motion area with red color
diffImages:SetProperty( 'hiColor', 'FF0000' )

-- Amount of difference image to add to the source image for motion highlighting
addImages:SetProperty( 'factor', 0.3 )

-- Motion alarm threshold
motionThreshold = 0.1

-- Highlight motion areas or not
highlightMotion = true

function Main( )
    image = Host.GetImage( )

    if oldImage ~= nil then
        -- Calculate difference between current and the previous frames
        diff = diffImages:ProcessImage( image, oldImage )

        -- Set previous frame to the current one
        oldImage:Release( )
        oldImage = image:Clone( )

        -- Get the difference amount
        diffPixels  = diffImages:GetProperty( 'diffPixels' )
        diffPercent = diffPixels * 100 / ( image:Width( ) * image:Height( ) )

        -- Output the difference value
        imageDrawing:CallFunction( 'DrawText', image, tostring( diffPercent ),
                                   { 1, 1 }, 'FFFFFF', '00000000' )

        -- Check if alarm has to be raised
        if diffPercent > motionThreshold then
            imageDrawing:CallFunction( 'DrawRectangle', image,
                { 0, 0 }, { image:Width( ) - 1, image:Height( ) - 1 }, 'FF0000' )

            -- Highlight motion areas
            if highlightMotion then
                addImages:ProcessImageInPlace( image, diff )
            end
        end

        diff:Release( )
    else
        oldImage = image:Clone( )
    end
end

以下是检测到运动时可能的样子示例。

Motion detection

另一个有趣的例子是展示一个查找圆形物体的脚本。它使用了一个插件,该插件在图像中查找单个斑点(物体),并检查它们是否具有圆形形状。在进行斑点处理之前,我们需要进行分割——将背景与前景分离。在这种情况下,脚本使用简单的阈值技术。这限制了我们的图像必须具有均匀的深色背景和较亮的物体。这对于这个例子来说很好。

local math   = require "math"
local string = require "string"

-- Create instances of plug-ins to use
grayscalePlugin     = Host.CreatePluginInstance( 'Grayscale' )
thresholdPlugin     = Host.CreatePluginInstance( 'Threshold' )
circlesFilterPlugin = Host.CreatePluginInstance( 'FilterCircleBlobs' )
drawingPlugin       = Host.CreatePluginInstance( 'ImageDrawing' )

-- Set threshold to separate background and objects
thresholdPlugin:SetProperty( 'threshold', 64 )

-- Don't do image filtering, only collect information about circles
circlesFilterPlugin:SetProperty( 'filterImage', false )
-- Set minimum radius of circles to collect
circlesFilterPlugin:SetProperty( 'minRadius', 5 )

-- Color used for drawing
drawingColor = '00FF00'

function Main( )
    image = Host.GetImage( )

    -- Pre-process image by grayscaling and thresholding it
    grayImage = grayscalePlugin:ProcessImage( image )
    thresholdPlugin:ProcessImageInPlace( grayImage )

    -- Apply circles filter
    circlesFilterPlugin:ProcessImageInPlace( grayImage )

    circlesFound    = circlesFilterPlugin:GetProperty( 'circlesFound' )
    circlesCenters  = circlesFilterPlugin:GetProperty( 'circlesCenters' )
    circlesRadiuses = circlesFilterPlugin:GetProperty( 'circlesRadiuses' )

    -- Tell how many circles are detected
    drawingPlugin:CallFunction( 'DrawText', image, 'Circles: ' .. tostring( circlesFound ),
                                { 5, 5 }, drawingColor, '00000000' )

    -- Highlight each detected circle
    for i = 1, circlesFound do
        center = { math.floor( circlesCenters[i][1] ), math.floor( circlesCenters[i][2] ) }
        radius = math.floor( circlesRadiuses[i] )
        dist   = math.floor( math.sqrt( radius * radius / 2 ) )

        lineStart = { center[1] + radius, center[2] - radius }
        lineEnd   = { center[1] + dist, center[2] - dist }

        drawingPlugin:CallFunction( 'FillRing', image, center, radius + 2, radius, drawingColor )

        drawingPlugin:CallFunction( 'DrawLine', image, lineStart, lineEnd, drawingColor )
        drawingPlugin:CallFunction( 'DrawLine', image, lineStart,
                                    { lineStart[1] + 20, lineStart[2] }, drawingColor )

        -- Tell radius of the circle
        drawingPlugin:CallFunction( 'DrawText', image, tostring( radius ),
                                    { lineStart[1] + 2, lineStart[2] - 12 },
                                    drawingColor, '00000000' )
    end

    grayImage:Release( )
end

Circles detection

最后一个脚本示例展示了如何使用图像导出插件并将延时图像写入实现为Lua脚本。是的,我们已经演示了如何在不需要脚本的情况下完成此操作——我们有一个专门的插件可以直接放入视频处理图中。但是,如果需要自定义图像保存逻辑,它仍然可以使用。

local os = require "os"

-- Folder to write images to
folder = 'C:\\Temp\\images\\'

-- Create instance of plug-in for saving images
imageWriter = Host.CreatePluginInstance( 'PngExporter' )
--imageWriter = Host.CreatePluginInstance( 'JpegExporter' )
--imageWriter:SetProperty( 'quality', 100 )
ext = '.' .. imageWriter:SupportedExtensions( )[1]

-- Interval between images in seconds
imageInterval = 10
lastClock     = -imageInterval

function Main( )
    image = Host.GetImage( )

    -- Get number of seconds the application is running
    now = os.clock( )

    if now - lastClock >= imageInterval then
        lastClock = now
        SaveImage( image )
    end
end

-- Save image to file
function SaveImage( image )
    dateTime = os.date( '%Y-%m-%d %H-%M-%S' )
    fileName = folder .. dateTime .. ext
    imageWriter:ExportImage( fileName, image )
end

更多脚本示例可在计算机视觉沙盒的官方安装包中找到,或在项目网页上找到。结合Lua脚本API描述和各种教程,它们可以深入介绍可用功能。

设备插件

当引入脚本插件允许实现更高级的视频处理时,下一步是实现对与不同设备交互的支持。其想法是让脚本能够与真实世界互动——根据某些设备的输入改变视频处理例程,或根据图像处理算法的结果设置设备的输出/执行器。因此,添加了两种新的插件类型——设备插件和通信设备插件。这些插件允许支持与外部设备进行通信,例如不同的I/O板、机器人控制器、连接到串口的设备等。由于这是最近添加的功能之一,因此目前这些类型的插件并不多。随着项目的发展,将来会添加更多。

虽然这两种新插件类型都旨在与外部设备通信,但它们提供了略有不同的API,允许以不同的方式进行设备交互。设备插件隐藏了所有通信细节/协议,并通过设置/获取插件属性来与设备通信。例如,如果我们有一些数字I/O板,其输出的设置可以通过设置插件的一些属性来实现。查询其输出引脚的状态可以通过读取属性来实现。但在某些情况下,这样的接口可能过于受限,需要更大的灵活性。通信设备插件扩展了API,并提供了读/写方法,允许使用设备支持的任何协议向设备发送原始数据。让我们看几个与某些设备交互的示例。

第一个要演示的插件是游戏手柄设备,它可以在许多应用中使用。例如,如果某个摄像头安装在云台设备上,可以通过游戏手柄进行控制。或者它也可以用于控制某些机器人、视频处理序列等。

local math = require 'math'

gamepad = Host.CreatePluginInstance( 'Gamepad' )

-- Connected to the first game pad device 
gamepad:SetProperty( 'deviceId', 0 )
if not gamepad:Connect( ) then
    error( 'Failed connecting to game pad' )
end

-- Query name of the device, number of axes and buttons
deviceName   = gamepad:GetProperty( 'deviceName' )
axesCount    = gamepad:GetProperty( 'axesCount' )
buttonsCount = gamepad:GetProperty( 'buttonsCount' )

function Main( )
    -- Query value of all axes and buttons as arrays
    axesValues   = gamepad:GetProperty( 'axesValues' )
    buttonsState = gamepad:GetProperty( 'buttonsState' )
    
    print( 'X: ' .. tostring( math.floor( axesValues[1] * 100 ) / 100 ) )
    print( 'Y: ' .. tostring( math.floor( axesValues[2] * 100 ) / 100 ) )

    -- Query value of the X axis only
    x = gamepad:GetProperty( 'axesValues', 1 )
    
    -- Query status of the first button only
    buttonState1 = gamepad:GetProperty( 'buttonsState', 1 )
    
    if buttonState1 then
        print( "Button 1 is ON" )
    else
        print( "Button 1 is OFF" )
    end
end

要制作自己的云台设备,可以使用Phidget Advanced Servo板。此插件不包含在官方安装包中,但可以从GitHub单独获取。一旦添加到计算机视觉沙盒中,它可以单独用于控制伺服电机,也可以与上面提到的游戏手柄设备插件一起使用。

servos = Host.CreatePluginInstance( 'PhidgetAdvancedServo' )

-- Connected to Phidget Servo board connected to the system
if not servos:Connect( ) then
    error( 'Failed connecting to servo board' )
end

-- Check number of supported servos
motorCount = servos:GetProperty( 'motorCount' )

-- Configure velocity limit, acceleration and position range
servos:SetProperty( 'velocityLimit', { 2, 2 } )
servos:SetProperty( 'acceleration', { 20, 20 } )
servos:SetProperty( 'positionRange', { { 105, 115 }, { 135, 145 } } )

-- Engage both servos
servos:SetProperty( 'engaged', { true, true } )

-- Set target position of servo 1 and 2
servos:SetProperty( 'targetPosition', { 110, 140 } )

function Main( )
    -- Check actual position of servos and they are still moving
    actualPosition = servos:GetProperty( 'actualPosition' )
    stopped        = servos:GetProperty( 'stopped' )
    
    -- Set new target positions
    servos:SetProperty( 'targetPosition', 1, 115 )
    servos:SetProperty( 'targetPosition', 2, 135 )
end

来自同一制造商的另一个受支持设备是Phidget接口套件,它允许与数字输入/输出和模拟输入进行交互。例如,可以根据输入状态控制视频处理程序。或者根据视频流中检测到的内容控制连接到数字输出的设备。

kit = Host.CreatePluginInstance( 'PhidgetInterfaceKit' )

-- Connected to Phidget Interface Kit board plugged into the system
if not kit:Connect( ) then
    error( 'Failed connecting to interface kit board' )
end

-- Check number of available digital/analog I/O
digitalInputCount  = kit:GetProperty( 'digitalInputCount' )
digitalOutputCount = kit:GetProperty( 'digitalOutputCount' )
analogInputCount   = kit:GetProperty( 'analogInputCount' )

-- Switch OFF all digital inputs (assuming 8 inputs available)
kit:SetProperty( 'digitalOutputs', { false, false, false, false,
                                     false, false, false, false } )

function Main( )
    -- Switch ON 1st and 2nd digital outputs
    kit:SetProperty( 'digitalOutputs', { true, true } )
    -- Also switch ON the 7th output
    kit:SetProperty( 'digitalOutputs', 7, true )
    
    -- Read digital/analog inputs
    analogInputs  = kit:GetProperty( 'analogInputs' )
    digitalInputs = kit:GetProperty( 'digitalInputs' )
    
    for i = 1, #analogInputs do
        print( 'Analog input', i, 'is', analogInputs[i] )
    end

    for i = 1, #digitalInputs do
        print( 'Digital input', i, 'is', digitalInputs[i] )
    end
end

现在,假设我们通过串口连接了一个设备,它实现了某种特定的通信协议。例如,它可能是一个运行着某个草图的Arduino板,该草图允许通过串口发送命令来控制其一些电子设备。为此,我们可以使用串口通信设备插件,并通过使用读/写API来实现所支持的协议。例如,下面的脚本演示了与Arduino设备的通信,以打开/关闭LED并查询按钮的状态(假设Arduino板正在运行这里的示例草图)。

local string = require 'string'

serialPort = Host.CreatePluginInstance( 'SerialPort' )
serialPort:SetProperty( 'portName', 'COM8' )

-- Use blocking input, read operations wait up to the configured
-- timeout value
serialPort:SetProperty( 'blockingInput', true )
-- Total Read Timeout = ioTimeoutConstant + ioTimeoutMultiplier * bytesRequested
serialPort:SetProperty( 'ioTimeoutConstant', 50 )
serialPort:SetProperty( 'ioTimeoutMultiplier', 0 )

function Main()
    
    if serialPort:Connect( ) then
        print( 'Connected' )
        
        -- Test IsConnected() method
        print( 'IsConnected: ' .. tostring( serialPort:IsConnected( ) ) )

        -- Let Arduino board reset and get ready
        sleep( 1500 )
        
        -- Switch LED on - send command as string
        sent, status = serialPort:WriteString( 'led_on\n' )
        
        print( 'status: ' .. tostring( status ) )
        print( 'sent  : ' .. tostring( sent ) )

        strRead, status = serialPort:ReadString( 10 )

        print( 'status  : ' .. tostring( status ) )
        print( 'str read: ' .. strRead )

        -- Switch LED off - sned command as table of bytes 
        sent, status = serialPort:Write( { 0x6C, 0x65, 0x64, 0x5F, 0x6F, 0x66, 0x66, 0x0A } )
        
        print( 'status: ' .. tostring( status ) )
        print( 'sent  : ' .. tostring( sent ) )

        readBuffer, status = serialPort:Read( 10 )
        
        print( 'status    : ' .. tostring( status ) )
        print( 'bytes read: ' )
        for i=1, #readBuffer do
            print( '[', i, ']=', readBuffer[i] )
        end
        
        -- Check button state
        sent, status = serialPort:WriteString( 'btn_state\n' )
        
        print( 'status: ' .. tostring( status ) )
        print( 'sent  : ' .. tostring( sent ) )

        strRead, status = serialPort:ReadString( 10 )

        print( 'status  : ' .. tostring( status ) )
        print( 'str read: ' .. strRead )
        
        if string.sub( strRead, 1, 1 ) == '1' then
            print( 'button is ON' )
        else
            print( 'button is OFF' )
        end

        -- Test that communication is not blocking
        print( 'Testing timeout' )
        strRead, status = serialPort:ReadString( 10 )

        print( 'status  : ' .. tostring( status ) )
        print( 'str read: ' .. strRead )
        
        serialPort:Disconnect( )
    end
end

正如我们所看到的,添加对设备插件的支持扩展了计算机视觉沙盒的应用范围。事实上,将视频处理和计算机视觉与各种可用设备结合起来有许多有趣的方式。

沙盒脚本线程

正如上一章刚刚演示的,设备插件允许与各种不同的设备通信,从而可以创建更具交互性的应用程序。与不同设备的交互可以从与执行视频处理相同的脚本中完成。然而,在许多情况下,最好将与设备的通信放入单独的脚本中,而不是从视频处理脚本中进行。这有许多原因。首先,可能与执行的视频处理根本没有关系。例如,云台设备可以以一定的间隔移动摄像头,这不取决于视频处理算法的结果。或者,机器人的移动可以根据另一个设备的输入进行控制。其次,通常最好尽快完成视频处理,以便视频源不会被阻塞。然而,与某些设备的通信可能涉及由连接速度、使用的协议等引起的某些延迟。另一个原因可能是需要在不基于视频源帧速率的时间间隔内与某些设备交互,即,与某些设备的交互更频繁,而与另一些设备的交互不那么频繁。

为了解决独立于视频处理运行某些脚本的需求,计算机视觉沙盒引入了沙盒脚本线程的概念。沙盒向导不仅允许配置沙盒内每个摄像头要运行的视频处理步骤,还可以创建额外的线程,这些线程以设定的时间间隔运行指定的脚本。例如,下面的屏幕截图演示了控制PiRex机器人的可能设置。第一个线程运行一个脚本,根据游戏手柄的输入控制机器人的电机。为了使机器人足够灵敏,该线程以10毫秒的间隔运行控制脚本。第二个线程运行一个不同的脚本,该脚本查询机器人超声波传感器提供的距离测量值。由于这主要是信息性的,我们选择每秒运行10次,即以100毫秒的间隔运行。

Sandbox threads

在沙盒线程中运行的脚本结构与用于对摄像头图像执行视频处理的脚本非常相似。它们有一个全局部分和一个Main()函数。全局部分在沙盒启动时执行一次(在启动任何视频源之前)。Main()函数然后以配置的时间间隔反复执行。

让我们看看上面所示沙盒线程所用脚本的潜在实现。第一个脚本执行机器人的控制——根据游戏手柄的输入改变电机的功率。它与来自机器人摄像头的视频无关,我们希望它以比摄像头FPS更高的速率运行。这看起来是完美地在沙盒线程中独立运行的候选。它所做的只是读取游戏手柄轴的值,将这些值转换为电机功率值,并将它们发送给机器人,以便机器人执行所需的运动。

local math = require 'math'

gamepadPlugin = Host.CreatePluginInstance( 'Gamepad' )
pirexPlugin   = Host.CreatePluginInstance( 'PiRexBot' )

prevLeftPower  = 1000
prevRightPower = 1000

-- Configure gamepad and connect to it
gamepadPlugin:SetProperty( 'deviceId', 0 )
gamepadPlugin:Connect( )

-- Configure PiRex Bot and connect to it
pirexPlugin:SetProperty( 'address', '192.168.0.12' )
pirexPlugin:Connect( )

function Main( )
    axesValues = gamepadPlugin:GetProperty( 'axesValues' )

    -- Pulling gamepad's axis up result in -100, down: 100
    -- So need to invert it here to get something making sense
    leftPower  = 0 - math.floor( axesValues[2] * 100 )
    rightPower = 0 - math.floor( axesValues[3] * 100 )

    -- Set motors' power
    if ( math.abs( prevLeftPower - leftPower ) ) then
        pirexPlugin:SetProperty( 'leftMotor', leftPower )
    end

    if ( math.abs( prevRightPower - rightPower ) ) then
        pirexPlugin:SetProperty( 'rightMotor', rightPower )
    end

    -- Remember motors' power
    prevLeftPower  = leftPower
    prevRightPower = rightPower
end

我们的第二个脚本以100毫秒的间隔运行,用于读取机器人超声波传感器提供的距离测量值。我们不会做太多,只是将其直接显示给用户,显示在来自机器人摄像头的视频上。这需要一些图像处理(绘图)来显示与障碍物的距离,这意味着我们可以将读取传感器的代码放入执行摄像头视频处理的脚本中。然而,如前所述,传感器读取可能会导致某些延迟,我们并不真正希望将这些延迟引入视频处理中。因此,我们将传感器读取和测量显示分离到两个脚本中,这两个脚本通过使用主机变量进行通信。

local string = require 'string'

pirexPlugin = Host.CreatePluginInstance( 'PiRexBot' )

-- Configure PiRex Bot and connect to it
pirexPlugin:SetProperty( 'address', '192.168.0.12' )
pirexPlugin:Connect( )

function Main( )
    -- Get distance to obstacles in front of the robot
    distance = pirexPlugin:GetProperty( 'obstacleDistance' )

    Host.SetVariable( 'obstacleDistance', string.format( '%.2f', distance ) )
end

从上面可以看出,脚本只读取距离测量值并将它们放入一个主机变量中——仅此而已。显然,这不会向用户显示任何内容,但这正是视频处理脚本发挥作用的地方。除了我们可能想对来自机器人摄像头的图像进行的其他操作之外,我们还可以输出距离测量值,该值可以从主机变量中检索。

drawing = Host.CreatePluginInstance( 'ImageDrawing' )

function Main( )
    image    = Host.GetImage( )
    distance = Host.GetVariable( 'obstacleDistance' )
    
    -- ... Perform any image processing we wish to ...
    
    -- Distance to obstacles in front of robot
    drawing:CallFunction( 'DrawText', image, 'Distance : ' .. distance,
                          { 10, 10 }, '00FF00', '00000000' )
end

上述用例演示了沙盒线程的使用,以及如何利用它们在配置的时间间隔内执行特定动作。在沙盒中运行的所有脚本(线程或视频处理)都可以通过设置/读取主机变量进行通信。这可以实现不同的场景。视频处理例程可以由读取某些传感器驱动。然而,反过来也可以做到,即视频处理脚本可以根据应用算法的结果设置一些变量,然后线程脚本可以读取这些变量并驱动某些设备的执行器。

为了演示上述所有功能与一些视频处理的结合,这里有一段关于PiRex机器人使用游戏手柄控制来寻找隐藏字形的短视频。

 

项目代码

该项目的全部源代码可在其GitHub仓库中找到。代码主要以C/C++开发,以充分利用可用资源并提供合理的性能。它也以可移植性为理念进行开发,以便最终可以为Windows以外的平台构建。在早期阶段,它确实如此,在Windows和Linux上都进行了测试,但后来更多精力投入到推出并运行某些功能。因此,目前只提供了Windows安装包,对其他平台的支持可能会在未来的版本中提供。

计算机视觉沙盒项目使用了许多开源组件进行图像/视频的解码/编码,提供脚本功能,内置编辑器等。除此之外,它还使用了Qt框架来实现跨平台的用户界面。

项目的构建分两个阶段进行。第一部分是构建所有外部组件。这通常只需要完成一次,即可获取所有依赖项所需的库和二进制文件。然后可以构建项目代码本身。可以通过运行单个脚本来构建所有内容,也可以根据需要构建单个组件,这在开发新功能时是常见的情况。项目目前支持两种工具链——Visual Studio 2015(社区版即可正常工作)和MinGW。VS主要用于开发/调试,而所有官方版本迄今为止都是用MinGW完成的。

在过去的几年里,该项目的源代码增长相当可观,因此现在在一篇文章中详细描述其细节可能不太可行。其基础由“afx”库提供,这些库提供通用类型和函数,包括图像处理算法和对某些视频源的访问。然后,一组核心库定义了插件的接口、它们的管理、脚本和运行视频处理沙盒的骨干。大量插件实现了这些接口,提供了各种视频源、图像/视频处理例程、图像导入/导出、设备等。最后,还提供了一些应用程序。其中主要的一个是计算机视觉沙盒,本文已对其进行了描述。另一个有用的是计算机视觉沙盒脚本运行器(cvssr),这是一个命令行工具,用于运行一些简单的脚本,用于图像处理、与设备交互等。所提供的插件集合也可以在其他应用程序中重复使用,因为项目提供了C++库用于它们的加载和管理。

结论

好了,关于计算机视觉沙盒项目就到此为止。尽管本文可能没有详细描述项目中实现的每一个功能,但它很好地概述了主要功能以及如何将其用于不同的应用程序。项目网站提供了额外的教程,详细描述了其余功能并提供了更多使用示例。

正如开头所述,这个想法是构建一个允许实现来自计算机视觉各个领域的不同应用程序的项目。它被设计成高度模块化,因此各个功能都作为插件交付。根据插件的类型以及它们的组合方式,可以实现非常不同的结果。如果需要支持新的摄像头、图像处理例程、设备等,只需添加一个新插件,无需深入主应用程序的代码。

过去,在做不同的计算机视觉相关项目时,我通常最终会为每个新项目创建一个新应用程序。然而,现在我尝试只用一个脚本来完成。如果我发现缺少什么,就开发一个新的插件。不再需要为不同的事情创建额外的应用程序,一个就足够了。

该项目已经存在多年(尽管不是以开源形式),并成功地用于实现各种应用程序。其中一些是不同的业余爱好项目。但也有一些用于实验室/生产中的过程自动化领域。希望计算机视觉沙盒项目将继续发展,并在此基础上开发出越来越多有趣的应用程序。

© . All rights reserved.