LSS论文与代码详解

本文首发于公众号【DeepDriving】,欢迎关注。

0. 前言

最近几年,BEV感知是自动驾驶领域中一个非常热门研究方向,其核心思想是把多路传感器的数据转换到统一的BEV空间中去提取特征,实现目标检测、地图构建等任务。如何把多路相机的数据从二维的图像视角转换到三维的BEV视角?LSS提出一种显示估计深度信息的方法,实现图像特征到BEV特征的转换,从而实现语义分割任务。

LSS是英伟达在ECCV2020上发表的文章《Lift, Splat, Shoot: Encoding Images from Arbitrary Camera Rigs by Implicitly Unprojecting to 3D》中提出的一个BEV感知算法,后续很多BEV感知算法如CaDDNBEVDet都是在LSS的基础上实现的。本文将结合论文和代码详细解读LSS的原理。

1. 核心思想

作者提出一种新的端到端架构,该架构可以从任意数量的相机中直接提取给定图像数据场景中的鸟瞰图(bird’s-eye-view,BEV)表示,其核心思想是将每张图像单独“提升(Lift)”到每个相机的特征视锥体中,然后将所有视锥体“溅射(Splat)”到栅格化的BEV网格中。结果表明,这样训练的模型不仅能够学习如何表示图像特征,还能够学习如何将来自全部相机的预测结果融合到单个内聚的场景表示中,同时对标定误差具有鲁棒性。在基于BEV的目标分割和地图分割等任务中,该模型都比之前的模型表现得更出色。

2. 算法原理

在自动驾驶中,通常会在车身周围安装多个传感器(相机、雷达等)以使车辆具备360°感知能力。每个传感器都有自己的一个坐标系,它们的输出数据或者感知结果最终会被汇总到一个统一的坐标系——自车坐标系进行处理。通过标定,我们可以得到车上每个相机的外参矩阵 E k ∈ R 3 × 4 E_{k} \in \mathbb{R}^{3\times 4} EkR3×4和内参矩阵 I k ∈ R 3 × 3 I_{k} \in \mathbb{R}^{3\times 3} IkR3×3。内、外参矩阵决定了从自车坐标系 ( x , y , z ) (x,y,z) (x,y,z)到图像坐标系 ( h , w , d ) (h,w,d) (h,w,d)的映射关系。对于从各个相机获取的 n n n张图片 { X k ∈ R 3 × H × W } n \left \{ X_{k} \in \mathbb{R}^{3\times H\times W}\right \}_{n} {XkR3×H×W}nLSS算法的目的是在BEV坐标系 y ∈ R C × X × Y y\in \mathbb{R}^{C\times X \times Y} yRC×X×Y中找到该场景的栅格化表示,然后在该表示的基础上实现目标分割、地图分割等感知任务。

LSS算法分为3个步骤:LiftSplatShoot

2.1 Lift: 潜在的深度分布

这一步的目的是把每个相机的图像从局部2D坐标系Lift到全部相机共享的统一3D坐标系,这个操作过程每个相机是独立进行的。

众所周知,从二维图像中看到的物体是没有深度信息的,所以从图像上我们不知道这些物体在三维空间中的实际位置和大小。为了解决深度信息缺失的问题,LSS算法提出的解决方案是在所有可能的深度上为每个像素生成一个表示。假设一个相机的图像表示为 X ∈ R 3 × H × W X\in \mathbb{R}^{3 \times H \times W} XR3×H×W,它的内、外参矩阵分别为 I I I E E E,像素 p p p在图像坐标系中的坐标为 ( h , w ) (h,w) (h,w)。对于图像中的每个像素 p p p,作者使用$\left | D \right | 个点 个点 个点\left { (h,w,d)\in \mathbb{R} ^{3} \right } 与像素进行关联,其中 与像素进行关联,其中 与像素进行关联,其中D 表示一组离散的深度值,定义为 表示一组离散的深度值,定义为 表示一组离散的深度值,定义为\left { d_{0}+\Delta ,\dots ,d_{0}+\left | D \right |\Delta \right } 。通过这种方式,就为每个相机的图像创建了数量为 。通过这种方式,就为每个相机的图像创建了数量为 。通过这种方式,就为每个相机的图像创建了数量为D \times H \times W$的点云,这个过程只跟相机的内外参有关,并没有可学习的参数。

在像素点 p p p,模型会预测一个上下文向量 c ∈ R C \mathbf{c} \in \mathbb{R}^{C} cRC和每个像素在深度上的分布 α ∈ Δ ∣ D ∣ − 1 \alpha \in \Delta^{\left | D \right |-1} αΔD1,与点 p d p_{d} pd关联的上下文特征向量 c d ∈ R C c_{d} \in \mathbb{R}^{C} cdRC定义为 c d = α d c \mathbf{c}_{d}=\alpha_{d}\mathbf{c} cd=αdc

总的来说,Lift这个操作是为每个相机的图像生成大小为 D × H × W D \times H \times W D×H×W空间位置查询,这个空间中的每个点对应一个上下文特征向量 c d ∈ R C c_{d} \in \mathbb{R}^{C} cdRC。在相机的可视范围内,这个空间是一个视椎体。

2.2 Splat:Pillar池化

作者采用与pointpillars算法中一样的方式处理Lift操作生成的点云,一个Pillar定义为无限高度的体素。每个点被分配到与其最近的Pillar中然后执行求和池化,产生一个可以被标准CNN处理的 C × H × W C\times H \times W C×H×W维度的张量。

为了提升效率,作者采用“累计求和”的方式实现求和池化,而不是等填充完每个Pillar后再来做池化。这种操作具有可分析的梯度,可以高效地计算以加速自动微分过程。由于Lift操作生成的点云坐标只与相机的内外参有关,因此可以预先给每个点分配一个索引,用于指示其属于哪个Pillar。对所有点按照索引进行排序,累积求和的具体实现过程如下:

图片来源于深蓝学院《BEV感知理论与实践》

2.3 Shoot:运动规划

这个操作是根据前面BEV空间的感知结果学习端到端的轨迹预测代价图用于运动规划。由于我们主要关注感知部分,这部分就不做过多介绍

3. 代码解析

如果只看论文,估计很多人看完论文后还是一头雾水,根本不知道LSS到底是怎么实现的。接下来我们就结合代码对LSS的每个步骤进行详细解析。

LSS模型被封装在src/model.py文件中的LiftSplatShoot类中,模型用Nuscense数据集进行训练,每次输入车身环视6个相机的图像。Nuscense数据集中的原始图像宽高为1600x900,在预处理的时候被缩放到352x128的大小,6个相机的图像经过预处理后组成一个维度为(B=1,N=6,C=3,H=128,W=352)的张量输入给LSS模型。前向推理时,LiftSplatShoot类的forward函数需要输入以下几个参数:

  • x: 6个相机的图像组成的张量,(1,6,3,128,352)
  • rots: 6个相机从相机坐标系到自车坐标系的旋转矩阵,(1,6,3,3)
  • trans: 6个相机从相机坐标系到自车坐标系的平移向量,(1,6,3)
  • intrins: 6个相机的内参矩阵,(1,6,3,3)
  • post_rots: 6个相机的图像因预处理操作带来的旋转矩阵,(1,6,3,3)
  • post_trans: 6个相机的图像因预处理操作带来的平移向量,(1,6,3)

LSS模型前向推理的大致流程如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

LiftSplatShoot类的初始化函数中,会调用create_frustum函数去为相机生成图像坐标系下的视锥点云,维度为(D=41,H=8,W=22,3),其中D表示深度方向上离散深度点的数量,3表示每个点云的坐标[h,w,d]。

def create_frustum(self):# make grid in image plane# 模型输入图片大小,ogfH:128, ogfW:352ogfH, ogfW = self.data_aug_conf['final_dim']# 输入图片下采样16倍的大小,fH:8, fW:22fH, fW = ogfH // self.downsample, ogfW // self.downsample# ds取值范围为4~44,采样间隔为1ds = torch.arange(*self.grid_conf['dbound'], dtype=torch.float).view(-1, 1, 1).expand(-1, fH, fW)D, _, _ = ds.shape# xs取值范围为0~351,在该范围内等间距取22个点,然后扩展维度,最终维度为(41,8,22)xs = torch.linspace(0, ogfW - 1, fW, dtype=torch.float).view(1, 1, fW).expand(D, fH, fW)# ys取值范围为0~127,在该范围内等间距取8个点,然后扩展维度,最终维度为(41,8,22)ys = torch.linspace(0, ogfH - 1, fH, dtype=torch.float).view(1, fH, 1).expand(D, fH, fW)# D x H x W x 3# frustum维度为(41,8,22,3)frustum = torch.stack((xs, ys, ds), -1)return nn.Parameter(frustum, requires_grad=False)

在推理阶段,会根据相机的内外参把图像坐标系下的视锥点云转换到自车坐标系下,这个过程在get_geometry函数中实现:

def get_geometry(self, rots, trans, intrins, post_rots, post_trans):B, N, _ = trans.shape# undo post-transformation# B x N x D x H x W x 3# 首先抵消因预处理带来的旋转和平移points = self.frustum - post_trans.view(B, N, 1, 1, 1, 3)points = torch.inverse(post_rots).view(B, N, 1, 1, 1, 3, 3).matmul(points.unsqueeze(-1))# 坐标系转换过程:图像坐标系 -> 相机坐标系 ->自车坐标系# points[:, :, :, :, :, :2]表示图像坐标系下的(h,w),points[:, :, :, :, :, 2:3]为深度dpoints = torch.cat((points[:, :, :, :, :, :2] * points[:, :, :, :, :, 2:3],points[:, :, :, :, :, 2:3]), 5)# 首先乘以内参的逆转到相机坐标系,再由相机坐标系转到自车坐标系combine = rots.matmul(torch.inverse(intrins))points = combine.view(B, N, 1, 1, 1, 3, 3).matmul(points).squeeze(-1)points += trans.view(B, N, 1, 1, 1, 3)return points

要想看懂这个函数中关于坐标系转换的代码,我们需要了解不同坐标系之间的关系。

图片来源于深蓝学院《BEV感知理论与实践》

假设用 K K K表示相机内参矩阵, d d d表示三维点 P P P在相机坐标系下的深度,该点在图像坐标系下的坐标为 ( u , v , d ) T (u,v,d)^{T} (u,v,d)T,那么该点在相机坐标系下的坐标 ( X c , Y c , Z c ) T (X_{c},Y_{c},Z_{c})^{T} (Xc,Yc,Zc)T可以表示为:

( X c Y c Z c ) = K − 1 ( u d v d d ) \begin{pmatrix} X_{c} \\ Y_{c} \\ Z_{c} \end{pmatrix}=K^{-1}\begin{pmatrix} ud \\ vd \\ d \end{pmatrix} XcYcZc =K1 udvdd

R R R表示由相机坐标系转换到自车坐标系的旋转矩阵, t \mathbf{t} t表示由相机坐标系转换到自车坐标系的平移向量,那么自车坐标系下的点 ( X e g o , Y e g o , Z e g o ) T (X_{ego},Y_{ego},Z_{ego})^{T} (Xego,Yego,Zego)T可以表示为:

( X e g o Y e g o Z e g o ) = R ( X c Y c Z c ) + t \begin{pmatrix} X_{ego} \\ Y_{ego} \\ Z_{ego} \end{pmatrix}=R\begin{pmatrix} X_{c} \\ Y_{c} \\ Z_{c} \end{pmatrix}+\mathbf{t} XegoYegoZego =R XcYcZc +t

如果把经过上述转换的点云在BEV空间下进行可视化,可以得到类似下面的图:

说完了视锥点云的创建与变换过程,我们再来看一下模型对输入图像数据的处理。由6个相机的图像组成的张量x的维度为(1,6,3,128,352),推理时首先把维度变换为(1 * 6,3,128,352),然后送入camencode模块中进行处理。在camencode模块中,图像数据首先被送入EfficientNet-B0网络中去提取特征,该网络输出的两层特征x1x2的维度分别为(6,320,4,11)和(6,112,8,22)。接下来,x1x2被送入到Up模块中进行处理。在该模块中,对x1进行上采样把维度变为(6,320,8,22),然后与x2拼接到一起,最后经过两层卷积处理,输出维度为(6,512,8,22)的张量。这个张量再经过一个核大小为1x1的卷积层depthnet处理,输出的维度为(6,105,8,22)。在这105个通道中,其中前41个会用SoftMax函数求取表示41个离散深度的概率,另外64个通道则表示前面说过的上下文向量,这41个深度概率与64个上下文特征向量会做一个求外积的操作。整个camencode模块输出的张量维度为(6,64,41,8,22),最终这个张量的维度会被变换为(1,6,41,8,22,64)。(这段文字对照上面的流程图来看效果会更好

到这里,Lift这部分的操作就讲完了,接下来我们来看Splat

Splat操作的第一步是构建BEV空间下的特征,这个过程在voxel_pooling函数中实现。该函数有两个输入,一个自车坐标系下的视锥点云坐标点geom,维度为(1,6,41,8,22,3);另一个是camencode模块输出的图像特征点云x,维度为(1,6,41,8,22,64)。voxel_pooling函数的处理过程如下:

    1. x的维度变换为(1 * 6 * 41 * 8 * 22,64);
    1. geom转换到体素坐标下,得到对应的体素坐标,并将参数范围外的点过滤掉;
    1. 将体素坐标系下的geom的维度变换为(1 * 6 * 41 * 8 * 22,3),然后给每个点分配一个体素索引,再根据索引值对geomx进行排序,这样归属于同一体素的点geom及其对应的特征向量x就会被排到相邻的位置;
    1. 用累计求和的方式对每个体素中的点云特征进行求和池化;
    1. unbind对张量沿Z维度进行分离,然后将分离的张量拼接到一起进行输出。由于Z维度的值为1,这样做实际上是去掉了Z维度,这样BEV空间下的特征就构建好了。下图是对BEV特征做可视化的结果:

def voxel_pooling(self, geom_feats, x):B, N, D, H, W, C = x.shapeNprime = B*N*D*H*W# 将特征点云展平,共有B*N*D*H*W个点,每个点包含C维特征向量x = x.reshape(Nprime, C)# 把自车坐标系下的坐标转换为体素坐标,然后展平geom_feats = ((geom_feats - (self.bx - self.dx/2.)) / self.dx).long()geom_feats = geom_feats.view(Nprime, 3)# 求每个点对应的batch sizebatch_ix = torch.cat([torch.full([Nprime//B, 1], ix,device=x.device, dtype=torch.long) for ix in range(B)])geom_feats = torch.cat((geom_feats, batch_ix), 1)# 过滤点范围外的点kept = (geom_feats[:, 0] >= 0) & (geom_feats[:, 0] < self.nx[0])\& (geom_feats[:, 1] >= 0) & (geom_feats[:, 1] < self.nx[1])\& (geom_feats[:, 2] >= 0) & (geom_feats[:, 2] < self.nx[2])x = x[kept]geom_feats = geom_feats[kept]# 求每个点对应的体素索引,并根据索引进行排序ranks = geom_feats[:, 0] * (self.nx[1] * self.nx[2] * B)\+ geom_feats[:, 1] * (self.nx[2] * B)\+ geom_feats[:, 2] * B\+ geom_feats[:, 3]sorts = ranks.argsort()x, geom_feats, ranks = x[sorts], geom_feats[sorts], ranks[sorts]# 累计求和,对体素中的点进行求和池化if not self.use_quickcumsum:x, geom_feats = cumsum_trick(x, geom_feats, ranks)else:x, geom_feats = QuickCumsum.apply(x, geom_feats, ranks)# final:(B x C x Z x X x Y),(1 x 64 x 1 x 200 x 200)final = torch.zeros((B, C, self.nx[2], self.nx[0], self.nx[1]), device=x.device)# 把特征赋给对应的体素中final[geom_feats[:, 3], :, geom_feats[:, 2], geom_feats[:, 0], geom_feats[:, 1]] = x# 去掉Z维度final = torch.cat(final.unbind(dim=2), 1)# final:(1,64,200,200)return final

作者设置的自车坐标系下的感知范围(以米为单位)为:

  • x:[-50.0, 50.0]
  • y:[-50.0, 50.0]
  • z:[-10.0, 10.0]

在划分体素时,3个坐标轴方向分别以0.5,0.5,20.0的间隔进行划分,所以一共有200x200x1个体素。

在构建好BEV特征后,该特征会被送入bevencode模块进行处理,bevencode模块采用ResNet-18网络对BEV特征进行多尺度特征提取与融合。bevencode模块输出的特征被用于实现BEV空间下的语义分割任务,下图是对语义分割结果做可视化的效果:

5. 参考资料

  • 《Lift, Splat, Shoot: Encoding Images from Arbitrary Camera Rigs by Implicitly Unprojecting to 3D》
  • 深蓝学院《BEV感知理论与实践》课程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/862707.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于bim数字孪生threejs中使用glb文件大小优化及加载慢的说明(笔记)

在用three.js开发的时候发现&#xff0c;稍微大一点的glb或者fbx文件加载的时候很慢很卡 一直不理解这个卡和慢取决于哪些条件&#xff0c;下面来详细说一下 1、关于模型 不是越大加载越卡顿&#xff0c;而是却决于三角面数量&#xff0c;当累计三角面数量达到3000万时会出现明…

ASUS/华硕天选5 FX607J系列 原厂Windows11系统

安装后恢复到您开箱的体验界面&#xff0c;带原机所有驱动和软件&#xff0c;包括myasus mcafee office 奥创等。 最适合您电脑的系统&#xff0c;经厂家手调试最佳状态&#xff0c;性能与功耗直接拉满&#xff0c;体验最原汁原味的系统。 原厂系统下载网址&#xff1a;http:…

LLaMA2模型训练加速秘籍:700亿参数效率提升195%!

点击蓝字 关注我们 关注并星标 从此不迷路 计算机视觉研究院 公众号ID &#xff5c; 计算机视觉研究院 学习群 &#xff5c; 扫码在主页获取加入方式 开源地址&#xff1a;https://github.com/hpcaitech/ColossalAI 计算机视觉研究院专栏 Column of Computer Vision Ins…

Microsoft Defender防病毒怎么关闭!详细步骤看这里!

Microsoft Defender是Windows系统中的防病毒软件&#xff0c;提供了实时的安全保护功能。但是&#xff0c;在某些情况下&#xff0c;用户想要关闭系统内的Microsoft Defender功能&#xff0c;但不知道要怎么操作才能关闭&#xff1f;接下来小编给大家带来详细的关闭步骤介绍。 …

柔性数组(flexible array)

柔性数组从C99开始支持使用 1.柔性数组的概念 概念&#xff1a; 结构体中&#xff0c;结构体最后一个元素允许是未知大小的数组&#xff0c;这就叫[柔性数组]的成员 struct S {int n;char arr[]; //数组大小未知(柔性数组成员) }; 柔性数组的特点&#xff1a; 结构体中柔性…

VBA技术资料MF170:调整多个工作薄中签名位置

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

数据可视化如何为智慧农业带来变革

数据可视化如何为智慧农业保驾护航&#xff1f;随着农业现代化的深入推进&#xff0c;智慧农业应运而生&#xff0c;通过集成物联网、大数据、人工智能等先进技术&#xff0c;实现农业生产的数字化、智能化和高效化。而在这一过程中&#xff0c;数据可视化技术作为重要的工具&a…

主从复制、哨兵以及Cluster集群

目录 1.Redis高可用 2.Redis主从复制 2.1 主从复制的作用 2.2 主从复制流程 2.3 搭建Redis主从复制 2.3.1 修改Redis配置文件&#xff08;Master节点操作&#xff09; 2.3.2 修改Redis配置文件&#xff08;Slave节点操作&#xff09; 2.3.2 验证主从复制结果 3.Redis哨…

基于改进天鹰优化算法(IAO)优化支持向量机(SVM)数据回归预测(IAO-SVM)

改进天鹰优化算法(IAO)见&#xff1a;【智能优化算法】改进的AO算法(IAO)-CSDN博客 支持向量机(SVM)数据时序预测&#xff1a;基于支持向量机的数据回归预测-CSDN博客 代码原理 基于改进天鹰优化算法&#xff08;IAO&#xff09;优化支持向量机&#xff08;SVM&#xff09;数…

代码随想录算法训练营第三十七天|01背包问题、分割等和子集

01背包问题 题目链接&#xff1a;46. 携带研究材料 文档讲解&#xff1a;代码随想录 状态&#xff1a;忘了 二维dp 问题1&#xff1a;为啥会想到i代表第几个物品&#xff0c;j代表容量变化&#xff1f; 动态规划中&#xff0c;每次决策都依赖于前一个状态的结果&#xff0c;在…

Java中使用键盘录用【Scanner】遇到的问题

目录 一、空格截断问题&#xff1a;二、next()、nextInt()、nextDouble()等nextXxx()与nextLine()连用、混用的问题&#xff1a;问题描述&#xff1a;代码演示问题问题原因&#xff1a;解决办法&#xff1a;示例代码&#xff1a; 最后 Java中使用键盘录入&#xff0c;尤其是通过…

ROS2开发机器人移动

.创建功能包和节点 这里我们设计两个节点 example_interfaces_robot_01&#xff0c;机器人节点&#xff0c;对外提供控制机器人移动服务并发布机器人的状态。 example_interfaces_control_01&#xff0c;控制节点&#xff0c;发送机器人移动请求&#xff0c;订阅机器人状态话题…

力扣SQL50 员工的直属部门 子查询 双重

Problem: 1789. 员工的直属部门 &#x1f468;‍&#x1f3eb; 参考题解 Code select employee_id, department_id from Employee where primary_flag Y # Y 表明是直属部门 or employee_id in (select employee_idfrom Employeegroup by employee_idhaving count(employee…

【STM32-MAP文件分析】

STM32-MAP文件分析 ■ MDK编译生成文件简介■ .o■ .axf■ .hex■ .crf■ .d■ .dep■ .lnp■ .lst■ .map■ .build_log.htm■ .htm 文件■ .map 文件 ■ map文件分析■ map 文件的 MDK 设置■ 1. 要生成 map 文件 在 Listing 选项卡里面■ 2. Keil5 中打开.map 文件 ■ map 文…

信息学奥赛初赛天天练-38-CSP-J2021阅读程序-约数个数、约数和、埃氏筛法、欧拉筛法筛素数应用

PDF文档公众号回复关键字:20240628 2021 CSP-J 阅读程序3 1阅读程序(判断题1.5分 选择题3分 共计40分 ) 01 #include<stdio.h> 02 using namespace std; 03 04 #define n 100000 05 #define N n1 06 07 int m; 08 int a[N],b[N],c[N],d[N]; 09 int f[N],g[N]; 10 11 …

矩阵快速幂

矩阵快速幂 矩阵&#xff1a; 一个矩阵 A A A&#xff0c;是由 n m n\times m nm 个数字组成&#xff0c; B B B 由 m p m\times p mp 组成&#xff0c;详见下。 A [ a 1 , 1 , a 1 , 2 , a 1 , 3 ⋯ a 1 , m a 2 , 1 , a 2 , 2 , a 2 , 3 ⋯ a 2 , m ⋅ ⋅ ⋅ a n , …

javaSE知识点整理总结(上)

目录 一、面向对象 1. 类、对象、方法 2.面向对象三大特征 &#xff08;1&#xff09;封装 &#xff08;2&#xff09;继承 &#xff08;3&#xff09;多态 二、常用类 1.Object类 2.Array类 3.基本数据类型包装类 4.String类 5.StringBuffer类 6.Math类 7.Random…

摄影楼电子相册打开的正确方式,快来看看

​随着科技的不断发展&#xff0c;电子相册已经成为许多人存储和分享照片的重要方式。然而&#xff0c;你知道如何正确打开电子相册吗&#xff1f;今天&#xff0c;我就来教大家一下电子相册的正确打开方式&#xff0c;快来学习一下吧&#xff01; 第一步&#xff1a;选择合适的…

QT拖放事件之七:子类化QMimeData,实现对多个自定义类型进行数据

1、前提说明 /*自定义的MIME类型数据存储在QMimeData对象中, 存在两种方法:1. setData(...)可以把自定义类型的数据以QByteArray的形式直接存储在QMimeData中,但是使用此方法一次只能对一个MIME类型进行处理(可参考 QT拖放事件六:自定义MIME类型的存储及读取demo ) 一文。…

成立近30年,它如何找到政企采购突破点?

回看中国采购行业的发展&#xff0c;大致可以被分为四个阶段&#xff1a;上世纪90年代的传统采购时代、本世纪初的ERP采购时代、近10年的SRM采购时代以及2018年以来开启的数字化采购时代。近年来&#xff0c;大数据、人工智能和物联网的高速发展&#xff0c;为采购信息化提供底…