0%

前言

为了告别配置环境配一天啥也搞不了的问题,决定学习使用以下docker正好我这篇需要复现的多传感器融合的代码有他自己的dockerfile可以作为学习的尝试和检验。顺便也尝试利用官方的英文文档进行学习,提高一下自己的学习能力。本文参考教程

首先来看看docker的优点:

  1. 应用于更快速的交付和部署
  • 传统:通过大量的帮助文档,安装程序!
  • Docker:打包镜像发布测试,一键运行!
  1. 更便捷的升级和扩缩容
  • 通过使用Docker,部署应用 如同搭积木一样!
  1. 更简单的系统运维
  • 使用容器化之后,开发和测试环境是高度一致的
  1. 更高效的计算资源利用
  • Docker是内核级别的虚拟化,可以在一个物理机上运行很多的容器,让服务器的性能可以压榨到极致!

docker的基本组成:

simple

镜像(Image):

docker镜像就好比一个模板,我们可以通过这个模板来创建容器服务,tomcat镜像===>run==>tomcat01容器(提供服务器),通过这个镜像可以创建多个容器(最终服务运行或者项目运行就是在容器中的)。

容器(container):

docker利用容器技术,独立运行一个或者一组应用 通过镜像来创建

启动,停止,删除,基本命令!
目前就可以把这个容器理解为就是一个简易的linux系统

仓库(repository):

仓库就是存放 镜像(image)的地方!

仓库又可以分为 公有仓库和私有仓库

安装Docker

可以打开安装网址按照下列指令进行安装。

主要进行curl -fsSL https://get.docker.com -o install-docker.shsudo sh install-docker.sh需要版本特定要求再细致安装。

拉取镜像

这个很简单就行git拉源码一样使用sudo docker pull 镜像地址就可以进行下载

诺出现网络问题可以通过更换代理的办法进行解决:
打开配置文件:

1
sudo vi /etc/docker/daemon.json

修改输入下列内容保存退出:
1
2
3
4
5
6
7
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://docker.1panel.live",
"https://hub.rat.dev"
]
}

重启docker:
1
sudo service docker restart

管理镜像

列出所有的docker镜像

1
sudo docker images

删除对应的镜像
1
sudo docker rmi 镜像名称

使用镜像创建并运行一个容器

1
sudo docker run 镜像名称


1
sudo docker run -d 镜像名称 


1
sudo docker run -p 宿主机端口:容器端口 镜像名称 


1
sudo docker run -v 宿主机目录:容器目录(绑定挂载)卷的名字:容器目录(命名卷挂载) 镜像名称 

-d不会占用当前终端,-p是将宿主主机端口转发到容器端口实现访问。需要注意某些容器运行时需要-e添加对应参数才能进行(项目手册基本会有说明)--name myname可以给容器自定义唯一名字。-it 让我的终端进入容器工作 --rm当容器停止就删除容器。--restart always/unless-stopped前者是只要容器停止就重启后者手动关闭容器不重启。

查看正在运行的容器:

1
sudo docker ps

如果加上-a就是查看所有容器无论是否在运行

docker创建挂载卷:

1
docker volume create 卷的名字

这样就可以在宿主机部分直接使用挂载卷名字了,同时也可以查看挂载卷在宿主机上的目录:

1
sudo docker volume imspect 挂载卷名字

tip:进入该目录需要sudo -i进入root用户才能够进行

关于卷的管理

查看所有创建过的卷:

1
sudo docker volume list

删除一个卷:
1
sudo docker volume rm 卷名

删除所有没有容器使用的卷
1
sudo docker volume prune -a

删除容器

1
sudo docker rm -f ps中显示的容器id

正在运行的容器必须是使用-f

控制容器状态

因为使用docker run默认是会创建一个容器,那麽控制一个容器的启动和停止运用以下指令:
终止

1
sudo docker stop 容器名/id

启动
1
sudo docker start 名字/id

查看容器信息

1
sudo docker inspect 名称/id

查看容器日志:

1
sudo docker logs id

后面加上-f可以滚动查看日志

在容器中执行linux命令

1
docker exec id linux命令

深入调试:

1
docker exec id /bin/sh

容器内软件安装

查看容器linux发行版本

1
cat /etc/os-release

然后按照发行版的安装办法进行安装就行

Dockerfile

为你的项目创建一个Dockerfile文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 指定基础镜像(推荐使用官方镜像)
FROM python:3.11-slim-bullseye

# 设置元数据(可选)
LABEL maintainer="your.email@example.com"
LABEL version="1.0"
LABEL description="Production-ready Python application"

# 设置环境变量(推荐使用ENV)
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV APP_HOME=/app

# 创建工作目录并设置为工作路径
WORKDIR $APP_HOME

# 安装系统依赖(apt示例)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*

# 复制依赖文件并安装Python依赖(利用Docker缓存层)
COPY requirements.txt ./
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt

# 复制应用代码(放在依赖安装之后以利用缓存)
COPY . .

# 设置非root用户(安全最佳实践)
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

# 暴露端口(容器运行时监听端口)
EXPOSE 8000

# 设置健康检查(容器健康状态监控)
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8000/health || exit 1

# 设置容器启动命令(使用数组形式)
CMD ["gunicorn", "--bind", ":8000", "app.wsgi"]

目标检测svm算法

支持向量机(Support Vector Machine,SVM)是一种经典的监督学习算法,用于解决二分类和多分类问题。其核心思想是通过在特征空间中找到一个最优的超平面来进行分类,并且间隔最大。

二分类直观理解

他看起来很像我之前了解过的knn算法,假设我们有一组二维空间中的数据点,这些数据点属于两个类别,分别用不同颜色表示,比如蓝色点和红色点分布在二维平面上。我们的目标是找到一条直线(在二维情况下是直线,在高维情况下是超平面),能够把蓝色点和红色点分开。这就像在一个平面上摆放着很多蓝色和红色的小球,我们希望用一根棍子(直线)把它们隔开,而 SVM 就是帮助我们找到这根最合适的棍子。

支持向量的确定

支持向量就是那些离分类超平面最近的点。这些点对分类超平面的位置确定起到关键作用。

simple

硬间隔和软间隔

硬间隔分类:在上面我们使用超平面进行分割数据的过程中,如果我们严格地让所有实例都不在最大间隔之间,并且位于正确的一边,这就是硬间隔分类。

硬间隔分类有两个问题,首先,它只在数据是线性可分离的时候才有效;其次,它对异常值非常敏感。

当有一个额外异常值的鸢尾花数据:左图的数据根本找不出硬间隔,而右图最终显示的决策边界与我们之前所看到的无异常值时的决策边界也大不相同,可能无法很好地泛化。

hard

软间隔分类:要避免这些问题,最好使用更灵活的模型。目标是尽可能在保持最大间隔宽阔和限制间隔违例(即位于最大间隔之上,甚至在错误的一边的实例)之间找到良好的平衡,这就是软间隔分类。
soft

在Scikit-Learn的SVM类中,可以通过超参数C来控制这个平衡:C值越小,则间隔越宽,但是间隔违例也会越多。上图显示了在一个非线性可分离数据集上,两个软间隔SVM分类器各自的决策边界和间隔。

左边使用了高C值,分类器的错误样本(间隔违例)较少,但是间隔也较小。

右边使用了低C值,间隔大了很多,但是位于间隔上的实例也更多。看起来第二个分类器的泛化效果更好,因为大多数间隔违例实际上都位于决策边界正确的一边,所以即便是在该训练集上,它做出的错误预测也会更少。

代码演示

在matlab中具体运行一下svm的代码查看一下相关效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
%% 生成模拟数据(两个可线性分离的类别)
rng(1); % 固定随机种子确保可重复性
N = 50; % 每类样本数量

% 生成类别1数据(均值为[1 1],协方差矩阵为[0.9 0.2; 0.2 0.3])
class1 = mvnrnd([1, 1], [0.9, 0.2; 0.2, 0.3], N);

% 生成类别2数据(均值为[3 4],协方差矩阵为[0.9 0.2; 0.2 0.3])
class2 = mvnrnd([3, 4], [0.9, 0.2; 0.2, 0.3], N);

% 合并数据并添加标签
X = [class1; class2]; % 特征矩阵
y = [ones(N,1); -ones(N,1)]; % 标签:类别1为1,类别2为-1

%% 训练线性SVM模型
model = fitcsvm(X, y, ...
'KernelFunction', 'linear', ... % 使用线性核
'BoxConstraint', 1, ... % 正则化参数
'ClassNames', [1, -1]); % 指定类别标签

%% 可视化结果
figure;
hold on;

% 绘制数据点
gscatter(X(:,1), X(:,2), y, 'rb', 'o^', [], 'off');
title('SVM分类结果可视化');
xlabel('特征1'); ylabel('特征2');
legend('类别1', '类别2', 'Location', 'northwest');

% 绘制决策边界
w = model.Beta; % 获取权重向量
b = model.Bias; % 获取偏置项
f = @(x1,x2) w(1)*x1 + w(2)*x2 + b; % 决策函数

% 创建网格用于绘制决策边界
h = fimplicit(f, [min(X(:,1))-0.5 max(X(:,1))+0.5 ...
min(X(:,2))-0.5 max(X(:,2))+0.5]);
h.Color = 'k';
h.LineWidth = 2;
h.LineStyle = '-';

% 绘制支持向量
sv = model.SupportVectors;
plot(sv(:,1), sv(:,2), 'ko', 'MarkerSize', 10, 'LineWidth', 1);

% 绘制间隔边界
fcontour(@(x1,x2) f(x1,x2), [min(X(:,1))-0.5 max(X(:,1))+0.5 ...
min(X(:,2))-0.5 max(X(:,2))+0.5], ...
'LevelList', [-1 1], ... % 间隔边界位于f(x)=±1处
'LineStyle', ':', ...
'LineColor', 'k');

% 添加图例
legend('类别1', '类别2', '决策边界', '支持向量', '间隔边界');
grid on;
hold off;

%% 显示模型信息
fprintf('SVM模型参数:\n');
fprintf('权重 w = [%.4f, %.4f]\n', w(1), w(2));
fprintf('偏置 b = %.4f\n', b);
fprintf('支持向量数量: %d\n', size(sv,1));

while true
% 提示用户输入坐标
prompt = {'请输入特征1的值:', '请输入特征2的值:'};
dlgtitle = 'SVM分类预测';
dims = [1 35];
definput = {'2', '3'}; % 默认值

answer = inputdlg(prompt, dlgtitle, dims, definput);

% 如果用户点击取消则退出
if isempty(answer)
break;
end

% 获取输入值
x1 = str2double(answer{1});
x2 = str2double(answer{2});

% 验证输入有效性
if isnan(x1) || isnan(x2)
errordlg('请输入有效的数值!', '输入错误');
continue;
end

% 预测类别
[pred_label, score] = predict(model, [x1, x2]);

% 显示结果
if pred_label == 1
class_name = '类别1';
color = 'b';
else
class_name = '类别2';
color = 'r';
end

% 在图上标记该点
figure(gcf); % 回到当前图形
hold on;
h_point = plot(x1, x2, 's', 'MarkerSize', 12, 'LineWidth', 2, ...
'MarkerFaceColor', color, 'MarkerEdgeColor', 'k');

% 显示信息框
dist_to_boundary = f(x1, x2); % 计算到决策边界的函数距离
msg = sprintf('预测结果: %s\n决策函数值: %.4f\n到边界距离: %.4f', ...
class_name, score(2), dist_to_boundary);
h_text = text(x1+0.1, x2+0.1, msg, 'FontSize', 10, ...
'BackgroundColor', [1 1 0.8], 'EdgeColor', 'k');

% 添加图例
legend('类别1', '类别2', '决策边界', '支持向量', '间隔边界', '预测点');

% 等待用户点击继续
uiwait(msgbox(msg, '预测结果', 'modal'));

% 清除临时标记
delete(h_point);
delete(h_text);
end

运行结果:

1
2

svm算法的优点和通常使用流程

1)SVM方法既可以用于分类(二/多分类),也可用于回归和异常值检测。

2)SVM具有良好的鲁棒性,对未知数据拥有很强的泛化能力,特别是在数据量较少的情况下,相较其他传统机器学习算法具有更优的性能。

  • 对样本数据进行归一化

  • 应用核函数对样本进行映射(最常采用和核函数是RBF和Linear,在样本线性可分时,Linear效果要比RBF好)

  • 用cross-validation和grid-search对超参数进行优选

  • 用最优参数调练得到模型

  • 测试

算法原理

SVM通过优化一个凸二次规划问题来求解最佳的超平面,其中包括最小化模型的复杂度(即最小化权重的平方和),同时限制训练样本的误分类情况。这个优化问题可以使用拉格朗日乘子法来求解。对于非线性可分的情况,SVM可以通过核函数(Kernel Function)将输入特征映射到高维空间,使得原本线性不可分的数据在高维空间中变得线性可分。常用的核函数包括线性核、多项式核、高斯核等。

好的,以下是修改后的内容:

在支持向量机(SVM)中,分割超平面公式为:

其中:

  • (w^T x) :这是向量 (w) 和向量 (x) 的点积。向量 (w) 是权重向量,它垂直于分割超平面,决定了超平面的方向。向量 (x) 是样本的特征向量。点积 (w^T x) 反映了样本点 (x) 在权重向量 (w) 方向上的投影长度。
  • (b) :它是偏置项,用于确定超平面在特征空间中的位置,相当于对超平面的平移量。通过调整 (b) 的值,可以在保持超平面方向不变的情况下,改变超平面距离原点的距离,从而影响分类边界的位置。

分割超平面的作用是将不同类别的样本分开。对于一个给定的样本 (x),当

时,模型预测该样本属于正类,正类使用1表示;当

时,预测为负类,负类使用-1表示;而当

时,样本点正好位于超平面上。

分类器求解优化问题

现在我们需要做的就是找出$w$和$b$。

确定数据点到分割面的距离并确定分割面放置位置时,间隔通过来计算他的函数间距(label一般取1或者-1)

注:为点到分割面的几何距离。

找到具有最小间隔的数据点,对间隔进行最大化,写作:

公式解释:

$\arg \max _{w,b}$ 表示我们要找到使大括号内的表达式达到最大值的 $w$ 和 $b$ 组合。而大括号内的部分是一个最小值函数 $\min (…) $,所以我们是在寻找能够使这个最小值尽可能大的 $w$ 和 $b$。

带约束条件的优化问题会让人很容易想到拉格朗日乘子法。

拉格朗日乘子法

问题定义

给定优化问题:

其中:

  • $f: \mathbb{R}^n \rightarrow \mathbb{R}$ 是目标函数
  • $g_i: \mathbb{R}^n \rightarrow \mathbb{R}$ 是等式约束
  • $\mathbf{x} \in \mathbb{R}^n$ 是优化变量

核心原理

在约束极值点 $\mathbf{x}^*$ 处,目标函数梯度与约束函数梯度线性相关:

其中 $\lambda_i$ 称为拉格朗日乘子。

拉格朗日函数

构造增广函数:

其中 $\boldsymbol{\lambda} = [\lambda_1, …, \lambda_m]^T$ 是乘子向量。

极值必要条件

极值点 $(\mathbf{x}^, \boldsymbol{\lambda}^)$ 满足:

  1. 梯度条件

  2. 原始可行性条件

求解步骤

  1. 构造拉格朗日函数 $\mathcal{L}(\mathbf{x}, \boldsymbol{\lambda})$
  2. 求偏导数方程组:
  3. 解 $(n+m)$ 个方程组成的方程组
  4. 验证解的极值性质(极小/极大)

不等式约束扩展 (KKT条件)

对于不等式约束 $h_j(\mathbf{x}) \leq 0$:

  1. 梯度条件

  2. 原始可行性

  3. 乘子非负性

  4. 互补松弛条件

关键性质

  1. 梯度对齐:约束梯度张成目标梯度的正交补空间
  2. 边界激活:不等式约束在 $\mu_j > 0$ 时等价于等式约束
  3. 敏感性:$\lambda_i$ 表示约束 $g_i$ 变化对目标值的影响率:其中 $g_i(\mathbf{x}) = c_i$

应用条件

  1. 函数 $f$ 和 $g_i$ 连续可微
  2. 约束梯度 $\nabla g_i$ 在极值点线性无关(约束规范)
  3. 对于凸问题,KKT条件是充分必要条件

此框架将约束优化转化为无约束问题求解,是连续优化理论的核心工具。

公式换算

这里的约束调节都是基于数据点的,因此我们需要将超平面公式写成数据点形式:

图中的公式是对支持向量机(SVM)的优化问题进行推导后得到的形式,其推导过程涉及从原始的分类间隔最大化问题到对偶问题的转换。以下是详细的推导步骤:

原始优化问题

SVM 的原始优化目标是最大化分类间隔,同时确保所有数据点被正确分类。分类间隔的大小与 (\frac{1}{|w|}) 成正比,因此我们希望最大化 (\frac{1}{|w|}),等价于最小化 (|w|^2)。同时,约束条件是每个数据点满足 (y_i \times (w^T x_i + b) \geq 1)。

原始优化问题可以表示为:
[
\begin{align}
\min_{w, b} \quad & \frac{1}{2} |w|^2 \
\text{subject to} \quad & y_i \times (w^T x_i + b) \geq 1, \quad i = 1, 2, \ldots, N
\end{align
}
]

引入拉格朗日乘子

为了求解这个带约束的优化问题,我们引入拉格朗日乘子 (\alphai \geq 0)((i = 1, 2, \ldots, N)),构建拉格朗日函数:
[
\mathcal{L}(w, b, \alpha) = \frac{1}{2} |w|^2 - \sum
{i=1}^N \alpha_i \left( y_i \times (w^T x_i + b) - 1 \right)
]

对偶问题

我们通过对拉格朗日函数进行极小化和极大化操作,将原始的约束优化问题转换为对偶问题。具体步骤如下:

  1. 对 (w) 和 (b) 进行极小化:
    [
    \min_{w, b} \mathcal{L}(w, b, \alpha)
    ]

  2. 对 (\alpha) 进行极大化:
    [
    \max{\alpha} \min{w, b} \mathcal{L}(w, b, \alpha)
    ]

求导并代入

通过对拉格朗日函数分别对 (w) 和 (b) 求导,并将导数等于零的条件代入,可以得到:

  1. 对 (w) 求导并令导数为零:
    [
    \frac{\partial \mathcal{L}}{\partial w} = w - \sum{i=1}^N \alpha_i y_i x_i = 0 \implies w = \sum{i=1}^N \alpha_i y_i x_i
    ]

  2. 对 (b) 求导并令导数为零:
    [
    \frac{\partial \mathcal{L}}{\partial b} = -\sum{i=1}^N \alpha_i y_i = 0 \implies \sum{i=1}^N \alpha_i y_i = 0
    ]

代入拉格朗日函数

将 (w = \sum{i=1}^N \alpha_i y_i x_i) 和 (\sum{i=1}^N \alpha_i y_i = 0) 代入拉格朗日函数 (\mathcal{L}(w, b, \alpha)),得到:

[
\mathcal{L} = \sum{i=1}^N \alpha_i - \frac{1}{2} \sum{i=1}^N \sum_{j=1}^N \alpha_i \alpha_j y_i y_j (x_i \cdot x_j)
]

对偶问题的优化目标

最终,对偶问题的优化目标变为:
[
\max{\alpha} \left[ \sum{i=1}^N \alphai - \frac{1}{2} \sum{i=1}^N \sum{j=1}^N \alpha_i \alpha_j y_i y_j (x_i \cdot x_j) \right]
]
其中,(\alpha_i \geq 0) 且 (\sum
{i=1}^N \alpha_i y_i = 0)。

这个公式表示我们通过最大化这个目标函数来找到最优的拉格朗日乘子 (\alpha_i),进而得到 (w) 和 (b),从而确定最优分类超平面。

前言

在学习和阅读相关多模态目标检测的论文时因为对其中的很多相关名词和核心方法不了解需要额外的学习补充为了防止大量的方法在学习之后忘记所以将他们每次学习的时候汇总到本章节中,利用博客自带的搜索功能进行快速查找复习方便后续的学习阅读。

目标检测的图像特征提取之HOG特征

方向梯度直方图(Histogram of Oriented Gradient, HOG)特征是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子。它通过计算和统计图像局部区域的梯度方向直方图来构成特征。Hog特征结合SVM分类器已经被广泛应用于图像识别中,尤其在行人检测中获得了极大的成功。需要提醒的是,HOG+SVM进行行人检测的方法是法国研究人员Dalal在2005的CVPR上提出的,而如今虽然有很多行人检测算法不断提出,但基本都是以HOG+SVM的思路为主。

核心算法

  • 灰度化(将图像看做一个x,y,z(灰度)的三维图像)

  • 采用Gamma校正法对输入图像进行颜色空间的标准化(归一化);目的是调节图像的对比度,降低图像局部的阴影和光照变化所造成的影响,同时可以抑制噪音的干扰;

公式:$I_{\text{norm}} = \sqrt{I}$

  • 计算图像每个像素的梯度(包括大小和方向);主要是为了捕获轮廓信息,同时进一步弱化光照的干扰。

使用中心差分法:
水平梯度 $G_x$:$I(x+1,y) - I(x-1,y)$
垂直梯度 $G_y$:$I(x,y+1) - I(x,y-1)$

梯度幅值:$\sqrt{G_x + G_y} $
梯度方向:$\arctan(G_x/G_y) $

  • 将图像划分成小cells(例如6*6像素/cell)

  • 统计每个cell的梯度直方图(不同梯度的个数),即可形成每个cell的descriptor

  • 将每几个cell组成一个block(例如3*3个cell/block),一个block内所有cell的特征descriptor串联起来便得到该block的HOG特征descriptor。

  • 将图像image内的所有block的HOG特征descriptor串联起来就可以得到该image(你要检测的目标)的HOG特征descriptor了。这个就是最终的可供分类使用的特征向量了。

这是对目标检测经典论文Histograms of Oriented Gradients for Human Detection的学习和复刻下面是代码复现过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;

/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param float c 常数,用于控制变化幅度
* @param float gamma 指数
* @breif 伽马变换
*/
void GammaGrayTrans(const cv::Mat& src, cv::Mat& dst, float c, float gamma)
{
//建立灰度映射表
float grayTable[256];
for (int i = 0; i < 256; i++)
{
grayTable[i] = c * pow(i, gamma);
}

//遍历修改灰度值
int temp = 0;
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
temp = src.at<uchar>(i, j);
dst.at<float>(i, j) = grayTable[temp];
}

//将数据缩放到0-255
cv::normalize(dst, dst, 0, 255, cv::NORM_MINMAX);
//dst的数据类型还原为CV_8UC1
dst.convertTo(dst, CV_8UC1);
}

/*
* @param cv::Mat angleImg 梯度方向矩阵
* @param cv::Mat magnitudeImg 梯度幅值矩阵
* @param cv::Size cellSize cell的尺寸
* @param std::vector<std::vector<double>> cellVector 存储每个cell的特征向量
* @brief 计算每个cell的特征向量
*/
void getCellVector(cv::Mat& angleImg,cv::Mat& magnitudeImg,cv::Size cellSize, std::vector<std::vector<double>>& cellVector)
{
for(int cell_i=0; cell_i < angleImg.rows; cell_i=cell_i+cellSize.height)
for (int cell_j = 0; cell_j < angleImg.cols; cell_j=cell_j + cellSize.width)
{
//映射直方图
//[0, 180] 度以20度为一个bin,平均分成9份
std::vector<double> table(9);
int temp_floor = 0;
double scale = 0;
for (int i = cell_i; i < cell_i + cellSize.height; i++)
for (int j = cell_j; j < cell_j + cellSize.width; j++)
{
temp_floor = std::floor(angleImg.at<float>(i, j) / 20); //向下取整
switch (temp_floor) {
case 0:
scale = (angleImg.at<float>(i, j) - 0) / 20;
table[0] += magnitudeImg.at<float>(i, j) * scale;
table[1] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 1:
scale = (angleImg.at<float>(i, j) - 20) / 20;
table[1] += magnitudeImg.at<float>(i, j) * scale;
table[2] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 2:
scale = (angleImg.at<float>(i, j) - 40) / 20;
table[2] += magnitudeImg.at<float>(i, j) * scale;
table[3] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 3:
scale = (angleImg.at<float>(i, j) - 60) / 20;
table[3] += magnitudeImg.at<float>(i, j) * scale;
table[4] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 4:
scale = (angleImg.at<float>(i, j) - 80) / 20;
table[4] += magnitudeImg.at<float>(i, j) * scale;
table[5] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 5:
scale = (angleImg.at<float>(i, j) - 100) / 20;
table[5] += magnitudeImg.at<float>(i, j) * scale;
table[6] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 6:
scale = (angleImg.at<float>(i, j) - 120) / 20;
table[6] += magnitudeImg.at<float>(i, j) * scale;
table[7] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 7:
scale = (angleImg.at<float>(i, j) - 140) / 20;
table[7] += magnitudeImg.at<float>(i, j) * scale;
table[8] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 8:
table[8] += magnitudeImg.at<float>(i, j);
break;
}
}
cellVector.push_back(table);
}
}

/*
* @param cv::Mat src 输入图像(CV_8U),灰度图
* @param cv::Mat dst 输出图像
* @param int thresh OTSU最大类间方差的灰度值
* @brief 计算HOG描述子
*/
void getHOGDescriptor(cv::Mat& src, std::vector<double>& HOGVector,
cv::Mat& gammaOut, cv::Mat& magOut, cv::Mat& angOut)
{
//gamma变换
cv::Mat gammaImg(src.size(),CV_32F);
GammaGrayTrans(src, gammaImg, 1, 0.5);
gammaOut = gammaImg.clone();

//计算梯度
cv::Mat sobel_x, sobel_y;
cv::Sobel(gammaImg, sobel_x, CV_32F, 1, 0);
cv::Sobel(gammaImg, sobel_y, CV_32F, 0, 1);
cv::Mat magnitudeImg(sobel_x.size(),CV_32F), angleImg(sobel_x.size(), CV_32F);
double temp_mag = 0, temp_angle = 0;
for (int i = 0; i < sobel_x.rows; i++)
for (int j = 0; j < sobel_x.cols; j++)
{
temp_mag = cv::abs(sobel_x.at<float>(i, j)) + cv::abs(sobel_y.at<float>(i, j));
temp_angle = cv::fastAtan2(sobel_y.at<float>(i, j), sobel_x.at<float>(i, j));

magnitudeImg.at<float>(i, j) = temp_mag;

if ((temp_angle > 180)) temp_angle -= 180; //将角度映射到0-180°,成为“无符号”梯度
angleImg.at<float>(i, j) = temp_angle;
}

// 保存中间结果
magOut = magnitudeImg.clone();
angOut = angleImg.clone();

//构建直方图
//设置cell的尺寸8*8,获得每个cell的向量
cv::Size cellSize(8,8);
std::vector<std::vector<double>> cellVector;
getCellVector(angleImg, magnitudeImg, cellSize, cellVector);

// 计算每个block向量,选用2*2的block
std::vector < std::vector < double >> blockVector;
int block_i_end = angleImg.rows / cellSize.height;
int block_j_end = angleImg.cols / cellSize.width;
for (int block_i = 0; block_i < block_i_end-1; block_i++) {
for (int block_j = 0; block_j < block_j_end-1; block_j++)
{
std::vector<double> block;
block.insert(block.end(), cellVector[block_i*block_j_end+block_j].begin(), cellVector[block_i * block_j_end + block_j].end());
block.insert(block.end(), cellVector[block_i * block_j_end + block_j + 1].begin(), cellVector[block_i * block_j_end + block_j + 1].end());
block.insert(block.end(), cellVector[(block_i+1) * block_j_end + block_j].begin(), cellVector[(block_i+1) * block_j_end + block_j].end());
block.insert(block.end(), cellVector[(block_i+1) * block_j_end + block_j + 1].begin(), cellVector[(block_i+1) * block_j_end + block_j + 1].end());
cv::normalize(block, block, 1, cv::NORM_L1);
blockVector.push_back(block);
}
}

//构建HOG特征描述子,合并每个block
for (int i = 0; i < blockVector.size(); i++)
{
for (int j = 0; j < blockVector[i].size(); j++)
{
HOGVector.push_back(blockVector[i][j]);
}
}
}

// 可视化HOG特征
void visualizeHOG(cv::Mat& img, std::vector<double>& hogVector, cv::Size winSize) {
const int CELL_SIZE = 8;
const int BIN_NUM = 9;
const float DEG_PER_BIN = 180.0 / BIN_NUM;
const float MAX_LEN = 6.0; // 箭头最大长度

cv::Mat hogImg = img.clone();
cv::cvtColor(hogImg, hogImg, cv::COLOR_GRAY2BGR);

int cellsInX = winSize.width / CELL_SIZE;
int cellsInY = winSize.height / CELL_SIZE;

for (int cy = 0; cy < cellsInY - 1; cy++) {
for (int cx = 0; cx < cellsInX - 1; cx++) {
int cellOffset = (cy * (cellsInX - 1) + cx) * BIN_NUM * 4;

for (int by = 0; by < 2; by++) {
for (int bx = 0; bx < 2; bx++) {
int cellY = cy * CELL_SIZE + by * CELL_SIZE;
int cellX = cx * CELL_SIZE + bx * CELL_SIZE;

cv::Point center(cellX + CELL_SIZE/2, cellY + CELL_SIZE/2);

for (int bin = 0; bin < BIN_NUM; bin++) {
float binValue = hogVector[cellOffset + (by * 2 + bx) * BIN_NUM + bin];
float angle = bin * DEG_PER_BIN;
float radian = angle * CV_PI / 180.0;

// 计算箭头方向
float dx = cos(radian) * binValue * MAX_LEN;
float dy = sin(radian) * binValue * MAX_LEN;

cv::Point direction(static_cast<int>(dx), static_cast<int>(dy));
cv::line(hogImg, center, center + direction,
cv::Scalar(0, 255, 0), 1, cv::LINE_AA);
}
}
}
}
}

cv::imshow("HOG Visualization", hogImg);
}

int main()
{
//读取图片
string filepath = "/home/song/object_detect_lab/HOGtestCPP/pic/miku.jpg";
cv::Mat src = cv::imread(filepath, cv::IMREAD_GRAYSCALE);
if (src.empty())
{
std::cout << "imread error" << std::endl;
return -1;
}
cv::resize(src, src, cv::Size(64, 128));
cv::imshow("Original Image", src);

// 存储中间结果
cv::Mat gammaImg, magImg, angImg;
std::vector<double> HOGVector;
getHOGDescriptor(src, HOGVector, gammaImg, magImg, angImg);

// 1. 显示伽马变换结果
cv::imshow("1Gamma Transform", gammaImg);

// 2. 显示梯度幅值(归一化)
cv::Mat magShow;
cv::normalize(magImg, magShow, 0, 255, cv::NORM_MINMAX);
magShow.convertTo(magShow, CV_8U);
cv::imshow("2Gradient Magnitude", magShow);

// 3. 显示梯度方向(转换为角度图)
cv::Mat angShow;
angImg.convertTo(angShow, CV_32F);
angShow = angShow * 255.0 / 180.0; // 0-180°映射到0-255
angShow.convertTo(angShow, CV_8U);
cv::imshow("3Gradient Orientation", angShow);

// 4. 打印HOG特征信息
std::cout << "HOG Descriptor Size: " << HOGVector.size() << std::endl;
std::cout << "First 10 values: ";
for(int i = 0; i < 10 && i < HOGVector.size(); i++) {
std::cout << HOGVector[i] << " ";
}
std::cout << std::endl;

// 5. 可视化HOG特征
visualizeHOG(src, HOGVector, src.size());

cv::waitKey(0);
return 0;
}

得到灰度化的原始图片:

original

原始图片灰度化后用Gamma校正法对输入图像进行颜色空间的标准化:

伽马变换结果

梯度方向角度图:

梯度方向(转换为角度图).png)

梯度方向幅值图:

梯度幅值(归一化)

HOG示意图:

HOG

学习思考

如果按照我的复现过程一张维64×128的图片其得到的HOG检测子应该为3780维,而在opencv的HOG特征提取代码getDefaultPeopleDetector()中却得到了3781维我们来看一下原因以免在以后被疯狂困扰。

这是因为另外一维是一维偏移

利用hog+svm检测行人,最终的检测方法是最基本的线性判别函数,wx + b = 0,刚才所求的3780维向量其实就是w,而加了一维的b就形成了opencv默认的3781维检测算子,而检测分为train和test两部分,在train期间我们需要提取一些列训练样本的hog特征使用svm训练最终的目的是为了得到我们检测的w以及b,在test期间提取待检测目标的hog特征x,带入方程是不是就能进行判别了呢?

前言

目前需要研究的多模态信息融合增强感知课题其中很大的一部分就是多模态的目标检测技术,作为入门我需要阅读这篇比较全面同时具有开源代码的论文。虽然在github的issue中针对代码的效果有一系列的问题,但是阅读和部署学习他的理念和技术手段也是有收获的。

绪论部分

简单介绍了该多模态目标检测代码采用的是RGB和热成像(提供弱光下的特征)进行融合,其中提到proben是一种简单的非学习算法,其是用贝叶斯规则和第一原理推导出来的。

贝叶斯规则

通俗易懂的例子:我们在大街上看到一个长得五大三粗,有花臂,带着大金链子,穿着个大花褂子的大汉,那么我们很有可能会觉得此人非好人,避而远之。原因是该人在电视剧中典型的反派打扮,我们都非常熟悉,给出了一个非常主观的判断。然后过了一会,你看到了大汉去扶了老奶奶过马路,此时我们又会觉得此人是好人的概率非常大,这是为什么?

我们用数学的思维想象一下这两个问题:

  • 第一步:最开始我们的判断都是根据我们以往的经验作为依据进行的主观判断,我们可以给出一个老土的叫法,称为先验判断;

  • 第二步:然后我们从他们的言行中获取到了对他们的一些新的认识,我们可以将所获取到的他们的言行当做是一些新的数据;

  • 第三步:我们根据获得的新数据,对之前的先验判断进行一个修正,得到一个新的判断,我们依旧给它取一个老土的名字,就叫后验判断。

  • 第三步:我们根据获得的新数据,对之前的先验判断进行一个修正,得到一个新的判断,我们依旧给它取一个老土的名字,就叫后验判断。

聊到这里,我们可以做一个小总结:我们通过新获取的数据来修正我们作出的先验判断,最后得到我们新的后验判断,这就是贝叶斯的基本原理。

核心贝叶斯公式:

公式推导:

贝叶斯推导图

贝叶斯公式推导

1. 条件概率定义

2. 联合概率关系

由公式 (1.2) 得:

3. 代入条件概率公式

将公式 (1.3) 代入公式 (1.1):

后验概率(posterior probability):$ P(A|B) $

似然值(likelihood):$ P(B∣A) $

先验概率(prior probability):$ P(A) $

4. 全概率公式展开分母

其中 $A_i$ 是样本空间的完备事件组

5. 最终贝叶斯公式

简化版(当 $A$ 和 $A^c$ 构成完备事件组时)

第一原则

第一性原理假设了多模态之间的条件独立性。在 ProbEn(概率集成)方法的上下文中,”first principles”(第一性原理)指的是构建该方法所依赖的最基本、最底层的概率论公理和定义,而非基于现有模型或经验的推导。具体来说,这些第一性原理包括:

1. 概率论基本公理

2. 条件概率定义

3. 独立性公理

4. 条件独立性假设 (ProbEn 核心原理)

对于模态观测 $ M_1, M_2 $ 和目标状态 $ S $:

5. 全概率公式

介绍

列举了简单检测堆叠会出现多目标检测结果堆叠的问题,提出了NMS(非最大值抑制)的解决方案。

NMS非最大值抑制

简单来说就是抑制不是极大值的元素,目标检测与图像分类不同,图像分类往往只有一个输出,但目标检测的输出个数却是未知的。除了Ground-Truth(标注数据)训练,模型永远无法百分百确信自己要在一张图上预测多少物体。

NMS伪代码:

fakecodeNMS

虽然有点反直觉但是情况确实如上述所说,nms的比对算法是这样进行的,选择一个元素与左右相邻的两个元素进行比较,如果他比相邻的元素都大那麽将会直接输出为maximumAt(i)。
如果出现I[i]<=I[i+1]的情况函数会直接跳转到相邻的I[i+1]将其更新为新的I[i]进行下一步比对。
在本文中将NMS也作为一种多模态融合地策略,非极大值抑制(NMS)未能融合来自低置信度模态的线索。

IoU(交并比):
IoU

还使用了一个重叠检测平均值作为对比(检测框因该是NMS得来的)

compare

总结:所有的一般融合方式都是1+1<1的效果他们大多都是降低了检测的分数。而proben就是要实现1+1>2的效果。

Multimodal Data.

  • 缺乏多模态配对数据数据

  • 对准RGB和热成像需要专业设备(如果后期考虑多模融合可以优先考虑RGB和深度图像结合)

许多未对齐数据集和仅仅标注了一种模态的的数据集使多模态学习变得复杂。

本文中提到了KAIST行人检测数据集其中就涉及了热成像和RGB。

Multimodal Fusion.

  • 早期融合:早期融合构建了一个四通道RGB加热输入,然后由(典型的)深度网络处理。

  • 中期融合:midfusion将RGB和热输入保持在不同的流中,然后在网络中合并它们的下游特征。

  • 晚期融合和中期融合对比:

mid compare last

本文的工作更多的是从不同检测器的检测结果进行融合而非不同检测输入的特征。所以他在使用过程中也需要注意处理某个1探测器失效时的失踪问题。

Multimodal Object Detection via Probabilistic Ensembling

fcode

算法 1

我们假设有一个带有标签 $y$(例如,”人”)的对象,并从两种模态测量信号:$x_1$(RGB)和 $x_2$(热成像)。我们写出适用于两种模态的公式,但扩展到多种模态(在我们的实验中进行了评估)是直接的。至关重要的是,我们假设测量在给定对象标签 $y$ 的条件下是条件独立的:

这也可以写成 $p(x_1 | y) = p(x_1 | x_2, y)$,这可能更容易直观理解。给定人员标签 $y$,预测其 RGB 外观 $x_1$;如果此预测不会改变对热信号 $x_2$ 的已有知识,则条件独立性成立。我们希望根据多模态测量推断标签:

通过将条件独立性假设从公式 (2.1) 应用到公式 (2.2),我们得到:

以上建议了一种简单的融合方法,当单模态特征在给定真实对象标签的条件下是条件独立的时,该方法是可证明的最优的:

  1. 训练独立的单模态分类器,以预测给定每个单独特征模态的标签 $y$ 的分布 $p(y | x_1)$ 和 $p(y | x_2)$。
  2. 通过将两个分布相乘、除以类别先验分布,并将最终结果(公式 4)标准化为总和为一,来生成最终分数。

tip:此处我有一点无法理解$ p(y) $按照直接理解他是这张图片是否有人的概率,这是一个令人疑惑的参数,查找以后我发现他可能是收到训练数据中有人和无人数据比例影响的一个参数,比如训练数据中一半是有人的一半是没有人的那么其就会等于0.5.(这是我目前的猜想,现在再继续阅读下文后其对$p(y)$ 有了更加深入的介绍)

关于获取类别先验概率及 ProbEn 方法的扩展

为了获得类别先验概率 $p(y)$,我们可以简单地对每个类别的样本数量进行归一化处理。将 ProbEn(公式 4)扩展到 $M$ 种模态是简单的:

这里公式 (2.5) 详细阐述了如何将 ProbEn 方法从两种模态扩展到 $M$ 种模态的情况。其中 $p(y)$ 是类别先验概率,通过归一化每个类别的样本计数得到。分子部分是 $M$ 种模态下条件概率的乘积,分母部分则是类别先验概率的 $M-1$ 次方,这样的形式体现了多模态融合过程中对各类别先验信息的综合考量。

归一化

公式 (2.5) 中提到的归一化操作,是将每个类别的样本计数进行处理,使得所有类别的概率之和为 1,具体步骤如下:

  1. 统计样本计数:对于每个类别 $y$,统计训练集中属于该类别的样本数量。
  2. 计算总样本数:将所有类别的样本数量相加,得到训练集的总样本数。
  3. 计算归一化因子:对于每个类别 $y$,将其样本数量除以总样本数,得到 $p(y)$。

这种归一化操作确保了所有类别先验概率之和为 1,符合概率的基本性质。通过这种方式,类别先验概率 $p(y)$ 反映了训练数据中各类别的相对频率,为模型提供了关于类别分布的先验信息。

是的!上面的内容进一步详细解释了 $p(y)$ 的作用和处理方式,尤其是与 ProbEn 方法的关系。以下是对 $p(y)$ 的更详细解释:

与 ProbEn 的关系

  • 在公式 中,分母 $p(y = k)^{M-1}$ 直接体现了 $p(y)$ 在计算多模态后验概率时的作用。ProbEn 方法通过对各单模态的条件概率进行处理(求积后除以 $p(y = k)^{M - 1}$),实现了对多模态数据的融合,这表明 $p(y)$ 是融合过程中的一个重要参数。

确定方式

  • 基于数据集统计 :如果训练数据集中有每个类别的样本占比,如类别 $y_1$ 占 $30\%$,类别 $y_2$ 占 $70\%$,则 $p(y_1) = 0.3$,$p(y_2) = 0.7$。
  • 假设均匀分布 :当对类别分布没有特别了解时,通常假设每个类别的先验概率相等。例如一共有 $C$ 个类别,则 $p(y_i) = \frac{1}{C}$($i = 1,2,…,C$)。
  • 领域知识 :在一些特定领域,如医学诊断中,某些疾病的发病率是已知的,就可以用发病率作为该疾病标签的先验概率。

处理缺失模态时的作用

  • 在处理缺失模态时,ProbEn 能够优雅地应对。因为它基于概率归一化的多模态后验概率 $p(y|x_1, x_2)$,这些后验概率可以直接与单模态后验概率 $p(y|x_1)$ 进行比较,无需额外复杂的处理步骤,这一过程也体现了 $p(y)$ 的重要性。

总之,这段内容对 $p(y)$ 的来源、确定方法和作用等进行了深入解释,更加明确了 $p(y)$ 在 ProbEn 方法及多模态融合中的重要地位。

logits 和 softmax函数

logits 是机器学习中的一个术语,特别是在分类问题中频繁出现。它是模型输出的原始得分,通常是一个实数值,需要经过进一步处理(如 softmax 函数)才能转换为概率。

logits 的定义

logits 可以理解为模型对输入数据属于某一类别的 “置信度” 或 “偏好度” 的一种度量。它本质上是模型的最后一层输出,尚未经过激活函数(如 softmax 或 sigmoid)转换为概率值。

数学表示

假设有一个分类模型,对于输入 $x$,模型输出一个向量 $s = [s_1, s_2, \ldots, s_n]$,其中 $s_i$ 表示输入 $x$ 属于类别 $i$ 的 logit 得分。这些得分通常是通过模型的最后一层(通常是全连接层)计算得到的。

与概率的关系

logits 得分需要通过 softmax 函数转换为概率分布。softmax 函数将 logits 向量 $s$ 转换为概率向量 $p = [p_1, p_2, \ldots, p_n]$,其中:

在 ProbEn 方法中的作用

在 ProbEn 方法中,logits 是单模态模型的输出。这些 logits 被用来计算条件概率 $p(y|x_i)$,然后在多模态融合过程中进行求和和归一化处理。

示例

假设一个模型有两个输出类别,logits 得分为 $s_1 = 2.0$ 和 $s_2 = 1.0$。通过 softmax 函数计算得到的概率为:

这个例子展示了 logits 如何被转换为概率,从而用于模型的预测。

简单来说,logits 是模型对不同类别的原始预测得分,经过 softmax 函数处理后得到概率分布。在 ProbEn 方法中,logits 是单模态模型的输出,用于后续的多模态融合和概率计算。

关于 ProbEn 方法

我们用单模态 logit 得分 $s_i[k]$ 来表示类别 $k$ 在模态 $i$ 下的概率。为简化符号,我们忽略其对底层输入模态 $x_i$ 的依赖:$p(y = k|x_i) = \frac{\exp(s_i[k])}{\sum_j \exp(s_i[j])} \propto \exp(s_i[k])$,这里我们利用了分母中的分区函数不是类别标签 $k$ 的函数这一特性。现在我们将上述表达式代入公式 (2.5):

ProbEn 实际上等价于对 logit 得分求和,除以类别先验概率,然后通过 softmax 进行归一化。我们的推导(公式 3.1)表明,如果不进行除法操作,仅对 logit 得分求和可能会过度计算类别先验,且这种过度计算会随着模态数量 $M$ 的增加而增长。补充材料表明,假设类别后验概率 $p(y)$ 均匀分布可以显著改善这种情况。在实践中,我们通过实验发现,即使在类别不平衡的数据集上,假设均匀先验概率也能取得出奇的良好效果。除非另有说明,这在我们的实验中是默认设置。

缺失模态

重要的是,在处理”缺失”模态时,求和和求平均的行为有显著差异。直观上看,不同的单模态检测器通常不会对同一对象同时触发。这意味着为了输出高于置信度阈值的最终检测结果(例如,用于计算精确度 - 召回率指标),需要将融合多模态检测的分数与单模态检测的分数进行比较。ProbEn 能够优雅地处理缺失模态,因为概率归一化的多模态后验概率 $ p(y|x_1, x_2) $ 可以直接与单模态后验概率 $ p(y|x_1) $ 进行比较。

边框处理

在边框处理上也采用(公式4)的办法将用 z 表示与给定检测相关的连续边界框随机变量(通过其中心坐标、宽度和高度来参数化)。

赋予检测框随机性,这里我们采用高斯分布可以写成:

然后提供了三种对边框的处理方案中设置 $\sigma_i^2$的方法 :

  • 第一种方法 “avg” 固定 $\sigma_i^2 = 1$,相当于直接对边界框坐标进行简单平均。

  • 第二种方法 “s - avg” 近似 $\sigma_i^2 \approx \frac{1}{p(y = k|x_i)}$,这意味着在融合边界框坐标时,更有信心的检测结果应具有更高的权重。这比简单的平均略好。

  • 第三种方法 “v - avg” 训练检测器以使用高斯负对数似然(GNLL)损失来预测回归方差/不确定性,同时还进行边界框回归损失训练。有趣的是,纳入 GNLL 不仅产生了更好的方差/不确定性估计,有助于融合,还提高了训练检测器的检测性能(详细信息见补充材料)。

逆协方差矩阵

在多元高斯分布中:

逆协方差矩阵 $\Sigma^{-1}$ 出现在指数项中,影响分布的形状。它衡量了数据点偏离均值时的概率密度下降速度。

高斯负对数似然(GNLL)

GNLL 是 “Gaussian Negative Log Likelihood Loss” 的缩写,即高斯负对数似然损失函数。它是一种用于回归任务中的损失函数,与普通的均方误差(MSE)损失不同,GNLL 不仅考虑预测值和真实值之间的差异,还考虑了预测的不确定性(方差)。GNLL 的形式如下:

假设真实值 $ y $ 服从均值为 $ \mu $方差为 $ \sigma^2 $ 的高斯分布,则 GNLL 的计算公式为:

总结

本文提出了一种简单高效的后期融合办法,将两个单检测器的结果通过贝叶斯公式融合在一起,具有很强的融合灵活性和不错的效果。我认为我还需要重点学习一下有关前期融合和后期融合的两篇文章,多了解一些融合的相关方法。之后在着手寻找通过各种传感器对自然灾害检测的论文找到检测自然灾害的相关模型,数据集以及方法尝试进行前期后期中期多方面的融合,目前看来前期融合和中期融合应该是比较难的,后期融可能比较简单,应该可以对比融合前后的准确度表现出相关的提升。

前言

在 ros 中经常使用cmake文件进行编译,其中进行了可执行文件添加和相关库的添加但是没有深入了解cmake的具体操作所以今天进行学习。

cmake 操作内容如下

  1. 创建 CMakeLists.txt 文件:定义项目、库、可执行文件和测试。

  2. 编写源代码和测试:编写代码和测试文件。

  3. 创建构建目录:保持源代码目录整洁。

  4. 配置项目:生成构建系统文件。

  5. 编译项目:生成目标文件。

  6. 运行可执行文件:执行程序。

  7. 运行测试:验证功能正确性。

  8. 使用自定义命令和目标:执行额外操作。

  9. 跨平台和交叉编译:支持不同平台和架构。

构建一个简单的 C++ 项目

假设我们有一个项目,包含一个主程序和一个库,库中有两个不同的功能模块。

项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

MyProject/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── lib/
│ │ ├── module1.cpp
│ │ ├── module2.cpp
│ ├── include/
│ └── mylib.h
└── tests/
├── test_main.cpp
└── CMakeLists.txt

创建 CMakeLists.txt 文件

根目录 CMakeLists.txt 文件

在 MyProject 根目录下创建一个 CMakeLists.txt 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

cmake_minimum_required(VERSION 3.10) # 指定最低 CMake 版本
project(MyProject VERSION 1.0) # 定义项目名称和版本

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 包含头文件路径
include_directories(${PROJECT_SOURCE_DIR}/src/include)

# 添加子目录
add_subdirectory(src)
add_subdirectory(tests)

src 目录 CMakeLists.txt 文件

在 src 目录下创建一个 CMakeLists.txt 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 创建库目标
add_library(MyLib STATIC
lib/module1.cpp
lib/module2.cpp
)

# 指定库的头文件
target_include_directories(MyLib PUBLIC ${CMAKE_SOURCE_DIR}/src/include)

# 创建可执行文件目标
add_executable(MyExecutable main.cpp)

# 链接库到可执行文件
target_link_libraries(MyExecutable PRIVATE MyLib)

tests 目录 CMakeLists.txt 文件

在 tests 目录下创建一个 CMakeLists.txt 文件:

1
2
3
4
5
6
7
8
9
10
11

# 查找 GTest 包
find_package(GTest REQUIRED)
include_directories(${GTEST_INCLUDE_DIRS})

# 创建测试目标
add_executable(TestMyLib test_main.cpp)

# 链接库和 GTest 到测试目标
target_link_libraries(TestMyLib PRIVATE MyLib ${GTEST_LIBRARIES})

编写源代码和测试

以下是各个文件的代码:

src/main.cpp 文件代码

1
2
3
4
5
6
7
8
9

#include <iostream>
#include "mylib.h"

int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}

src/lib/module1.cpp 文件代码

1
2
3
4
5

#include "mylib.h"

// Implementation of module1

src/lib/module2.cpp 文件代码

1
2
3
4
#include "mylib.h"

// Implementation of module2

src/include/mylib.h 文件代码

1
2
3
4
5
6
7
8

#ifndef MYLIB_H
#define MYLIB_H

// Declarations of module functions

#endif // MYLIB_H

tests/test_main.cpp 文件代码

1
2
3
4
5
6
7
8

#include <gtest/gtest.h>

// Test cases for MyLib
TEST(MyLibTest, BasicTest) {
EXPECT_EQ(1, 1);
}

创建构建目录

在项目根目录下创建一个构建目录:

1
2
mkdir build
cd build

配置项目

在构建目录中运行 CMake 以配置项目:

1
cmake ..

编译项目

1
make

运行可执行文件

编译完成后,可以运行生成的可执行文件:

1
./MyExecutable

运行测试

使用生成的测试目标进行测试:

1
./TestMyLib

使用自定义命令和目标

自定义命令

在 src/CMakeLists.txt 文件中添加自定义命令:

1
2
3
4
5
6
7

add_custom_command(
TARGET MyExecutable
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Build complete!"
)

自定义目标

在 src/CMakeLists.txt 文件中添加自定义目标:

1
2
3
4
5
6

add_custom_target(run
COMMAND ${CMAKE_BINARY_DIR}/MyExecutable
DEPENDS MyExecutable
)

运行自定义目标:

1
make run

跨平台和交叉编译

指定平台

如果需要指定平台进行构建,可以在运行 CMake 时指定平台:

1
cmake -DCMAKE_SYSTEM_NAME=Linux ..

使用工具链文件

创建一个工具链文件 toolchain.cmake:

1
2
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

使用工具链文件进行构建:

1
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake ..

实践操作

完全按照教程中的配置进行cmake编译完全没有办法生成makefile从而无法make

从简单的开始实践:

单个文件使用外部库的情况

1
2
3
4
5
6
7
8
9
cmake_minimum_required (VERSION 3.10)

project (jsoncpp)

find_package(jsoncpp REQUIRED)

add_executable(jsoncpp readFromStream.cpp)

target_link_libraries(jsoncpp PRIVATE jsoncpp_lib)

多个文件包含personal.h,personal.cpp,main.cpp

src/ # 源代码目录
├── CMakeLists.txt # src目录的CMake配置文件
├── include/ # 头文件目录
│ └── personal.h # 自定义头文件
├── personal.cpp # 自定义源文件
└── main.cpp # 主程序文件

1
2
3
4
5
6
7
cmake_minimum_required (VERSION 2.8)

project (personaldata)

include_directories(${PROJECT_SOURCE_DIR}/include)

add_executable(personaldata main.cpp personal.cpp)

关于诺干源文件一起编译

如果同一目录下有无穷多源文件,那么一个一个添加就很慢了。此时可以使用cmake中的函数存储这些源文件,aux_source_directory(dir var) 他的作用是把dir目录中的所有源文件都储存在var变量中,然后需要用到源文件的地方用 变量var来取代此时 CMakeLists.txt 可以这样优化。

1
2
3
4
5
6
7
8
9
cmake_minimum_required (VERSION 2.8)

project (personaldata)

include_directories(${PROJECT_SOURCE_DIR}/include)

aux_source_directory(. SRC_FILES) # 收集当前目录所有源文件 .为当前目录 SRC_FILE为储存的名称

add_executable(personaldata ${SRC_FILE}) # 使用收集的文件创建可执行目标

但是不推荐这样使用因为:

不包含头文件:

  • 只收集源文件(.cpp/.c),不包含头文件(.h)

  • 头文件需要单独使用 include_directories() 处理

不递归子目录

  • 只扫描指定目录本身,不会进入子目录

  • 如果需要子目录文件,需在每个子目录使用此命令或使用 add_subdirectory()

虽然方便,但官方文档不推荐使用 `aux_source_directory`: 当添加新文件时,CMake 不会自动检测,需要手动重新运行 CMake 可能包含不需要的文件(如备份文件、测试文件等) 更好的做法是显式列出文件:
1
2
3
4
5
6
set(SRC_FILES
main.cpp
personal.cpp
# 明确列出所有需要的文件
)
add_executable(MyExecutable ${SRC_FILES})
### 指定头文件文件夹 对于集中的头文件,CMake提供了一个很方便的函数`include_directories ( dir )`他的作用是 自动去dir目录下寻找头文件,相当于 gcc中的 `gcc -I dir`此时 CMakeLists.txt 可以这样优化
1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required (VERSION 2.8)

project (personaldata)

include_directories(./lamnotinclude)

set(SRC_FILES
main.cpp
personal.cpp
# 明确列出所有需要的文件
)

add_executable(personaldata ${SRC_FILES})
## 生成动态库和静态库(重点)

前言

对于利用vscode进行代码开发和调试我需要补充一些新的知识

了解 {} launch.json

launch.json是什么?

在 Visual Studio Code (VS Code) 中,launch.json 是一个用于配置调试会话的重要文件。它定义了如何启动和配置调试器,以及代码在调试过程中的行为。无论你是在本地开发环境还是远程服务器上进行调试,launch.json 都是一个关键的配置文件,能够帮助你更加高效地进行代码调试。

我没有这个文件应该如何获得?

创建launch.json文件后可以打开文件的编辑界面在右下角找到添加配置选择你想要添加的配置你就会获得一个基础的launch.json(其他两个文件同理)

其中的内容有什么含义?

其中生成的launch.json如下,我们挑几个经常用的先讲一下,其他的以后遇到再补充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) 启动",
"type": "cppdbg",
"request": "launch",
"program": "${fileDirname}/${fileBasenameNoExtension}",
"args": [],
"stopAtEntry": false,
"cwd": "${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},

],
"preLaunchTask": "C/C++: g++ 生成活动文件",
"miDebuggerPath": "/usr/bin/gdb",
}
]
}

其中每个key的功能如下:

键名 (Key) 类型 功能描述 示例值/选项 重要性
name string 配置项在调试器下拉菜单中的显示名称 "(gdb) 启动" ⭐⭐⭐⭐
type string 指定调试器类型 "cppdbg" (C++调试) ⭐⭐⭐⭐⭐
request string 调试启动类型 "launch" (启动程序) 或 "attach" (附加进程) ⭐⭐⭐⭐
program string 被调试程序路径 ${fileDirname}/${fileBasenameNoExtension} ⭐⭐⭐⭐⭐
args array 传递给程序的命令行参数 ["--param=value", "input.txt"] ⭐⭐⭐
stopAtEntry boolean 是否在 main 函数入口暂停 true (暂停)/false (不暂停) ⭐⭐⭐⭐
cwd string 程序工作目录 ${fileDirname} (当前文件目录) ⭐⭐⭐⭐
environment array 设置环境变量 [{"name": "PATH", "value": "/custom/bin"}] ⭐⭐
externalConsole boolean 是否使用外部终端 true (外部)/false (VSC内置终端) ⭐⭐⭐
MIMode string 指定调试引擎 "gdb" (GNU调试器) ⭐⭐⭐⭐⭐
setupCommands array gdb 初始化命令 -enable-pretty-printing (结构化输出) ⭐⭐⭐
preLaunchTask string 调试前执行的任务 "C/C++: g++ 生成活动文件" (编译任务) ⭐⭐⭐⭐
miDebuggerPath string gdb 调试器路径 "/usr/bin/gdb" ⭐⭐⭐⭐⭐

关键路径变量说明:

  • ${fileDirname}:当前打开文件所在目录
  • ${fileBasenameNoExtension}:当前文件名(不含扩展名)
  • ${workspaceFolder}:VSCode 打开的工作区根目录

使用技巧:

  1. 单文件调试
    program 使用 ${fileDirname}/${fileBasenameNoExtension} 可直接调试当前打开的文件

  2. ROS节点调试

    1
    2
    "program": "${workspaceFolder}/devel/lib/pkg_name/node_name",
    "args": ["_param:=value"]
  3. 添加条件断点
    1
    2
    "text": "break main if argc > 1",
    "ignoreFailures": false
  4. 添加ros环境
    1
    2
    3
    4
        "environment": [{
    "name": "ROS_MASTER_URI",
    "value": "http://localhost:11311"
    }]

    tasks.json是什么?

使用不同的编程语言可能有不同的开发流程,比如 C/C++ 就需要编译(广义编译,包括了链接)、运行、测试、打包等等流程,而 Python 只需要运行即可,为了把各种语言的不同开发流程抽象成同一套流程,于是有了编码(Code)— 构建(build)— 运行/调试(run/debug)— 测试 (test) — 打包(package) 等等,其中每个环节都可以认为是一个 task,所以可以利用 tasks.json来手动完成那些使用 IDE 时被隐藏的开发流程细节

其中的内容有什么含义?

其中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++ 生成活动文件",
"command": "/usr/bin/g++",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}"
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "调试器生成的任务。"
}
],
"version": "2.0.0"
}

键名 (Key) 类型 功能描述 示例值/选项 重要性
type string 任务类型 "cppbuild" (C++编译任务) ⭐⭐⭐⭐
label string 任务标识符 "C/C++: g++ 生成活动文件" ⭐⭐⭐⭐⭐
command string 编译器路径 "/usr/bin/g++" ⭐⭐⭐⭐⭐
args array 编译参数 ["-fdiagnostics-color=always", "-g", ...] ⭐⭐⭐⭐
options.cwd string 编译时的工作目录 "${fileDirname}" (当前文件目录) ⭐⭐⭐
problemMatcher array 错误匹配器 ["$gcc"] (GCC错误格式) ⭐⭐⭐
group.kind string 任务分组类型 "build" (构建任务) ⭐⭐⭐
group.isDefault boolean 是否为默认构建任务 true (是默认任务) ⭐⭐⭐⭐
detail string 任务描述信息 "调试器生成的任务。"

编译参数 (args) 详解:

参数 功能说明 重要性
-fdiagnostics-color=always 启用彩色错误输出 ⭐⭐⭐
-g 生成调试信息 ⭐⭐⭐⭐⭐
${file} 当前活动文件 ⭐⭐⭐⭐⭐
-o 指定输出文件名 ⭐⭐⭐⭐
${fileDirname}/${fileBasenameNoExtension} 输出文件路径 ⭐⭐⭐⭐⭐

常用扩展参数:

1
2
3
4
5
6
7
8
"args": [
"-Wall", // 开启所有警告
"-Wextra", // 启用额外警告
"-O0", // 禁用优化(调试时推荐)
"-I${workspaceFolder}/include", // 添加头文件路径
"-L${workspaceFolder}/lib", // 添加库文件路径
"-lmy_library" // 链接指定库
]

c_cpp_properties.json的功能

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c17",
"cppStandard": "gnu++14",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
键名 (Key) 类型 功能描述 示例值/选项 重要性
name string 配置名称 "Linux" (平台标识) ⭐⭐⭐
includePath array 头文件搜索路径 ["${workspaceFolder}/**"] ⭐⭐⭐⭐⭐
defines array 预处理器宏定义 ["DEBUG", "VERSION=1.0"] ⭐⭐⭐
compilerPath string 编译器路径 "/usr/bin/gcc" ⭐⭐⭐⭐⭐
cStandard string C语言标准版本 "c17" (C17标准) ⭐⭐⭐
cppStandard string C++语言标准版本 "gnu++14" (GNU C++14标准) ⭐⭐⭐⭐
intelliSenseMode string IntelliSense 引擎模式 "linux-gcc-x64" (Linux+GCC+x64) ⭐⭐⭐⭐
version number 配置文件版本 4

关键配置说明:

  1. includePath

    • ${workspaceFolder}/**:包含工作区所有子目录
    • 添加ROS路径:"/opt/ros/noetic/include/**"
  2. cppStandard 选项:

    • c++11, c++14, c++17, c++20
    • GNU扩展版本:gnu++14
  3. intelliSenseMode 常用值:

    • linux-gcc-x64
    • windows-msvc-x64
    • macos-clang-x64

开始基础调试

执行编译

  • group中的isDefault: 值为true表示支持通过快捷键ctrl+shift+B来执行该编译任务。如果值改为false,也可以从菜单中选择运行:Terminal(终端)>Run Build Task(进行生成任务)。

生成结果如下,表示g++编译成功:
build

按+号创建一个新的在当前目录下的终端

newshell

同时可以输入ls查看发现多了一个相关的可执行文件,输入./filename你就可以运行这个文件了

runfile

执行编译遇到的问题

在编译包含外部库jsoncpp的cpp文件时出现了使用代码g++ readFromString.cpp -ljsoncpp -std=c++11 -o readFromString可以正常编译使用ctrl+shift+b却编译失败的问题

详细介绍一下g++ readFromString.cpp -ljsoncpp -std=c++11 -o readFromString:

命令部分 功能说明 详细解释 是否必需
g++ 调用编译器 GNU C++ 编译器的主程序 ✅ 必须
readFromString.cpp 指定源文件 要编译的 C++ 源代码文件 ✅ 必须
-ljsoncpp 链接 JSON 库 1. -l 表示链接库
2. jsoncpp 是 JSON 库的简称
3. 实际链接 libjsoncpp.so 文件
✅ 必须
-std=c++11 指定 C++ 标准版本 使用 C++11 语言特性进行编译 ⚠️ 推荐
-o 指定输出文件名 设置生成的可执行文件名称 ⚠️ 可选
readFromString 输出文件名称 编译后生成的可执行文件名(省略扩展名) ⚠️ 可选

感觉vscode编译功能失败的原因应该在tasks.json的配置上尝试修改配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++ 生成活动文件",
"command": "/usr/bin/g++",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}",
"-ljsoncpp", // 必须包含库链接
"-std=c++11" // 必须包含标准版本
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "调试器生成的任务。"
}
],
"version": "2.0.0"
}

问题成功解决!!!

执行调试

设置一下stopAtEntry

stopAtEntry: 默认情况下,C++拓展不会向源代码添加任何断点,stopAtEntry 值设置为 false。 将stopAtEntry值更改为 true 将使调试器在开始调试时停止在 main 方法上。

开始一个调试会话

在调试的程序页面按f5或者Run(运行) > Start Debugging(启动调试),用户界面的几个变化: 集成终端出现在源代码编辑器的底部,在“Debug Output”选项卡中,会看到指示调试器已启动并正在运行的输出。编辑器突出显示 main 方法中的第一条语句。这是 C++拓展自动设置的断点。

debug

使用箭头指出的工具进行调试:

  • 第一个是从断点开始继续进行程序

  • 第二个是逐步进行程序

  • 第三个是更细致的逐步进行

  • 第四个是重新启动

  • 第五个是停止

逐行执行代码时在debug下可以看到变量的值,同时可以在监管里点击+添加你要监控的变量,将鼠标直接放在变量上也会显示他的值:

debugwatch

前言

为了从python转到cpp编程必须学习一下cpp的包管理和安装,其并没有python的那麽方便。

命令方式

1
2
sudo apt install libxxx-dev
sudo apt install xxx

安装位置会在/usr/lib/usr/lib

1
sudo apt install ros-yourrosversion-xxxx

源码方式

源码获取方式:github,git

1
2
3
4
5
6
7
8
9
10
11
12
#cmake构建安装 源码最外层目录中有cmakelsit.txt
1.git clone yourcodewebsite
2.code 打开源码
3.源代码管理->tag选择版本
4.build mkdir
5.cd build
6.cmake ..
7.make 或者 sudo make
8.sudo make install
·············································
#一定需要在build文件夹中
如果需要卸载就使用: sudo make uninstall
1
2
3
4
5
6
7
8
9
10
11
#configure构建安装
1.git clone yourcodewebsite
2.进入目录找到 configure文件
3.mkdir build
4.cd build
5.../configure --prefix=/usr/local #生成makefile,并指定库的安装位置
6.make 或 sudo make
7.sudo make install
·············································
#一定需要在build文件夹中
如果需要卸载就使用: sudo make uninstall

pip源码安装用的很少

直接拷贝

.so .a 文件直接拷贝到/usr/local/lib下
对应的头文件放在/usr/local/include,方便直接引用

deb安装

最有效的安装方式

阅读第三方库的readme.md查看是否支持

前言

什么是 JSON ?

  1. JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation)
  2. JSON 是轻量级的文本数据交换格式
  3. JSON 独立于语言:JSON 使用 Javascript语法来描述数据对象,但是 JSON 仍然独立于语言和平台。JSON 解析器和 JSON 库支持许多不同的编程语言。 目前非常多的动态(PHP,JSP,.NET)编程语言都支持 JSON
  4. JSON 具有自我描述性,更易理解

这里有一些json的工具以后可能会用到:

JSON 格式化工具
JSON 转义/去除转义
JSON 在线解析工具
JSON 差异对比工具

来开始看看json的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name":"wangsong",
"age":"23",
"hobbies":["Swimmming","Running"],
"address":{
"city":"Huzhou",
"Inc":"Huzhou institute of Zhejiang University",
"coordinates":{
"latitude":40.7128,
"longitude":-74.0060
}
}
}
  • 在json中使用{}来定义一个对象,括号中的一切都代表存储数据的键值对

  • 其中name是键(key),wangsong是值(value)

  • 数组采用[]进行定义其中的元素用,分隔

  • 每个键值对之间用逗号隔开直到最后一个

  • 可以在json中进行对象的嵌套

让我们看看如何访问json文件

python

使用json模块将json数据转换为字典进行使用

1
2
3
4
import json
personal_data = json.loads(personal_data.json)
# accessing data
print(personal_data['name']) # "wangsong"

cpp

在cpp中想要使用json文件就需要配置jsoncppp

  1. 拉取jsoncpp的源码

  2. 打开你下载的jsoncpp文件夹

  3. 双击运行amalgamate.py文件(需要有python环境)

  4. 操作后,会生成一个dist的文件夹里面就是我们需要的jsoncpp的源代码了,只有三个文件可以直接包含到项目中一起编译这个源文件还是跨平台的,在windows下和linux下都可以使用

或者按照我接下来的实践进行操作

  1. 拉取jsoncpp源码,查看源码中找到cmake安装文件进行安装

  2. 编写流式读取代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <json/json.h>
#include <fstream>
#include <iostream>

using namespace std;

int main() {
// 1. 检查文件是否成功打开
ifstream ifs("myjson/wangsong.json");
if (!ifs.is_open()) {
cerr << "错误:无法打开文件" << endl;
return 1;
}

Json::Reader reader;
Json::Value root; // 使用更清晰的变量名

// 2. 检查解析是否成功
if (!reader.parse(ifs, root)) {
cerr << "JSON解析错误:" << reader.getFormattedErrorMessages() << endl;
return 1;
}

// 3. 正确访问各字段
// 访问顶层字段
cout << "姓名: " << root["name"].asString() << endl;

// 访问空键名字段(JSON中键名为""的字段)
if (root.isMember("age")) {
cout << "年龄: " << root["age"].asString() << endl;
} else {
cout << "警告:未找到年龄字段" << endl;
}

// 访问数组
cout << "爱好: ";
const Json::Value& hobbies = root["hobbies"];
for (const auto& hobby : hobbies) {
cout << hobby.asString() << " ";
}
cout << endl;

// 4. 访问嵌套对象(需要逐级访问)
const Json::Value& address = root["address"];
cout << "城市: " << address["city"].asString() << endl;
cout << "机构: " << address["Inc"].asString() << endl;

const Json::Value& coords = address["coordinates"];
cout << "纬度: " << coords["latitude"].asFloat() << endl;
cout << "经度: " << coords["longitude"].asFloat() << endl;

return 0;
}
  1. 输入代码进行编译g++ filename.cpp -ljsoncpp -std=c++11 -o progname,也可以使用ctrl + shift +b进行编译但是要注意修改taske.josn中的配置。

fixtasks

成功编译后输入./progname进行运行

mydatajson

第七章:训练一个最先进的模型

本章介绍了更高级的技术,用于训练图像分类模型并获得最先进的结果。如果您想了解更多关于深度学习的其他应用,并稍后回来,您可以跳过它——后续章节不会假设您已掌握这些材料。

在此先做跳过,我对图像处理并非十分感兴趣。