2024年4月

本文分享自华为云社区《
昇腾 CANN YOLOV8 和 YOLOV9 适配
》,作者:jackwangcumt。

1 概述

华为昇腾 CANN YOLOV8 推理示例 C++样例 , 是基于
Ascend CANN Samples
官方示例中的
sampleYOLOV7
进行的YOLOV8适配。一般来说,YOLOV7模型输出的数据大小为[1,25200,85],而YOLOV8模型输出的数据大小为[1,84,8400],因此,需要对
sampleYOLOV7
中的后处理部分进行修改,从而做到YOLOV8/YOLOV9模型的适配。因项目研发需要,公司购置了一台 Atlas 500 Pro 智能边缘服务器, 安装的操作系统为Ubuntu 20.04 LTS Server,并按照官方说明文档,安装的Ascend-cann-toolkit_7.0.RC1_linux-aarch64.run等软件。具体可以参考另外一篇博文【
Atlas 500 Pro 智能边缘服务器推理环境搭建
】,这里不再赘述。

2 YOLOV8模型准备

在进行YOLOV8模型适配工作之前,首先需要获取YOLOV8的模型文件,这里以官方的 YOLOV8n.pt模型为例,在Windows操作系统上可以安装YOLOV8环境,并执行如下python脚本(pth2onnx.py)将.pt模型转化成.onnx模型:

import argparsefromultralytics import YOLO

def main():
parser
=argparse.ArgumentParser()
parser.add_argument(
'--pt', default="yolov8n", help='.pt file')
args
=parser.parse_args()
model
=YOLO(args.pt)
onnx_model
= model.export(format="onnx", dynamic=False, simplify=True, opset=11)if __name__ == '__main__':
main()

具体的YOLOV8环境搭建步骤,可以参考
https://github.com/ultralytics/ultralytics
网站。当成功执行后,会生成yolov8n.onnx模型。输出内容示例如下所示:


(base) I:\yolov8\Yolov8_for_PyTorch>python pth2onnx.py --pt=yolov8n.pt
Ultralytics YOLOv8.
0.229

随着Aspire发布preview5的发布,Microsoft.Extensions.ServiceDiscovery随之更新,

服务注册发现这个属于老掉牙的话题解决什么问题就不赘述了,这里主要讲讲Microsoft.Extensions.ServiceDiscovery(preview5)以及如何扩展其他的中间件的发现集成 .

Microsoft.Extensions.ServiceDiscovery官方默认提供的Config,DNS,YARP三种Provider,使用也比较简单 :

builder.Services.AddServiceDiscovery();

builder.Services.AddHttpClient<CatalogServiceClient>(static client =>
    {
        client.BaseAddress = new("http://todo");
    });

builder.Services.ConfigureHttpClientDefaults(static http =>
{
    // 全局对HttpClient启用服务发现
    http.UseServiceDiscovery();
});

然后 appsettings.json 为名为 todo 的服务配置终结点:

  "Services": {
    "todo": {
      "http": [
        "http://localhost:5124"
      ]
    }
  }

然后使用服务发现:


#region 模拟服务端的todo接口: 
var sampleTodos = new Todo[] {
    new(1, "Walk the dog"),
    new(2, "Do the dishes", DateOnly.FromDateTime(DateTime.Now)),
    new(3, "Do the laundry", DateOnly.FromDateTime(DateTime.Now.AddDays(1))),
    new(4, "Clean the bathroom"),
    new(5, "Clean the car", DateOnly.FromDateTime(DateTime.Now.AddDays(2)))
};

var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
    sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
        ? Results.Ok(todo)
        : Results.NotFound());
#endregion

public record Todo(int Id, string? Title, DateOnly? DueBy = null, bool IsComplete = false);

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

#region 测试服务发现和负载

app.MapGet("/test", async (IHttpClientFactory clientFactory) =>
{
    //这里服务发现将自动解析配置文件中的服务
    var client = clientFactory.CreateClient("todo");
    var response = await client.GetAsync("/todos");
    var todos = await response.Content.ReadAsStringAsync();
    return Results.Content(todos, contentType: "application/json");
});

#endregion

运行程序后将会发现成功执行:
image

当然对于这样写死配置的服务发现一点都不灵活,因此应运而生了 YARP和DNS这些Provider, 目前服务注册发现使用Consul的还是挺多的,当然还有很多其他的轮子就不赘述了,这里我们来扩展一个Consul的服务发现Provider :

实现核心接口IServiceEndPointProvider

internal class ConsulServiceEndPointProvider(ServiceEndPointQuery query, IConsulClient consulClient, ILogger logger)
        : IServiceEndPointProvider, IHostNameFeature
    {
        const string Name = "Consul";
        private readonly string _serviceName = query.ServiceName;
        private readonly IConsulClient _consulClient = consulClient;
        private readonly ILogger _logger = logger;

        public string HostName => query.ServiceName;

#pragma warning disable CA1816 // Dispose 方法应调用 SuppressFinalize
        public ValueTask DisposeAsync() => default;

        public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken)
        {
            var flag = ServiceNameParts.TryParse(_serviceName, out var serviceNameParts);
            var sum = 0;
            if (flag)
            {
                var queryResult = await _consulClient.Health.Service(serviceNameParts.Host, string.Empty, true, cancellationToken);
                foreach (var serviceEntry in queryResult.Response)
                {
                    var address = $"{serviceEntry.Service.Address}:{serviceEntry.Service.Port}";
                    var isEndpoint = ServiceNameParts.TryCreateEndPoint(address, out var endPoint);
                    if (isEndpoint)
                    {
                        ++sum;
                        var serviceEndPoint = ServiceEndPoint.Create(endPoint!);
                        serviceEndPoint.Features.Set<IServiceEndPointProvider>(this);
                        serviceEndPoint.Features.Set<IHostNameFeature>(this);
                        endPoints.EndPoints.Add(serviceEndPoint);
                        _logger.LogInformation($"ConsulServiceEndPointProvider Found Service {_serviceName}:{address}");
                    }
                }
            }

            if (sum == 0)
            {
                _logger.LogWarning($"No ConsulServiceEndPointProvider were found for service '{_serviceName}' ('{HostName}').");
            }
        }

        /// <inheritdoc/>
        public override string ToString() => Name;
    }

实现 IServiceEndPointProviderFactory:


internal class ConsulServiceEndPointProviderFactory(IConsulClient consulClient, ILogger<ConsulServiceEndPointProviderFactory> logger) : IServiceEndPointProviderFactory
    {
        private readonly IConsulClient _consulClient = consulClient;
        private readonly ILogger<ConsulServiceEndPointProviderFactory> _logger = logger;

        public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
        {
            resolver = new ConsulServiceEndPointProvider(query, _consulClient, _logger);
            return true;
        }
    }

接着扩展一下IServiceCollection


public static IServiceCollection AddConsulServiceEndpointProvider(this IServiceCollection services)
{
	services.AddServiceDiscoveryCore();
	services.AddSingleton<IServiceEndPointProviderFactory, ConsulServiceEndPointProviderFactory>();
	return services;
}

最后添加一行代码 :

// 使用Microsoft.Extensions.ServiceDiscovery实现负载均衡
builder.Services.AddServiceDiscovery()
    .AddConfigurationServiceEndPointResolver() //config
    .AddConsulServiceEndpointProvider(); //consul

下面是Consul中注册完成的服务:
image

然后我们请求 ./test 调用服务,观察调试日志,成功了!

image

完整的代码:
https://github.com/vipwan/Biwen.Microsoft.Extensions.ServiceDiscovery.Consul

当然你也可以直接使用nuget引用 Biwen.Microsoft.Extensions.ServiceDiscovery.Consul 我已经发布到了nuget上 , 最后因为Aspire还在不停的迭代所以Biwen.Microsoft.Extensions.ServiceDiscovery.Consul后面还会存在一些变化, 前面的几个早期版本我都做了适配以最新的为准

由于此版本的mask2former官方只提供了macOS和Linux的安装说明,所以windows安装会趟一些坑记录一下

1.安装Anaconda
2.安装PyCharm
3.创建python3.8环境(最高3.8因为有一个依赖包最高支持python3.8)
4.安装GCC
下载地址:
https://sourceforge.net/projects/mingw/
点击Download
下载完成之后,双击打开点击Install,一路Next安装

安装GCC编译器

打开编译器然后在里点击Basic Setup,在Packpag里找到mingw-gcc-g++ -bin,左键点击小框框然后点击Mark for installation

然后点击菜单栏的Installation,点击Apply Changes

在弹出的对话框里点击Apply,然后等待安装完成即可

配置环境变量
先找到MinGW的安装bin文件的路径,然后复制

编辑环境变量中的Path

点完之后正常情况就安装完成了,马上测试一下
按WIN+R键,然后输入cmd,再输入gcc -v,出现以下信息则安装成功

5.安装pytorch,注意和cuda版本严格对应
6.安装库

点击查看代码
pip install -U opencv-python
conda install git   
pip install fvcore  

7.安装detectron2

点击查看代码
git clone git@github.com:facebookresearch/detectron2.git
cd detectron2
pip install -e .
pip install git+https://github.com/cocodataset/panopticapi.git
pip install git+https://github.com/mcordts/cityscapesScripts.git
pip install pycocotools-windows

8.安装detectron2的依赖库文件

点击查看代码
pip install -r requirements.txt

``
9.编译ops
9.1把mask2former/modeling/pixel_decoder/ops提到复制到上层文件夹 否则编译时候报错路径太长生成文件失败
9.2 报错
![](https://img2024.cnblogs.com/blog/3383332/202404/3383332-20240466666654043699-335577736.png)

把visual studio下cl.exe的加入path环境变量

9.3
报错subprocess.CalledProcessError: Command ‘[‘ninja‘, ‘-v‘]‘ returned non-zero exit status 1
将 mask2former/modeling/pixel_decoder/ops
目录下的setup.py中的
cmdclass={'build_ext': BuildExtension},
这一行改为
cmdclass={'build_ext':torch.utils.cpp_extension. BuildExtension.with_options(use_ninja=False)},
pytorch默认使用ninjia作为backend,这里把它禁用掉就好了
9.4在mask2former/modeling/pixel_decoder/ops下
python setup.py build install
编译成功

注意:需要安装安装visual studio和cuda

关键字auto在C++98中的语义是定义一个自动生命周期的变量,但因为定义的变量默认就是自动变量,因此这个关键字几乎没有人使用。于是C++标准委员会在C++11标准中改变了auto关键字的语义,使它变成一个类型占位符,允许在定义变量时不必明确写出确切的类型,让编译器在编译期间根据初始值自动推导出它的类型。这篇文章我们来解析auto自动类型推导的推导规则,以及使用auto有哪些优点,还有罗列出自C++11重新定义了auto的含义以后,在之后发布的C++14、C++17、C++20标准对auto的更新、增强的功能,以及auto有哪些使用限制。

推导规则

我们将以下面的形式来讨论:

auto var = expr;

这时auto代表了变量var的类型,除此形式之外还可以再加上一些类型修饰词,如:

const auto var = expr;
// 或者
const auto& var = expr;

这时变量var的类型是const auto或者const auto&,const也可以换成volatile修饰词,这两个称为CV修饰词,引用&也可以换成指针
,如const auto
,这时明确指出定义的是指针类型。

根据上面定义的形式,根据“=”左边auto的修饰情况分为三种情形:

  • 规则一:只有auto的情况,既非引用也非指针,表示按值初始化

如下的定义:

auto i = 1;	// i为int
auto d = 1.0;	// d为double

变量i将被推导为int类型,变量d将被推导为double类型,这时是根据“=”右边的表达式的值来推导出auto的类型,并将它们的值复制到左边的变量i和d中,因为是将右边expr表达式的值复制到左边变量中,所以右边表达式的CV(const和volatile)属性将会被忽略掉,如下的代码:

const int ci = 1;
auto i = ci;		// i为int

尽管ci是有const修饰的常量,但是变量i的类型是int类型,而非const int,因为此时i拷贝了ci的值,i和ci是两个不相关的变量,分别有不同的存储空间,变量ci不可修改的属性不代表变量i也不可修改。

当使用auto在同一条语句中定义多个变量时,变量的初始值的类型必须要统一,否则将无法推导出类型而导致编译错误:

auto i = 1, j = 2;	// i和j都为int
auto i = 1, j = 2.0;	// 编译错误,i为int,j为double
  • 规则二:形式如auto&或auto*,表示定义引用或者指针

当定义变量时使用如auto&或auto*的类型修饰,表示定义的是一个引用类型或者指针类型,这时右边的expr的CV属性将不能被忽略,如下的定义:

int x = 1;
const int cx = x;
const int& rx = x;
auto& i = x;	// (1) i为int&
auto& ci = cx;	// (2) ci为const int&
auto* pi = &rx;	// (3) pi为const int*

(1)语句中auto被推导为int,因此i的类型为int&。(2)语句中auto被推导为const int,ci的类型为const int &,因为ci是对cx的引用,而cx是一个const修饰的常量,因此对它的引用也必须是常量引用。(3)语句中的auto被推导为const int,pi的类型为const int*,rx的const属性将得到保留。

除了下面即将要讲到的第三种情况外,auto都不会推导出结果是引用的类型,如果要定义为引用类型,就要像上面那样明确地写出来,但是auto可以推导出来是指针类型,也就是说就算没有明确写出auto*,如果expr的类型是指针类型的话,auto则会被推导为指针类型,这时expr的const属性也会得到保留,如下的例子:

int i = 1;
auto pi = &i;	// pi为int*
const char word[] = "Hello world!";
auto str = word;	// str为const char*

pi被推导出来的类型为int
,而str被推导出来的类型为const char

  • 规则三:形式如auto&&,表示万能引用

当以auto&&的形式出现时,它表示的是万能引用而非右值引用,这时将视expr的类型分为两种情况,如果expr是个左值,那么它推导出来的结果是一个左值引用,这也是auto被推导为引用类型的唯一情形。而如果expr是个右值,那么将依据上面的第一种情形的规则。如下的例子:

int x = 1;
const int cx = x;
auto&& ref1 = x;	// (1) ref1为int&
auto&& ref2 = cx;	// (2) ref2为const int&
auto&& ref3 = 2;	// (3) ref3为int&&

(1)语句中x的类型是int且是左值,所以ref1的类型被推导为int&。(2)语句中的cx类型是const int且是左值,因此ref2的类型被推导为const int&。(3)语句中右侧的2是一个右值且类型为int,所以ref3的类型被推导为int&&。

上面根据“=”左侧的auto的形式归纳讨论了三种情形下的推导规则,接下来根据“=”右侧的expr的不同情况来讨论推导规则:

  • expr是一个引用

如果expr是一个引用,那么它的引用属性将被忽略,因为我们使用的是它引用的对象,而非这个引用本身,然后再根据上面的三种推导规则来推导,如下的定义:

int x = 1;
int &rx = x;
const int &crx = x;
auto i = rx;	// (1) i为int
auto j = crx;	// (2) j为int
auto& ri = crx;	// (3) ri为const int&

(1)语句中rx虽然是个引用,但是这里是使用它引用的对象的值,所以根据上面的第一条规则,这里i被推导为int类型。(2)语句中的crx是个常量引用,它和(1)语句的情况一样,这里只是复制它所引用的对象的值,它的const属性跟变量j没有关系,所以变量j的类型为int。(3)语句里的ri的类型修饰是auto&,所以应用上面的第二条规则,它是一个引用类型,而且crx的const属性将得到保留,因此ri的类型推导为const int&。

  • expr是初始化列表

当expr是一个初始化列表时,分为两种情况而定:

auto var = {};	// (1)
// 或者
auto var{};	// (2)

当使用第一种方式时,var将被推导为initializer_list
类型,这时无论花括号内是单个元素还是多个元素,都是推导为initializer_list 类型,而且如果是多个元素,每个元素的类型都必须要相同,否则将编译错误,如下例子:

auto x1 = {1, 2, 3, 4};		// x1为initializer_list<int>
auto x2 = {1, 2, 3, 4.0};	// 编译错误

x1的类型为initializer_list
,这里将经过两次类型推导,第一次是将x1推导为initializer_list 类型,第二次利用花括号内的元素推导出元素的类型T为int类型。x2的定义将会引起编译错误,因为x2虽然推导为initializer_list 类型,但是在推导T的类型时,里面的元素的类型不统一,导致无法推导出T的类型,引起编译错误。

当使用第二种方式时,var的类型被推导为花括号内元素的类型,花括号内必须为单元素,如下:

auto x1{1};	// x1为int
auto x2{1.0};	// x2为double

x1的类型推导为int,x2的类型推导为double。这种形式下花括号内必须为单元素,如果有多个元素将会编译错误,如:

auto x3{1, 2};	// 编译错误

这个将导致编译错误:error: initializer for variable 'x3' with type 'auto' contains multiple expressions。

  • expr是数组或者函数

数组在某些情况会退化成一个指向数组首元素的指针,但其实数组类型和指针类型并不相同,如下的定义:

const char name[] = "My Name";
const char* str = name;

数组name的类型是const char[8],而str的类型为const char*,在某些语义下它们可以互换,如在第一种规则下,expr是数组时,数组将退化为指针类型,如下:

const char name[] = "My Name";
auto str = name;	// str为const char*

str被推导为const char*类型,尽管name的类型为const char[8]。

但如果定义变量的形式是引用的话,根据上面的第二种规则,它将被推导为数组原本的类型:

const char name[] = "My Name";
auto& str = name;	// str为const char (&)[8]

这时auto被推导为const char [8],str是一个指向数组的引用,类型为const char (&)[8]。

当expr是函数时,它的规则和数组的情况类似,按值初始化时将退化为函数指针,如为引用时将为函数的引用,如下例子:

void func(int, double) {}
auto f1 = func;		// f1为void (*)(int, double)
auto& f2 = func;	// f2为void (&)(int, double)

f1的类型推导出来为void (*)(int, double),f2的类型推导出来为void (&)(int, double)。

  • expr是条件表达式语句

当expr是一个条件表达式语句时,条件表达式根据条件可能返回不同类型的值,这时编译器将会使用更大范围的类型来作为推导结果的类型,如:

auto i =  condition ? 1 : 2.0;	// i为double

无论condition的结果是true还是false,i的类型都将被推导为double类型。

使用auto的好处

  • 强制初始化的作用

当你定义一个变量时,可以这样写:

int i;

这样写编译是能够通过的,但是却有安全隐患,比如在局部代码中定义了这个变量,然后又接着使用它了,可能面临未初始化的风险。但如果你这样写:

auto i;

这样是编译不通过的,因为变量i缺少初始值,你必须给i指定初始值,如下:

auto i = 0;

必须给变量i初始值才能编译通过,这就避免了使用未初始化变量的风险。

  • 定义小范围内的局部变量时

在小范围的局部代码中定义一个临时变量,对理解整体代码不会造成困扰的,比如:

for (auto i = 1; i < size(); ++i) {}

或者是基于范围的for循环的代码,只是想要遍历容器中的元素,对于元素的类型不关心,如:

std::vector<int> v = {};
for (const auto& i : v) {}
  • 减少冗余代码

当变量的类型非常长时,明确写出它的类型会使代码变得又臃肿又难懂,而实际上我们并不关心它的具体类型,如:

std::map<std::string, int> m;
for (std::map<std::string, int>::iterator it = m.begin(); it != m.end(); ++it) {}

上面的代码非常长,造成阅读代码的不便,对增加理解代码的逻辑也没有什么好处,实际上我们并不关心it的实际类型,这时使用auto就使代码变得简洁:

for (auto it = m.begin(); it != m.end(); ++it) {}

再比如下面的例子:

std::unordered_multimap<int, int> m;
std::pair<std::unordered_multimap<int, int>::iterator,
		  std::unordered_multimap<int ,int>::iterator>
	range = m.equal_range(k);

对于上面的代码简直难懂,第一遍看还看不出来想代表的意思是什么,如果改为auto来写,则一目了然,一看就知道是在定义一个变量:

auto range = m.equal_range(k);
  • 无法写出的类型

如果说上面的代码虽然难懂和难写,毕竟还可以写出来,但有时在某些情况下却无法写出来,比如用一个变量来存储lambda表达式时,我们无法写出lambda表达式的类型是什么,这时可以使用auto来自动推导:

auto compare = [](int p1, int p2) { return p1 < p2; }
  • 避免对类型硬编码

除了上面提到的可以减少代码的冗余之外,使用auto也可以避免对类型的硬编码,也就是说不写死变量的类型,让编译器自动推导,如果我们要修改代码,就不用去修改相应的类型,比如我们将一种容器的类型改为另一种容器,迭代器的类型不需要修改,如:

std::map<std::string, int> m = { ... };
auto it = m.begin();
// 修改为无序容器时
std::unordered_map<std::string, int> m = { ... };
auto it = m.begin();

C++标准库里的容器大部分的接口都是相同的,泛型算法也能应用于大部分的容器,所以对于容器的具体类型并不是很重要,当根据业务的需要更换不同的容器时,使用auto可以很方便的修改代码。

  • 跨平台可移植性

假如你的代码中定义了一个vector,然后想要获取vector的元素的大小,这时你调用了成员函数size来获取,此时应该定义一个什么类型的变量来承接它的返回值?vector的成员函数size的原型如下:

size_type size() const noexcept;

size_type是vector内定义的类型,标准库对它的解释是“an unsigned integral type that can represent any non-negative value of difference_type”,于是你认为用unsigned类型就可以了,于是写下如下代码:

std::vector<int> v;
unsigned sz = v.size();

这样写可能会导致安全隐患,比如在32位的系统上,unsigned的大小是4个字节,size_type的大小也是4个字节,但是在64位的系统上,unsigned的大小是4个字节,而size_type的大小却是8个字节。这意味着原本在32位系统上运行良好的代码可能在64位的系统上运行异常,如果这里用auto来定义变量,则可以避免这种问题。

  • 避免写错类型

还有一种似是而非的问题,就是你的代码看起来没有问题,编译也没有问题,运行也正常,但是效率可能不如预期的高,比如有以下的代码:

std::unordered_map<std::string, int> m = { ... };
for (const std::pair<std::string, int> &p : m) {}

这段代码看起来完全没有问题,编译也没有任何警告,但是却暗藏隐患。原因是std::unordered_map容器的键值的类型是const的,所以std::pair的类型不是std::pair<std::string, int>而是std::pair<const std::string, int>。但是上面的代码中定义p的类型是前者,这会导致编译器想尽办法来将m中的元素(类型为std::pair<const std::string, int>)转换成std::pair<std::string, int>类型,因此编译器会拷贝m中的所有元素到临时对象,然后再让p引用到这些临时对象,每迭代一次,临时对象就被析构一次,这就导致了无故拷贝了那么多次对象和析构临时对象,效率上当然会大打折扣。如果你用auto来替代上面的定义,则完全可以避免这样的问题发生,如:

for (const auto& p : m) {}

新标准新增功能

  • 自动推导函数的返回值类型(C++14)

C++14标准支持了使用auto来推导函数的返回值类型,这样就不必明确写出函数返回值的类型,如下的代码:

template<typename T1, typename T2>
auto add(T1 a, T2 b) {
    return a + b;
}

int main() {
    auto i = add(1, 2);
}

不用管传入给add函数的参数的类型是什么,编译器会自动推导出返回值的类型。

  • 使用auto声明lambda的形参(C++14)

C++14标准还支持了可以使用auto来声明lambda表达式的形参,但普通函数的形参使用auto来声明需要C++20标准才支持,下面会提到。如下面的例子:

auto sum = [](auto p1, auto p2) { return p1 + p2; };

这样定义的lambda式有点像是模板,调用sum时会根据传入的参数推导出类型,你可以传入int类型参数也可以传入double类型参数,甚至也可以传入自定义类型,如果自定义类型支持加法运算的话。

  • 非类型模板形参的占位符(C++17)

C++17标准再次拓展了auto的功能,使得能够作为非类型模板形参的占位符,如下的例子:

template<auto N>
void func() {
    std::cout << N << std::endl;
}

func<1>();	// N为int类型
func<'c'>();	// N为chat类型

但是要保证推导出来的类型是能够作为模板形参的,比如推导出来是double类型,但模板参数不能接受是double类型时,则会导致编译不通过。

  • 结构化绑定功能(C++17)

C++17标准中auto还支持了结构化绑定的功能,这个功能有点类似tuple类型的tie函数,它可以分解结构化类型的数据,把多个变量绑定到结构化对象内部的对象上,在没有支持这个功能之前,要分解tuple里的数据需要这样写:

tuple x{1, "hello"s, 5.0};
itn a;
std::string b;
double c;
std::tie(a, b, c) = x;	// a=1, b="hello", c=5.0

在C++17之后可以使用auto来这样写:

tuple x{1, "hello"s, 5.0};
auto [a, b, c] = x;	// 作用如上
std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;

auto的推导功能从以前对单个变量进行类型推导扩展到可以对一组变量的推导,这样可以让我们省略了需要先声明变量再处理结构化对象的麻烦,特别是在for循环中遍历容器时,如下:

std::map<std::string, int> m;
for (auto& [k, v] : m) {
    std::cout << k << " => " << v << std::endl;
}
  • 使用auto声明函数的形参(C++20)

之前提到无法在普通函数中使用auto来声明形参,这个功能在C++20中也得到了支持。你终于可以写下这样的代码了:

auto add (auto p1, auto p2) { return p1 + p2; };
auto i = add(1, 2);
auto d = add(5.0, 6.0);
auto s = add("hello"s, "world"s);	// 必须要写上s,表示是string类型,默认是const char*,
                                	// char*类型是不支持加法的

这个看起来是不是和模板很像?但是写法要比模板要简单,通过查看生成的汇编代码,看到编译器的处理方式跟模板的处理方式是一样的,也就是说上面的三个函数调用分别产生出了三个函数实例:

auto add<int, int>(int, int);
auto add<double, double>(double, double);
auto add<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >);

使用auto的限制

上面详细列出了使用auto的好处和使用场景,但在有些地方使用auto还存在限制,下面也一并罗列出来。

  • 类内初始化成员时不能使用auto

在C++11标准中已经支持了在类内初始化数据成员,也就是说在定义类时,可以直接在类内声明数据成员的地方直接写上它们的初始值,但是在这个情况下不能使用auto来声明非静态数据成员,比如:

class Object {
	auto a = 1;	// 编译错误。
};

上面的代码会出现编译错误:error: 'auto' not allowed in non-static class member。虽然不能支持声明非静态数据成员,但却可以支持声明静态数据成员,在C++17标准之前,使用auto声明静态数据成员需要加上const修饰词,这就给使用上造成了不便,因此在C++17标准中取消了这个限制:

class Object {
	static inline auto a = 1;	// 需要写上inline修饰词
};
  • 函数无法返回initializer_list类型

虽然在C++14中支持了自动推导函数的返回值类型,但却不支持返回的类型是initializer_list
类型,因此下面的代码将编译不通过:

auto createList() {
    return {1, 2, 3};
}

编译错误信息:error: cannot deduce return type from initializer list。

  • lambda式参数无法使用initializer_list类型

同样地,在lambda式使用auto来声明形参时,也不能给它传递initializer_list
类型的参数,如下代码:

std::vector<int> v;
auto resetV = [&v](const auto& newV) { v = newV; };
resetV({1, 2, 3});

上面的代码会编译错误,无法使用参数{1, 2, 3}来推导出newV的类型。


此篇文章同步发布于我的微信公众号:
深入解析C++的auto自动类型推导
如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享或者微信号iTechShare并关注,以便在内容更新时直接向您推送。
image

无监督多视角行人检测 Unsupervised Multi-view Pedestrian Detection

论文url

https://arxiv.org/abs/2305.12457

论文简述

该论文提出了一种名为Unsupervised Multi-view Pedestrian Detection (UMPD)的新方法,旨在通过多视角视频监控数据准确地定位行人,而无需依赖于人工标注的视频帧和相机视角。

总体框架图

figure1
当我第一时间看到这个框架图,顿时感觉头发都掉了好几根,他这个设计确实有点复杂,并且和之前看的多视角检测方法很不一样,可能有些理解偏差,欢迎指正。

输入

  • 不同视角下多个摄像头的同步图像数据

语义感知迭代分割 Semantic-aware Iterative Segmentation(SIS)

PS
: 该模块所在部分就是上图绿色框部分,该模块主要分为两个部分,一个是
PCA主成分迭代分析
生成前景掩码部分,一个是
零样本分类视觉-语言模型CLIP
部分生成 $ {S}^{human} $ 语意掩码选择PCA的前景掩码部分。

  • PCA主成分迭代分析:


    • 首先, 多个摄像头的同步图像数据通过无监督模型提取预训练特征,将所有图像的预训练特征向量集合并成一个更大的特征矩阵,在这个矩阵中,每一行代表一个图像的特征向量,每一列代表特征向量中的一个维度。(猜测具体操作应该是模型中的最后一个卷积层的特征图进行展平操作,变成一个一维特征向量。将所有的一维特征向量堆叠起来就形成了一个二维的特征矩阵。)
    • 然后将这个二维的特征矩阵进行PCA降维操作,PCA的目的是找到一个新的低维特征空间,其中第一个主成分捕捉原始高维特征中的最大方差。通过PCA,数据被投影到第一个主成分(即PCA向量)上,生成一个新的一维特征表示。这个一维表示是每个原始高维特征向量在PCA方向上的投影长度。
    • 根据一维PCA值为每个视角生成初步的行人掩膜(即二值图像,设定一个阈值,其中行人前景(大于阈值)被标记为1,背景(小于等于阈值)为0)。
  • 零样本分类视觉-语言模型CLIP:
    CLIP拥有两个模块


    • CLIP Visual Encoder
      输入的是多个摄像头的同步图像数据
      输出是视觉特征图
    • CLIP Text Encoder
      输入是与行人相关的文本描述
      生成语言特征向量
    • 将语言特征向量与视觉特征图进行余弦相似度计算,得出图 $ {S}^{human} $

    figure2

  • 两模块结合操作:


    • 将CLIP生成的 $ {S}^{human} $ 与PCA生成的前景掩码进行重叠,来判断哪些前景掩码属于行人前景,然后将这些前景掩码继续用PCA进行迭代以及CLIP判断直到规定的迭代次数将前景掩码输入到下一部分作为伪标签。

figure3

几何感知体积探测器 Geometric-aware Volume-based Detector(GVD)

PS
: 该模块所在部分就是第一张图红色框部分

  • 2D特征提取
    :每个视角拍摄的图都用ResNet Visual Encoder进行特征提取。
  • 2D到3D的几何投影
    :提取的特征随后被映射到3D空间中。这一步骤涉及到使用相机的内参和外参矩阵,将2D图像中的像素点映射到3D空间中的体素上。这个过程基于针孔相机模型,通过几何变换将2D图像中的信息转换为3D体积的一部分。
    figure4
  • 3D体积融合
    :由于每个视角都会生成一个3D体积,GVD模块需要将这些体积融合成一个统一的3D体积。这通常通过一个Soft-Max Volume Feat. Fusion函数来实现,该函数可以对来自不同视角的3D体积进行加权和融合。
    figure5
  • 3D卷积网络解码器
    :融合后的3D体积被送入一个3D卷积网络解码器,该解码器负责预测每个体素的密度和颜色。这个解码器通常由一系列3D卷积层组成,能够学习从2D图像到3D体积的复杂映射关系。(论文中没有给出该解码器具体是怎么设计的)
  • 3D渲染为2D
    :作者用PyTorch3D可微分渲染框架将预测的3D密度 $ {D} $ 渲染为2D掩码
    \(\tilde{M}\)
    ,并且将预测的3D颜色 $ {C} $ 渲染为2D图像
    \(\tilde{I}\)
    , $ {M} $ 为SIS输出的前景掩码, $ {I} $ 论文中说是根据前景掩码得出的颜色图像(猜测应该是前景图像中为1的部分才保留原图颜色)。

垂直感知BEV正则化 Vertical-aware BEV Regularization

  • 通过GVD得出的3D体积中的密度信息沿着Z轴(垂直轴)进行最大值投影,以生成BEV(Bird Eye View)表示。这样可以得到一个二维平面图,其中高密度区域表示行人的位置,得出结果。
  • 并且为了应对出现的行人躺着或者斜着的情况(在大多数情况下,行人的姿态是接近垂直的),论文提出了Vertical-aware BEV Regularization(VBR)方法。通过计算 $ {L}_{VBR} $ 损失函数来优化这个影响。
    figure6
  • 损失函数
    figure7
    运用了
    Huber Loss

效果图

figure8

后记


作者最后应该还做了些后处理,但是论文中没有提及具体内容。该篇内容细节很多,公式变换复杂,有些细节我做了一定的省略,建议结合着论文原文来看。
ps:终于干完这篇了,鼠鼠我要逝了