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

使用 Pixy 视觉传感器在 Raspberry Pi 上使用 Windows 10 IoT Core 进行对象跟踪

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2018年4月28日

CPOL

14分钟阅读

viewsIcon

37998

downloadIcon

296

处理 Pixy 摄像头检测到的、通过 I2C 在 Raspberry Pi 上接收到的视觉对象的位置信息,并在 C# 程序中解析机器人传感器数据时使用通用设计模式。

引言

Microsoft Windows 10 IoT Core 于 2015 年的发布为 C# 开发者带来了新的机遇,让他们能够利用 Visual Studio 和最受欢迎的单板计算机之一——树莓派来探索机器人世界。在本文中,我们将介绍我的 C# 代码,该代码将 RPi 与Pixy集成起来——Pixy 是 Charmed Labs 设计的、专为对象跟踪而生的视觉传感器。

基于 Pixy 简化的数据检索协议,我将向您展示如何使用 Windows 10 IoT Core 应用程序在 RPi 上通过 I2C 总线接收和解析有关视觉对象大小和位置的信息。除了实现解决方案的技术方面,我还将分享我构建代码库的方法。我们将项目分解为不同的层,利用 LINQ To Objects 的强大功能处理从 Pixy 接收到的数据,并让通用设计模式为您服务。我希望无论是学习 .NET 的机器人爱好者还是刚接触机器人领域的老练 C# 开发人员,都能从中找到感兴趣的内容。

背景

我们中的许多人在刚开始接触机器人时,都必须选择第一个传感器来玩。虽然我不是从 Pixy 开始的,但对于经验尚可的程序员来说,Pixy 是一个不错的选择。它能以每秒 50 次的速度 crunch 大量视觉信息,并将对象位置数据以紧凑的格式提供给您。仅需 69 美元的投入,就能在程序中跟踪一个视觉对象,这很酷!

我一年前在 Raspberry PI 2 和 Visual Studio 2015 上完成了这个项目,但现在您可以使用 RPI 3 Model B+ 和 VS 2017。在撰写本文时,我使用的 Pixy CMUcam5 仍然是该设备的最新版本。

对于不熟悉 Windows 10 IoT Core 和 C# 的机器人爱好者,我想补充一点,Microsoft 免费提供的开发框架使您能够掌握与众多专业程序员用来构建企业软件和商业网站相同的技术。使用 VS.NET 并应用面向对象编程原则,您可以构建一个大型、组织良好的、面向增长的系统。标准设计模式、NuGet 包、代码库和现成解决方案可供我们使用,从而将实验应用程序的范围大大扩展到其原始范围之外。如果您在设计早期考虑关注点分离、层内逻辑的隔离以及它们之间的松耦合,您将多年来享受您不断发展的项目。无论专业还是业余构建机器人应用程序,这一点都成立。

使用 Pixy 视觉传感器

Pixy 直接以此处说明的格式将具有预设颜色签名的多个视觉对象的坐标发送到 RPi。

有几种方法可以使用这些信息。您可以找到在屏幕上显示对象框的代码示例,以及使用两个伺服电机让 Pixy 跟随对象的示例。我构建了后者,但伺服控制超出了本文的范围。

本文附带的源代码旨在为自主机器人提供对象坐标和大小。根据 Pixy 捕获的预设对象大小、到对象的距离以及从其坐标转换而来的对象角度,RPi 可以发送信号给电机驱动器来接近对象。因此,我们将只跟踪一个对象,但您可以根据自己的需求修改此逻辑。

这是一张 Pixy 连接到我机器人上的云台机制的图片

Pixy 最多可以存储 7 种不同的颜色签名,从而能够跟踪 7 种具有唯一颜色的不同对象。由于在此应用程序中,我们只对单个对象感兴趣,并且光照的显著变化会对 Pixy 的颜色过滤算法产生不利影响,因此我使用了所有 7 种签名来训练 Pixy 在 7 种不同光照条件下识别同一对象。

必备组件

您需要以下物品

  • Pixy 摄像头 - 您可以在亚马逊RobotShopSparkFun 购买,价格为 69 美元
  • Raspberry RPi2 或 3,附带电源和连接线
  • Visual Studio 2015 或 2017

附带的源代码被包装成一个可直接构建的 Visual Studio 解决方案,但是,它不应成为您的第一个 Windows 10 IoT Core 项目。那些愿意尝试的人应该已经有一个工作项目,其中包含为 ARM 处理器架构构建的通用 Windows 平台 (UWP) 应用程序,并且已验证可在 Raspberry Pi 上运行。请注意,我的代码假定是一个有界面的应用程序(请参阅 Timer 类型上的注释)。

如果您还没有玩过 RPi 和 Windows IoT Core,那么“Hello, Blinky”是一个很受欢迎的入门项目。如果您在此链接找不到它,请在 https://developer.microsoft.com/en-us/windows/iot/samples 上查找。

还有许多其他示例指导开发人员创建他们的第一个 RPi Windows 10 IoT Core 应用程序。例如,请查看以下文章 - 使用 C# 为 Windows 10 IoT 构建您的第一个应用程序

连接 Pixy 到 Raspberry Pi

我强烈建议使用扁平电缆连接您的 Pixy,而不是使用面包板跳线。在将 Pixy 放置在云台机制上时,这种连接方式要安全得多。Uxcell IDC 插座 10 针扁平电缆对我来说效果很好。

在我的机器人中,我将一个排针焊接到一块原型板上,我在那里创建了 I2C 集线器以及所有 I2C 设备的 5V 和地线。SDA 和 SCL 通过焊接到原型板上的跳线连接到 RPi 的 GPIO 2 和 3。电源由独立的 NiMH 电池和 5V 电压调节器提供,尽管只是玩 Pixy,您也可以简单地使用 RPi 的电源。

Pixy I2C 连接引脚

1 2 电源
3 4
5 SCL 6 地线
7 8
9 SDA 10

分层设计

实现分为 3 层

  1. 数据访问层 - 从数据源接收原始数据。此层包含 PixyDataReaderI2C 类。
  2. 存储库 - 将从源接收到的数据转换为对象块实体数据模型。这通过 PixyObjectFinder 来实现。
  3. 应用程序逻辑 - 查找最大的感兴趣对象并使用 CameraTargetFinder 确定目标对象。

在一个涉及许多不同传感器的庞大系统中,我会将这些层分离到单独的项目中,或者至少是单独的项目文件夹中。

控制流

数据访问层负责处理所有定时器事件,查找任何匹配的对象。而应用程序逻辑只对成功匹配感兴趣。

信息通过通过泛型委托实现的 2 个回调,从底层流向顶层。以下流程总结了这一点,但一旦您审查了每个层的详细信息,您应该重新审视本节。请注意,更高级别的对象不直接创建较低级别的对象,而是使用接口。

  1. Camera Target Finder 通过 fFindTargetm_dlgtFindTarget 委托传递给 Pixy Object Finder。
  2. Pixy Object Finder 通过 fParseBlocksm_dlgtExtractBlocks 委托传递给 Pixy Data Reader。
  3. Camera Target Finder 创建读取器和对象查找器的实例,然后启动 Pixy Object Finder。
  4. Pixy Object Finder 调用 Pixy Data Reader 创建一个计时器并开始监听设备。
  5. 当数据被读取后,Pixy Data Reader 会在 Pixy Object Finder 中调用 m_dlgtExtractBlocks(通过 fParseBlocks),将数据转换为颜色签名对象。
  6. m_dlgtExtractBlocks 调用 Camera Target Finder 中的 m_dlgrFindTarget(通过 fFindTarget),以提取每种颜色签名的最大对象并确定目标坐标。

当与接口结合使用时,这种流程可以将我们的类与其依赖项解耦,以便可以最小或不更改我们的类的源代码来替换或更新依赖项。更多内容见下文。

数据模型和接口

Pixy Object Block 包含 x/y 坐标及其宽度/高度。此外,我们希望跟踪检测到它时的时间。

    public class CameraObjectBlock
    {
        public int signature = -1;
        public int x = -1;
        public int y = -1;
        public int width = -1;
        public int height = -1;
        public DateTime dt;
    }

Camera Data Reader 接口定义了一个签名,供更高级别的代码使用,以实现与 Reader 实现的依赖性解耦。虽然我们无意在这里使用其他读取器,但这为扩展留下了空间,因此如果我们将来决定使用另一个 Reader,更高级别的逻辑不必更改以实例化另一个类,因为该其他 Reader 仍然符合既定的接口。

接下来,我们为 Pixy Object Finder 定义一个接口。最好将所有接口放在一起,并与它们的实现分开。这样,您可以拥有一个由数据模型和操作组成的独立域,有效地展示应用程序执行的功能以及它处理的数据类型。

    public interface ICameraDataReader
    {
        void Init(Func<byte[], int> fParseBlocks);
        Task Start();
        void Stop();
        void Listen();
        int GetBlockSize();    // bytes
        int GetMaxXPosition(); // pixels
        int GetMaxYPosition(); // pixels
    }

    public interface ICameraObjectFinder
    {
        void Start();
        void Stop();
        List<CameraObjectBlock> GetVisualObjects();
    }

    public abstract class CameraDataReader
    {
         protected CameraDataReader(ILogger lf)
         {}
    }

    public abstract class CameraObjectFinder
    {
        protected CameraObjectFinder(ICameraDataReader iCameraReader,
                       Func<List<CameraObjectBlock>, bool> fFindTarget,
                       ILogger lf)
        { }
    }

    public interface ILogger
    {
        void LogError(string s);
    }

创建了两个 abstract 类来强制执行特定的构造函数参数。

数据访问层

Pixy 每秒处理 1/50th 帧图像。这意味着您每 20 毫秒(PIXY_INTERVAL_MS = 20)即可获得所有检测到的对象位置的完整更新。有关更多信息,请参阅 http://cmucam.org/projects/cmucam5

PixyDataReaderI2C 类实现了 IPixyDataReader 接口。

    public class PixyDataReaderI2C : CameraDataReader, ICameraDataReader
    {
        // We are creating DispatcherTimer so that you could add some UI should you choose to do so.
        // If you are creating a headless background application then use ThreadPoolTimer.
        private DispatcherTimer m_timerRead = null;
        private Windows.Devices.I2c.I2cDevice m_deviceI2cPixy = null;
        private Func<byte[], int> m_fParseBlocks = null;

        private const int PIXY_INTERVAL_MS = 20; // PIXY runs every 20 millisecons
        private const int BLOCK_SIZE_BYTES = 14;

        private int m_maxNumberOfExpectedObjects = 50;
        public int MaxNumberOfExpectedObjects { 
		get { return m_maxNumberOfExpectedObjects; } 
		set { m_maxNumberOfExpectedObjects = value; } 
	}
        public int m_sizeLeadingZeroesBuffer = 100; // PIXY data buffer may contain leading zeroes
        // Lens field-of-view: 75 degrees horizontal, 47 degrees vertical
        // The numbers for x, y, width and height are in pixels on Pixy's camera.
        // The values range from 0 to 319 for width and 0 to 199 for height.
        public int GetMaxXPosition() { return 400; }
        public int GetMaxYPosition() { return 200; }

        private ILogger m_lf = null;

        public PixyDataReaderI2C(ILogger lf) : base(lf)
        {
            m_lf = lf;
        }

        public void Init(Func<byte[], int> fParseBlocks)
        {
            // This method is required because the reader is created by the factory - at the higher
	    // level where the Parse Blocks method is unavailable.
            m_fParseBlocks = fParseBlocks;
        }

数据读取器接受泛型委托 fParseBlocks,以便在不更改底层逻辑的情况下调用更高级别的转换方法,以防翻译器发生更改。

由于我的 RPi 通过 I2C 与 Pixy 通信,我们首先从操作系统检索设备选择器,然后使用它来枚举 I2C 控制器。最后,使用设备设置对象,我们获取到我们设备的句柄。

        public async Task Start()
        {
            try
            {
                string deviceSelector = Windows.Devices.I2c.I2cDevice.GetDeviceSelector();

                // Get all I2C bus controller devices 
                var devicesI2C = await DeviceInformation.FindAllAsync(deviceSelector).AsTask();
                if (devicesI2C == null || devicesI2C.Count == 0)
                    return;

                // Create settings for the device address configured via PixyMon.
                var settingsPixy = new Windows.Devices.I2c.I2cConnectionSettings(0x54);
                settingsPixy.BusSpeed = Windows.Devices.I2c.I2cBusSpeed.FastMode;

                // Create PIXY I2C Device
                m_deviceI2cPixy = await Windows.Devices.I2c.I2cDevice
					.FromIdAsync(devicesI2C.First().Id, settingsPixy);
            }
            catch (Exception ex)
            {
                m_lf.LogError(ex.Message);
            }
        }

接下来,我们设置一个计时器和一个处理程序,将 Pixy 的原始数据读取到 dataArray 中,并调用 m_fParseBlocks 进行翻译。

        public void Listen()
        {
            if (m_timerRead != null)
                m_timerRead.Stop();

            m_timerRead = new DispatcherTimer();
            m_timerRead.Interval = TimeSpan.FromMilliseconds(PIXY_INTERVAL_MS);
            m_timerRead.Tick += TimerRead_Tick;
            m_timerRead.Start();
        }

        private void TimerRead_Tick(object sender, object e)
        {
            try
            {
                if (m_deviceI2cPixy == null)
                    return;

                byte[] dataArray = new byte[MaxNumberOfExpectedObjects * BLOCK_SIZE_BYTES 
						+ m_sizeLeadingZeroesBuffer];
                m_deviceI2cPixy.Read(dataArray);
                m_fParseBlocks(dataArray);
            }
            catch (Exception ex)
            {
                m_lf.LogError(ex.Message);
            }
        }

请注意,我们也可以使用 async/await - 异步设计模式 - 来构建备用的 Reader,而不是使用计时器。这样的 Reader 可以通过类工厂注入到流程中,如应用程序逻辑层部分所述。

我的代码假定是带界面的应用程序,但如果您要在无界面的应用程序中运行它,请将计时器类型从 DispatcherTimer 更改为 ThreadPoolTimer。请参阅源代码中的相应注释。

存储库层

总的来说,我们使用 Repository 将数据检索逻辑与业务或应用程序逻辑分离,方法是将源数据转换为实体模型 - 业务逻辑使用的数据结构。这种额外的封装层称为存储库模式。在我们的用例中,转换器处理来自数据源的原始数据以提取感兴趣的视觉对象。这在 PixyObjectFinder 中完成,它将 Pixy 字节流转换为具有 x/y/w/h 属性的对象。

    public class PixyObjectFinder : CameraObjectFinder, ICameraObjectFinder
    {
        const UInt16 PixySyncWord = 0xaa55;
        const int BlockRetentionSeconds = 3;

        private ICameraDataReader m_pixy = null;
        private ILogger m_lf = null;
        public Object m_lockPixy = new Object();
        private Func<List<CameraObjectBlock>, bool> m_fFindTarget;
        private Func<byte[], int> m_dlgtExtractBlocks;

        private List<CameraObjectBlock> m_pixyObjectBlocks = new List<CameraObjectBlock>();
        public List<CameraObjectBlock> GetVisualObjects() { return m_pixyObjectBlocks;  }

Pixy 对象查找器

PixyObjectFinder 将缓冲区从 Pixy 对象块格式转换为我们检测到的对象实体模型,以便应用程序逻辑只处理其自己的格式,并与底层源保持独立。

PixyObjectFinder 使用 Start 方法初始化 Pixy 并在数据访问层中启动其计时器。

        public void Start()
        {
            m_pixy.Init(m_dlgtExtractBlocks);
            // Initialize Pixy I2C device.
            Task.Run(async () => await m_pixy.Start());
            // Launch Pixy listener
            m_pixy.Listen();
        }

转换主要实现在 m_dlgtExtractBlocks 中,它通过 m_pixy.Init(m_dlgtExtractBlocks) 作为参数传递给 Pixy 数据读取器。

        public PixyObjectFinder(ICameraDataReader ipixy,
                                Func<List<CameraObjectBlock>, bool> fFindTarget,
                                ILogger lf) : base(ipixy, fFindTarget, lf)
        {
            m_pixy = ipixy;
            m_fFindTarget = fFindTarget;
            m_lf = lf;

            m_dlgtExtractBlocks = delegate (byte[] byteBuffer)
            {
                lock (m_lockPixy)
                {
                    if (byteBuffer == null || byteBuffer.Length == 0)
                        return 0;

                    try
                    {
                        // Convert bytes to words
                        int blockSize = ipixy.GetBlockSize();
                        int lengthWords = 0;
                        int[] wordBuffer = ConvertByteArrayToWords(byteBuffer, ref lengthWords);
                        if (wordBuffer == null)
                            return 0;

                        // 0, 1     0              sync (0xaa55)
                        // 2, 3     1              checksum(sum of all 16 - bit words 2 - 6)
                        // 4, 5     2              signature number
                        // 6, 7     3              x center of object
                        // 8, 9     4              y center of object
                        // 10, 11   5              width of object
                        // 12, 13   6              height of object

                        // Find the beginning of each block
                        List<int> blockStartingMarkers = Enumerable.Range(0, wordBuffer.Length)
                            .Where(i => wordBuffer[i] == PixySyncWord)
                            .ToList<int>();

                        // Drop blocks that are more than BlockRetentionSeconds old
                        m_pixyObjectBlocks=m_pixyObjectBlocks.SkipWhile
                                                        (p => ((TimeSpan)(DateTime.Now - p.dt))
							.Seconds > BlockRetentionSeconds).ToList();

                        // Extract object blocks from the stream
                        blockStartingMarkers.ForEach(blockStart =>
                        {
                            if (blockStart < lengthWords - blockSize / 2)
                                m_pixyObjectBlocks.Add(new CameraObjectBlock()
                                {
                                    signature = wordBuffer[blockStart + 2],
                                    x = wordBuffer[blockStart + 3],
                                    y = wordBuffer[blockStart + 4],
                                    width = wordBuffer[blockStart + 5],
                                    height = wordBuffer[blockStart + 6],
                                    dt = DateTime.Now
                                });
                        });

                        m_fFindTarget(m_pixyObjectBlocks);
                        // Reset the blocks buffer
                        m_pixyObjectBlocks.Clear();
                    }
                    catch (Exception e)
                    {
                        m_lf.LogError(e.Message);
                    }
                }
                return m_pixyObjectBlocks.Count;
            };
        }

PixyObjectFinder 接受 fFindTarget 泛型委托来调用更高级别的处理器,该处理器将检测到的对象转换为目标坐标。

m_pixyObjectBlocks 数组包含检测到的对象。转换遵循上面代码片段中指定的 Pixy 流格式。

有关 Pixy 数据格式的更多详细信息,请参阅 Pixy 串行协议。请注意,串行和 I2C 以相同的流格式传递 Pixy 数据。

此外,我会在一个读取操作以上累积块,以在更长的时间段内(即超过 20 毫秒)平滑目标检测。这是通过 SkipWhile 实现的,它会丢弃比 BlockRetentionSeconds 旧的对象。

解析数据流

上述查找器方法必须首先将输入字节流转换为 16 位字,并将它们放入一个整数数组中。在这个数组中,我们将找到检测到的对象的 x、y、宽度和高度。

ConvertByteArrayToWords - PixyObjectFinderprivate 方法 - 将从 Pixy I2C 设备接收到的字节流转换为 16 位字。

        private int[] ConvertByteArrayToWords(byte[] byteBuffer, ref int lengthWords)
        {
            // http://cmucam.org/projects/cmucam5/wiki/Pixy_Serial_Protocol
            // All values in the object block are 16-bit words, sent least-signifcant byte 
            // first (little endian). So, for example, to send the sync word 0xaa55, you 
            // need to send 0x55 (first byte) then 0xaa (second byte).
            try
            {
                // Skip leading zeroes
                byteBuffer = byteBuffer.SkipWhile(s => s == 0).ToArray();
                if (byteBuffer.Length == 0)
                    return new int[0];

                // Convert bytes to words
                int length = byteBuffer.Length;
                lengthWords = length / 2 + 1;
                int[] wordBuffer = new int[lengthWords];
                int ndxWord = 0;
                for (int i = 0; i < length - 1; i += 2)
                {
                    if (byteBuffer[i] == 0 && byteBuffer[i + 1] == 0)
                        continue;
		            
		            // convert from little endian
                    int word = ((int)(byteBuffer[i + 1])) << 8 | ((int)byteBuffer[i]); 

                    if (word == PixySyncWord && ndxWord > 0 && PixySyncWord == wordBuffer[ndxWord - 1])
                        wordBuffer[ndxWord - 1] = 0; // suppress Pixy sync word marker duplicates

                    wordBuffer[ndxWord++] = word;
                }
                if (ndxWord == 0)
                    return null;

                return wordBuffer;
            }
            catch (Exception e)
            {
                m_lf.LogError(e.Message);
                return null;
            }
        }

如您所见,我必须调整解析器以跳过潜在的前导零和重复的同步字。如果您使用的是 RPi 3 和/或更新的 UWP 工具 SDK,您可能不必处理这些问题。

以下是 Pixy 发送到字节缓冲区中的单个对象块字节序列示例

00-00-00-00-55-AA-55-AA-BB-01-01-00-3D-01-73-00-04-00-06-00-00-00-

您应该能够通过 BitConverter.ToString(byteBuffer) 在调试器中查看字节缓冲区。

应用程序逻辑层

目标查找器根据存储库层提供的选定对象来确定目标。正是在这一层,我们应用了一种称为 Factory 的创建设计模式来创建和保留较低级别对象的实例。

类工厂

此模式有助于将我们的类与负责查找和管理依赖项的生存期解耦。请注意,我们的类工厂仅公开接口,并在内部调用构造函数。数据读取器和对象查找器都会在此处创建和存储。我们使用构造函数依赖注入来实例化它们,这为我们提供了通过在类工厂中创建它们来插入其他读取器和查找器实现的灵活性。

通过使用 Factory,我们应用了控制反转原则,该原则用对抽象(即接口)的依赖替换了对象之间的直接依赖。虽然这个概念远远超出了我的示例,但通常一个简单的类工厂就足够了。

Create 函数会传入用于计算目标的方法,该方法通过委托 Func<List<CameraObjectBlock>, bool> fFindTarget 来完成。

    public class MyDeviceClassFactory
    {
        private ICameraDataReader m_cameraDataReaderI2C = null;
        private ICameraObjectFinder m_cameraObjectFinder = null;

        private ILogger m_lf = new LoggingFacility();
        public ILogger LF { get { return m_lf; } }

        public void Create(Func<List<CameraObjectBlock>, bool> fFindTarget)
        {
            if (m_cameraObjectFinder != null)
                return;

            m_cameraDataReaderI2C = new PixyDataReaderI2C(m_lf);
            m_cameraObjectFinder = new PixyObjectFinder(m_cameraDataReaderI2C, fFindTarget, m_lf);
        }

        public ICameraDataReader CameraDataReader { get { return m_cameraDataReaderI2C; } }
        public ICameraObjectFinder CameraObjectFinder { get { return m_cameraObjectFinder; } }
    }

目标查找器

项目顶部是 CameraTargetFinder 类,它过滤预选对象以查找单个目标。它会忽略面积小于 minAcceptableAreaPixels 的对象,按大小对剩余对象进行排序,然后从顶部选取一个。它可能还会应用其他过滤器。最后,它调用 SetTargetPosition 来设置目标的位置和大小(以像素为单位)。

    public class CameraTargetFinder
    {
        private const int minAcceptableAreaPixels = 400;

        private MyDeviceClassFactory cf = new MyDeviceClassFactory();
        private Func<List<CameraObjectBlock>, bool> m_dlgtFindTarget;
        private Action<int, int, int, int> m_fSetTarget;

        public CameraTargetFinder(Action<int, int, int, int> fSetTarget)
        {
            m_dlgtFindTarget = delegate (List<CameraObjectBlock> objectsInView)
            {
                try
                {
                    if (objectsInView.Count == 0)
                        return false;

                    objectsInView = GetBiggestObjects(objectsInView);

                    // Select the biggest signature. We are only interested in a single object 
                    // because all signatures represent same object under different light conditions.
                    CameraObjectBlock biggestMatch = (from o in objectsInView
                                                      where o.width * o.height > minAcceptableAreaPixels
                                                      select o)
                                                    .OrderByDescending(s => s.width * s.height)
                                                    .FirstOrDefault();

                    if (biggestMatch == null || biggestMatch.signature < 0)
                        return false;

                    m_fSetTarget(biggestMatch.x, f.CameraDataReader.GetMaxYPosition() - biggestMatch.y, 
							biggestMatch.width, biggestMatch.height);
                    return true;
                }
                catch (Exception e)
                {
                    cf.LF.LogError(e.Message);
                    return false;
                }
            };

            m_fSetTarget = fSetTarget;
        }

生成的视觉对象列表通常包含许多误报,即与所需目标颜色签名相同的微小对象。除了进行调整以提高准确性外,我们通过调用 GetBiggestObjects() 来删除它们,只保留每个颜色签名中的最大对象。此方法首先按颜色签名对它们进行分组,然后在每个组中查找最大尺寸,并仅返回这些对象。

        private List<CameraObjectBlock> GetBiggestObjects(List<CameraObjectBlock> objectsInView)
        {
            // Find the biggest occurrence of each signature, the one with the maximum area
            List<CameraObjectBlock> bestMatches = (from o in objectsInView
                             group o by o.signature into grpSignatures
                             let biggest = grpSignatures.Max(t => t.height * t.width)
                             select grpSignatures.First(p => p.height * p.width == biggest))
                     .ToList();

            return bestMatches;
        }

GetBiggestObjects 方法是使用 LINQ 处理机器人应用程序中数据的绝佳示例。请注意,与机器人示例代码中常见的嵌套循环相比,查询代码是多么简洁。Python 开发人员会想在这里评论说,他们也可以使用集成查询的强大功能,尽管语法/谓词不同。

应用程序逻辑通过 StartCamera 方法启动摄像头并初始化目标跟踪。

        public void StartCamera()
        {
            try
            {
                cf.Create(m_dlgtFindTarget);
                cf.CameraObjectFinder.Start();
            }
            catch (Exception e)
            {
                cf.LF.LogError(e.Message);
                throw e;
            }
        }

在您的项目中使用的代码

首先,您必须教会 Pixy 识别一个对象。

接下来,创建一个 PixyTagetFinder 实例,并传入一个处理目标坐标的处理器。这是一个例子

    // Test
    public class BizLogic
    {
        CameraTargetFinder ctf = new CameraTargetFinder(delegate (int x, int y, int w, int h)
        {
            Debug.WriteLine("x: {0}, y: {1}, w: {2}, h: {3}", x, y, w, h);
        });
        public void Test()
        {
            ctf.StartCamera();
        }
    }

如果您知道目标对象的实际尺寸,您可以将高度和宽度转换为到目标的距离,同时将 x 和 y 转换为摄像头和目标之间的角度,这样您的控制器就可以相应地转动伺服电机,使摄像头始终指向目标。

要运行我的源代码,您可以直接将 PixyCamera.cs 文件添加到您的项目中,然后 - 为了测试代码 - 将上述示例集成到您应用程序的 MainPage 函数中。

如果您更愿意使用附加的解决方案,请在 Visual Studio 中将目标平台设置为 ARM,构建它,将其部署到 RPi 并在调试模式下运行。一旦 Pixy 摄像头初始化,将您预设的对象放在摄像头前。当 Pixy 检测到对象时,其 LED 指示灯将亮起,对象位置数据将出现在 Visual Studio 输出窗口中,例如:

结论

使用 Pixy 和熟悉的 Visual Studio 环境跟踪对象是一个非常有价值的项目,尤其是在它运行在一个小型计算机(如 RPi)的自主系统上时。如果底层程序组织良好并遵循其他开发人员认可的设计模式,那么它会更有趣。我们花费时间来正确构建和持续重构解决方案,以跟上项目的发展,这是值得的。

欢迎随意在您的个人或商业项目中使用该代码。它已经被我那个由 Pixy 引导的 20 磅重的 6 轮车充分测试过了。

有用资源

© . All rights reserved.