参考:Json(视频)、C++开源库Jsoncpp(视频)、Json、Jsoncpp的编译和使用
作者:爱编程的大丙
跟随苏老师学习JSON数据格式并简单应用在tensorRT_Pro项目中,若有问题欢迎各位批评指正!
Copy自苏老师的Json教程,建议看原视频的详细讲解
JSON(JaveScrip Object Notation)是一种轻量级的数据交换格式
Json是一种数据格式,和语言无关,在什么语言中都可以使用Json。基于这种通用的数据格式,一般处理两方面的任务:
1.组织数据(数据序列化),用于数据的网络传输
2.组织数据(数据序列化),写磁盘文件实现数据的持久化存储(一般以.json
作为文件后缀)
Json中主要有两种数据格式:Json数组和Json对象,并且这两种格式可以交叉嵌套使用。
如果大家熟悉vscode,那么肯定接触过JSON这种数据格式,当你在vscode中配置C++的调试功能时,肯定用到c_cpp_properties.json、task.json、launch.json这三个文件,这三个文件均是以JSON这种数据格式为载体的配置文件。
Json数组使用[]
表示,[]
里边是元素,元素和元素之间使用逗号间隔,最后一个元素后边没有逗号,一个Json数组中支持同时存在多种不同类型的成员,包括:整型
、浮点型
、字符串
、布尔类型
、json数组
、json对象
、空值-null
。由此可见Json数组比起C/C++数组要灵活很多
Json数组中的元素数据类型一致
// 整型
[1,2,3,4,5]
// 字符串
["luffy", "sanji", "zoro", "nami", "robin"]
Json数组中的元素数据类型不一致
[12, 13.14, true, false, "hello world", null]
Json数组中的数组嵌套使用
[
["cat", "dog", "panda", "beer", "rabbit"],
["北京", "上海", "天津", "重庆"],
["luffy", "boy", 19]
]
Json数组和对象嵌套使用
[
{
"luffy":{
"age":19,
"father":"Monkey·D·Dragon",
"grandpa":"Monkey D Garp",
"brother1":"Portgas D Ace",
"brother2":"Sabo"
}
}
]
Json对象可以使用{}
来描述,每个Json对象中可以存储若干个元素,每一个元素对应一个键值对(key:value结构),元素和元素之间使用逗号间隔,最后一个元素后边没有逗号。对于每个元素中的键值对有以下细节需要注意:
1.键值(key)必须是字符串,位于同一层级的键值不要重复(因为Json数据解析是通过键值取出对应的value值)
2.value值得类型是可选的,可根据实际需求指定,可用类型包括:整型
、浮点型
、字符串
、布尔类型
、json数组
、json对象
、空值-null
。
使用Json对象描述一个人得信息:
{
"Name":"Ace",
"Sex":"man",
"Age":20,
"Family":{
"Father":"Gol·D·Roger",
"Mother":"Portgas·Rouge",
"Brother":["Sabo", "Monkey D. Luffy"]
},
"IsAlive":false,
"Comment":"yyds"
}
Json的结构虽然简单,但是进行嵌套之后就可以描述很复杂的事情,在项目开发过程中往往需要我们根据实际需求自己定义Json格式用来存储项目数据。
另外,如果需要将Json数据持久化到磁盘文件中,需要注意一个问题:在一个Json文件中只能有一个Json数组或者Json对象的根节点,不允许同时存储多个并列的根节点。
错误的写法
// test.json
{
"name":"luffy",
"age":19
}
{
"user":"ace",
"passwd":"123456"
}
错误原因分析:在一个Json文件中有两个并列的Json根节点(并列包含Json对象和Json对象、Json对象和Json数组、Json数组和Json数组),根节点只能有一个。
正确的写法
// test.json
{
"Name":"Ace",
"Sex":"man",
"Age":20,
"Family":{
"Father":"Gol·D·Roger",
"Mother":"Portgas·Rouge",
"Brother":["Sabo", "Monkey D. Luffy"]
},
"IsAlive":false,
"Comment":"yyds"
}
在上面的例子中通过Json对象以及Json数组的嵌套描述了一个人的身份信息,并且根节点只有一个就是Json对象,如果还需要使用Json数组或者Json对象描述其它信息,需要将这些信息写入到其它文件中,不要和这个Json对象并列写入到同一个文件里边,切记!!!
既然知道有JSON这种数据格式,那么我们应该怎么使用呢?当然你可以选择自己写个简单的JSON解析器,这里博主使用Jsoncpp这个跨平台的C++开源库来处理Json数据,其github地址是https://github.com/open-source-parsers/jsoncpp
当然还有一些其它的开源JSON开源库,比如json for modern c++,具体可参考json for modern c++的使用
参考自苏老师的jsoncpp的编译和使用,更多细节请看视频
本文只关注在Linux下jsoncpp的编译使用,在Windows的使用可参考苏老师的视频。Linux下jsoncpp的使用在网上大多都是采取将源码编译成静态库/动态库文件,然后链接使用,博主在这里使用其源文件引入到项目中自行编译,参考自Linux中jsoncpp的编译使用。jsoncpp的源码地址是https://github.com/open-source-parsers/jsoncpp
本次使用其历史版本1.8.3进行测试。Linux下代码克隆指令如下
git clone -b 1.8.3 https://github.com/open-source-parsers/jsoncpp.git
也可手动点击下载,首先点击master
的Tags
切换到1.8.3版本,然后点击右上角的绿色的Code
按键,将代码下载下来。也可以点击here[password:json]下载博主准备好的代码。
在Github上将代码下载好以后,在命令行下进入该项目所在的地址,直接执行python amalgamate.py命令,会在dist目录下生成两个头文件分别是json.h、json-forwards.h和一个源文件jsoncpp.cpp。后续JSON的使用仅仅需要json.h和jsoncpp.cpp两个文件即可,可以将其与工程中其它代码一起编译。图解如下所示
json
库中的类被定义到了一个Json
命名空间中,建议在使用这个库的时候先声明这个命名空间:
using namespace Json;
使用jsoncpp
库解析json
格式的数据,我们只需要掌握三个类:
1.Value类
:将json支持的数据类型进行了包装,最终得到一个Value类型
2.FastWriter类
:将Value对象中的数据序列化为字符串
3.Reader类
:反序列化,将json字符串解析成Value类型
这个类可以看做是一个包装器,它可以封装Json支持的所有类型,这样我们在处理数据的时候就方便多了。
枚举类型 | 说明 |
---|---|
nullValue | 不表示任何数据,空值 |
intValue | 表示有符号整数 |
uintValue | 表示无符号整数 |
realValue | 表示浮点数 |
stringValue | 表示utf8格式的字符串 |
booleanValue | 表示布尔数 |
arrayValue | 表示数组,即JSON串中的[] |
objectValue | 表示键值对,即JSON串中的{} |
构造函数
Value类为我们提供了很多构造函数,通过构造函数来封装数据,最终得到一个统一的类型。
// 因为Json::Value已经实现了各种数据类型的构造函数
Value(ValueType type = nullValue);
Value(Int value);
Value(UInt value);
Value(Int64 value);
Value(UInt64 value);
Value(double value);
Value(const char* value);
Value(const char* begin, const char* end);
Value(bool value);
Value(const Value& other);
Value(Value&& other);
检测保存的数据类型
// 检测保存的数据类型
bool isNull() const;
bool isBool() const;
bool isInt() const;
bool isInt64() const;
bool isUInt() const;
bool isUInt64() const;
bool isIntegral() const;
bool isDouble() const;
bool isNumeric() const;
bool isString() const;
bool isArray() const;
bool isObject() const;
将Value对象转换为实际类型
Int asInt() const;
UInt asUInt() const;
Int64 asInt64() const;
UInt64 asUInt64() const;
LargestInt asLargestInt() const;
LargestUInt asLargestUInt() const;
JSONCPP_STRING asString() const;
float asFloat() const;
double asDouble() const;
bool asBool() const;
const char* asCString() const;
对json数组的操作
ArrayIndex size() const;
Value& operator[](ArrayIndex index);
Value& operator[](int index);
const Value& operator[](ArrayIndex index) const;
const Value& operator[](int index) const;
// 根据下标的index返回这个位置的value值
// 如果没找到这个index对应的value, 返回第二个参数defaultValue
Value get(ArrayIndex index, const Value& defaultValue) const;
Value& append(const Value& value);
const_iterator begin() const;
const_iterator end() const;
iterator begin();
iterator end();
对json对象的操作
Value& operator[](const char* key);
const Value& operator[](const char* key) const;
Value& operator[](const JSONCPP_STRING& key);
const Value& operator[](const JSONCPP_STRING& key) const;
Value& operator[](const StaticString& key);
// 通过key, 得到value值
Value get(const char* key, const Value& defaultValue) const;
Value get(const JSONCPP_STRING& key, const Value& defaultValue) const;
Value get(const CppTL::ConstString& key, const Value& defaultValue) const;
// 得到对象中所有的键值
typedef std::vector<std::string> Members;
Members getMemberNames() const;
将Value对象数据序列化为string
// 序列化得到的字符串有样式 -> 带换行 -> 方便阅读
// 写配置文件的时候
std::string toStyledString() const;
// 将数据序列化 -> 单行
// 进行数据的网络传输
std::string Json::FastWriter::write(const Value& root);
bool Json::Reader::parse(const std::string& document,
Value& root, bool collectComments = true);
参数:
- document: json格式字符串
- root: 传出参数, 存储了json字符串中解析出的数据
- collectComments: 是否保存json字符串中的注释信息
// 通过begindoc和enddoc指针定位一个json字符串
// 这个字符串可以是完成的json字符串, 也可以是部分json字符串
bool Json::Reader::parse(const char* beginDoc, const char* endDoc,
Value& root, bool collectComments = true);
// write的文件流 -> ofstream
// read的文件流 -> ifstream
// 假设要解析的json数据在磁盘文件中
// is流对象指向一个磁盘文件, 读操作
bool Json::Reader::parse(std::istream& is, Value& root, bool collectComments = true);
假设现在我们要将一个Json对象写入到一个JSON文件中,并读取出来。
首先将之前生成的json.h和jsoncpp.cpp文件复制到当前项目文件下,整个项目文件的目录结构如下,现在做个简单介绍
├── build
├── CMakeLists.txt
├── src
│ ├── json
│ │ ├── jsoncpp.cpp
│ │ └── json.h
│ └── main.cpp
└── workspace
需要读写的JSON对象如下
{
"name":"Luffy",
"age":19,
"height":1.74,
"brother":["sabo", "ace"],
"crew":{
"zoro":"mate"
"sanzi":"cook"
"nami":"sailing"
}
}
#include
#include
#include
using namespace Json;
using namespace std;
int main()
{
writeJson();
readJson();
return 0;
}
void writeJson()
{
// 最外层的数组看做一个Value
// 创建最外层的Value对象
Value root;
root["name"] = "Luffy";
root["age"] = 19;
root["height"] = 1.74;
// 创建并初始化一个子数组
Value subArray;
subArray.append("sabo");
subArray.append("ace");
root["brother"] = subArray;
// 创建并初始化一个子对象
Value subObj;
subObj["zoro"] = "mate";
subObj["sanzi"] = "cook";
subObj["nami"] = "sailing";
root["crew"] = subObj;
// 序列化
#if 1
// 有格式的字符串
std::string str = root.toStyledString();
#else
FastWtiter f;
string str = f.write(root);
#endif
// 将序列化的字符串写磁盘文件
std::ofstream ofs("test.json");
ofs << str;
ofs.close();
}
void readJson()
{
// 1.将磁盘文件中的json字符串读到磁盘文件
std::ifstream ifs("test.json");
// 2.反序列化 -> value对象
Value root;
Reader r;
r.parse(ifs, root);
// 3.从value对象中将数据依次读出
Value::Members keys = root.getMemberNames();
for (int k = 0; k < keys.size(); ++k)
{
cout << keys.at(k) << ":" << root[keys[k]] << endl;
}
}
CMakeList.txt内容如下:
cmake_minimum_required(VERSION 3.9)
project(pro)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_BUILD_TYPE Debug)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/workspace)
include_directories(
${PROJECT_SOURCE_DIR}/src
${PROJECT_SOURCE_DIR}/src/json
)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -O0 -Wfatal-errors -pthread -w -g")
file(GLOB_RECURSE cpp_srcs ${PROJECT_SOURCE_DIR}/src/*.cpp)
add_executable(pro ${cpp_srcs})
target_link_libraries(pro pthread)
main.cpp内容如下:
#include
#include
#include
using namespace Json;
using namespace std;
void writeJson()
{
// 最外层的数组看做一个Value
// 创建最外层的Value对象
Value root;
root["name"] = "Luffy";
root["age"] = 19;
root["height"] = 1.74;
// 创建并初始化一个子数组
Value subArray;
subArray.append("sabo");
subArray.append("ace");
root["brother"] = subArray;
// 创建并初始化一个子对象
Value subObj;
subObj["zoro"] = "mate";
subObj["sanzi"] = "cook";
subObj["nami"] = "sailing";
root["crew"] = subObj;
// 序列化
#if 1
// 有格式的字符串
std::string str = root.toStyledString();
#else
FastWtiter f;
string str = f.write(root);
#endif
// 将序列化的字符串写磁盘文件
std::ofstream ofs("test.json");
ofs << str;
ofs.close();
}
void readJson()
{
// 1.将磁盘文件中的json字符串读到磁盘文件
std::ifstream ifs("test.json");
// 2.反序列化 -> value对象
Value root;
Reader r;
r.parse(ifs, root);
// 3.从value对象中将数据依次读出
Value::Members keys = root.getMemberNames();
for (int k = 0; k < keys.size(); ++k)
{
cout << keys.at(k) << ":" << root[keys[k]] << endl;
}
}
int main()
{
writeJson();
readJson();
return 0;
}
json.h和jsoncpp.cpp可以参考之前的生成方式,也可以使用博主提供的生成好的文件,给出下载链接Baidu Drive[password:json]【注意jsoncpp版本为1.8.3】
编译和运行图解如下所示
从上可以看到执行程序后workspace文件下会生成test.json文件,并在终端将其内容打印出来,生成的json内容如下所示,可以看出与我们预期的一致。
虽然Json这种格式无外乎数组和对象两种,但是需求不同我们设计的Json文件的组织方式也不同,一般都是特定的文件对应特定的解析函数,一个解析函数可以解析任何的Json文件这种设计思路是坚决不推荐的。
博主学习JSON的主要原因在于上篇文章Jetson nano部署YOLOv7中有一些参数需要调节,如NMS的阈值,置信度阈值等等,每次需要在程序中修改然后再编译运行,过程繁琐,故使用JSON这种数据格式作为配置文件,程序读取对应JSON配置文件来获取一些参数比如NMS的阈值,置信度阈值等等,方便项目的开发利用。
本次是在上篇文章的一个小扩展,即简单添加JSON配置文件,值得一提的是tensorRT_Pro项目中已经包含的jsoncpp未编译的源码,这也是博主关注并学习jsoncpp这个开源库的原因之一。
这里以上篇博客Jetson nano部署YOLOv7为基础,默认大家已经阅读过,细节就不再强调。本次以官方yolov7-tiny.pt模型为主,简单应用jsoncpp解析JSON文件。博主给出权重及其ONNX文件的下载链接Baidu Drive[password:yolo],关于ONNX导出的细节请参考上篇博客,这里不再赘述。
yolo模型的推理代码主要在src/application/app_yolo.cpp文件中,需要推理的图片放在workspace/inference文件夹中,将上述修改后导出的ONNX文件放在workspace文件夹下。源码修改较简单,只需修改一处:将app_yolo.cpp 177行"yolov7"改成"yolov7-tiny",构建yolov7-tiny.pt模型
编译生成可执行文件pro,保存在workspace/文件夹下,指令如下:
$ cd tensorRT_Pro-main
$ mkdir build && cd build
$ cmake .. && make -j8
编译图解如下所示
编译完成后的可执行文件pro存放在workspace文件夹下,故进入workspace文件夹下执行pro会生成yolov7-tiny.FP32.trtmodel引擎文件用于模型推理,会生成yolov7-tiny_Yolov7_FP32_result文件夹,该文件夹下保存了推理的图片
模型构建图解如下图所示
模型推理效果如下图所示
现在来构建一个JSON数据格式的配置文件,用来修改yolov7模型推理时的置信度阈值以及NMS阈值,方便后续调试。tensorRT_Pro中jsoncpp的源码位于tensorRT_Pro/src/tensorRT/common文件夹下,该文件夹下存放着json.hpp、json.cpp两个文件用于解析JSON文件,其jsoncpp版本为1.8.3。
首先在wokspace/文件夹下创建一个JSON文件,命名为config.json,其内容如下:
{
"conf_thresh" : 0.25,
"nms_thresh" : 0.45
}
然后修改源码,位于src/application/app_yolo.cpp,修改较简单主要有以下几点:
具体修改如下
#include // 修改1 json头文件
#include
using namespace Json;
static void inference_and_performance(int deviceid, const string& engine_file, TRT::Mode mode, Yolo::Type type, const string& model_name){
ifstream ifs("config.json"); // 修改2 json解析
Value config;
Reader r;
r.parse(ifs, config);
float conf_thresh = config["conf_thresh"].asFloat(); // 解析置信度阈值
float nms_thresh = config["nms_thresh"].asFloat(); // 解析nms阈值
cout << "conf_thresh = " << conf_thresh << endl;
cout << "nms_thresh = " << nms_thresh << endl;
auto engine = Yolo::create_infer(
engine_file, // engine file
type, // yolo type, Yolo::Type::V5 / Yolo::Type::X
deviceid, // gpu id
conf_thresh, // confidence threshold ---置信度阈值由json配置文件解析得到---
nms_thresh, // nms threshold ---nms阈值由json配置文件解析得到---
Yolo::NMSMethod::FastGPU, // NMS method, fast GPU / CPU
1024, // max objects
false // preprocess use multi stream
);
...
}
进入build文件夹重新编译即可,编译图解如下所示。编译完成后可以修改config.json配置文件中置信度和nms阈值来观察模型推理结果的变化。
设置conf_thresh=0.85,nms_thresh=0.45,并观察模型推理效果如下图所示
从上图可以看出,配置文件修改运行后模型推理结果发生了改变,由于将置信度设置为0.85,所以模型并没有保留低置信度的预测结果,也证明了配置文件的有效性。
设置conf_thresh=0.25,nms_thresh=0.90,并观察模型推理效果如下图所示
从上图可以看出,将配置文件中的nms阈值调整后,出现多个重叠框。
博主在这里基于tensorRT_Pro项目对Json实现了最简单的应用,大家可能觉得没有太大的必要,但是在实际项目开发过程中还是需要的,有些参数在试验阶段可能需要频繁改动,比如在进行TCP/UDP网络通信时的IP、端口号,网络摄像头的IP地址等等,将这些参数写入配置文件中,在程序中进行解析可以解决反复编译的繁琐过程,有利于项目的开发。
配置文件不一定要JSON数据格式,也可以是ini或者XML等多种数据格式,只要能在程序正常解析即可。但JSON配置文件简单且直观,在tensorRT_Pro项目中也得到了应用,故博主在这里做个简单分享。
本篇博客介绍了Json这种数据格式以及对应的开源库jsoncpp的使用,并基于上篇博客Jetson nano部署YOLOv7做了一个小扩展,使用Json作为配置文件并进行解析。博主在这里只做了最基础的演示,好让大家有个基本的印象,关于Json及其开源库的更多应用需要各位自己去挖掘啦。感谢各位看到最后,创作不易,读后有收获的看官请帮忙点个。