Json及Jsoncpp开源库的应用

前言

参考:Json(视频)C++开源库Jsoncpp(视频)JsonJsoncpp的编译和使用

作者:爱编程的大丙

跟随苏老师学习JSON数据格式并简单应用在tensorRT_Pro项目中,若有问题欢迎各位批评指正!😄

1.JSON

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这种数据格式为载体的配置文件。

在这里插入图片描述

1.1 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"
            }
        }
    ]
    

1.2 Json对象

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"
}

1.3 注意事项

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对象并列写入到同一个文件里边,切记!!!

2.Jsoncpp

既然知道有JSON这种数据格式,那么我们应该怎么使用呢?当然你可以选择自己写个简单的JSON解析器,这里博主使用Jsoncpp这个跨平台的C++开源库来处理Json数据,其github地址是https://github.com/open-source-parsers/jsoncpp

当然还有一些其它的开源JSON开源库,比如json for modern c++,具体可参考json for modern c++的使用

参考自苏老师的jsoncpp的编译和使用,更多细节请看视频

2.1 下载

本文只关注在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

也可手动点击下载,首先点击masterTags切换到1.8.3版本,然后点击右上角的绿色的Code按键,将代码下载下来。也可以点击here[password:json]下载博主准备好的代码。

在这里插入图片描述

在这里插入图片描述

在Github上将代码下载好以后,在命令行下进入该项目所在的地址,直接执行python amalgamate.py命令,会在dist目录下生成两个头文件分别是json.h、json-forwards.h和一个源文件jsoncpp.cpp后续JSON的使用仅仅需要json.h和jsoncpp.cpp两个文件即可,可以将其与工程中其它代码一起编译。图解如下所示

在这里插入图片描述

2.2 Jsoncpp的使用

json库中的类被定义到了一个Json命名空间中,建议在使用这个库的时候先声明这个命名空间:

using namespace Json;

使用jsoncpp库解析json格式的数据,我们只需要掌握三个类:

1.Value类:将json支持的数据类型进行了包装,最终得到一个Value类型

2.FastWriter类:将Value对象中的数据序列化为字符串

3.Reader类:反序列化,将json字符串解析成Value类型

2.2.1 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;
2.2.2 FastWriter类
// 将数据序列化 -> 单行
// 进行数据的网络传输
std::string Json::FastWriter::write(const Value& root);
2.2.3 Reader类
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);

2.3 示例代码

假设现在我们要将一个Json对象写入到一个JSON文件中,并读取出来。

首先将之前生成的json.h和jsoncpp.cpp文件复制到当前项目文件下,整个项目文件的目录结构如下,现在做个简单介绍

  • build文件夹:存放编译过程中的中间文件
  • CMakeLists.txt文件:用于编译cpp文件
  • src文件夹:存放源码文件
    • json文件夹:存放json.h和jsoncpp.cpp文件,用于json解析
    • main文件:主函数用于读写json文件
  • workspace文件夹:存放编译生成的可执行文件以及程序生成的其它中间文件
├── 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 <json/json.h>
#include <iostream>
#include <fstream>

using namespace Json;
using namespace std;

int main()
{
    writeJson();
    readJson();
    return 0;
}
2.3.1 写json文件
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();
}
2.3.2 读json文件
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; 
    }    
}
2.3.3 测试

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 <json/json.h>
#include <iostream>
#include <fstream>

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文件这种设计思路是坚决不推荐的

3.JSON的应用

博主学习JSON的主要原因在于上篇文章Jetson nano部署YOLOv7中有一些参数需要调节,如NMS的阈值,置信度阈值等等,每次需要在程序中修改然后再编译运行,过程繁琐,故使用JSON这种数据格式作为配置文件,程序读取对应JSON配置文件来获取一些参数比如NMS的阈值,置信度阈值等等,方便项目的开发利用。

本次是在上篇文章的一个小扩展,即简单添加JSON配置文件,值得一提的是tensorRT_Pro项目中已经包含的jsoncpp未编译的源码,这也是博主关注并学习jsoncpp这个开源库的原因之一。

3.1 yolov7模型部署

这里以上篇博客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模型

3.1.1 编译

编译生成可执行文件pro,保存在workspace/文件夹下,指令如下:

$ cd tensorRT_Pro-main
$ mkdir build && cd build
$ cmake .. && make -j8

编译图解如下所示

在这里插入图片描述

3.1.2 模型构建和推理

编译完成后的可执行文件pro存放在workspace文件夹下,故进入workspace文件夹下执行pro会生成yolov7-tiny.FP32.trtmodel引擎文件用于模型推理,会生成yolov7-tiny_Yolov7_FP32_result文件夹,该文件夹下保存了推理的图片

模型构建图解如下图所示

在这里插入图片描述

模型推理效果如下图所示

在这里插入图片描述

在这里插入图片描述

3.2 tensorRT_Pro的jsoncpp应用

现在来构建一个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,修改较简单主要有以下几点:

  • 1.app_yolo.cpp添加json头文件和对应命名空间
  • 2.app_yolo.cpp 40行 inference_and_performance()函数中,添加json文件解析代码

具体修改如下

#include <common/json.hpp>			// 修改1 json头文件
#include <fstream>
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及其开源库的更多应用需要各位自己去挖掘啦。感谢各位看到最后,创作不易,读后有收获的看官请帮忙点个👍。

下载链接

参考

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱听歌的周童鞋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值