Arcface部署应用实战

1、概述

人脸识别的一个比较常用的网络arcface,依赖于其特殊设计的loss函数,使得模型在训练的时候能够实现类间距离增大,类内的距离不断减小,最终使得所训练的backbone能够获取鉴别性很高的特征,便于人脸识别。

本文以专家博主Bubbliiiing所提供的仓库代码实现的arcface为基础,主要分享实现arcface模型动态batch推理的python和C++代码实现,至于训练和测试,可以参考仓库使用说明。仓库地址为:https://github.com/bubbliiiing/arcface-pytorch

2、cuda版本python快速推理

本节主要包含使用python实现arcface模型转化为onnx通用平台模型,然后转化为TensorRT所支持的加速engine全过程。

2.1 arcface模型转化为ONNX模型

深度学习训练的模型转化为ONNX都是通用的代码使用方法,利用torch.onnx.export函数进行相应的导出,博主这里考虑在测试时会同时计算多张图片的特征图,因此将batch设置为动态,以方便后续使用。

代码实现如下:

def export_onnx(model, img, onnx_path, opset, dynamic=False, simplify=True, batch_size=1):
    torch.onnx.export(model, img, onnx_path, verbose=True, opset_version=opset,
                      export_params=True,
                      do_constant_folding=True,
                      input_names=['images'],
                      output_names=['output'],
                      dynamic_axes={'images': {0: 'batch_size'},  # shape(1,3,112,112)
                                    'output': {0: 'batch_size'}  # shape(1,128)
                                    } if dynamic else None)
    # Checks
    model_onnx = onnx.load(onnx_path)  # load onnx model
    onnx.checker.check_model(model_onnx)  # check onnx model
    if simplify:
        try:
            model_onnx, check = onnxsim.simplify(
                model_onnx,
                dynamic_input_shape=dynamic,
                test_input_shapes={'images': list(img.shape)} if dynamic else None)
            assert check, 'assert check failed'
            onnx.save(model_onnx, onnx_path)
            print('simplify onnx success')
        except Exception as e:
            print('simplify onnx failure')

具体的使用方案如下所示:

def convert2onnx_demo():
    model_path = './model_data/arcface_mobilefacenet.pth'
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print('Loading weights into state dict...')
    net = arcface(backbone='mobilefacenet', mode="predict").eval()
    net.load_state_dict(torch.load(model_path, map_location=device), strict=True)
    net = net.to(device)
    batch_size = 4
    print('{} model loaded.'.format(model_path))
    dummy_input = torch.randn(batch_size, 3, 112, 112).to(device)
    onnx_path = './model_data/arcface_mobilefacenet.onnx'
    opset = 10
    export_onnx(net, dummy_input, onnx_path, opset, dynamic=True, simplify=True)
    ort_session = ort.InferenceSession(onnx_path)
    outputs = ort_session.run(None, {'images': np.random.randn(batch_size, 3, 112, 112).astype(np.float32)})
    print(outputs[0])

导出的onnx模型的输入输出如下所示:可以清楚的看到输入维度batch是动态变换的。
在这里插入图片描述

2.2 arcface ONNX模型转化为engine

onnx转换为engine方式有很多,可以借助trtexec.exe进行转换,也可以使用python编写相应的代码进行使用,博主采用python编写相应的脚本进行转换,具体代码转化如下:

def onnx2engine():
    import tensorrt as trt

    def export_engine(onnx_path, engine_path, half, workspace=4, verbose=False):
        print('{} starting export with TensorRT {}...'.format(onnx_path, trt.__version__))
        # assert img.device.type != 'cpu', 'export running on CPU but must be on GPU, i.e. `python export.py --device 0`'
        if not os.path.exists(onnx_path):
            print(f'failed to export ONNX file: {onnx_path}')
        logger = trt.Logger(trt.Logger.INFO)
        if verbose:
            logger.min_severity = trt.Logger.Severity.VERBOSE

        builder = trt.Builder(logger)
        config = builder.create_builder_config()
        config.max_workspace_size = workspace * 1 << 30

        flag = (1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
        network = builder.create_network(flag)
        parser = trt.OnnxParser(network, logger)
        if not parser.parse_from_file(str(onnx_path)):
            raise RuntimeError(f'failed to load ONNX file: {onnx_path}')

        ## 支持动态batch使用必须
        profile = builder.create_optimization_profile()
        profile.set_shape("images", (1, 3, 112, 112), (8, 3, 112, 112), (16, 3, 112, 112))
        config.add_optimization_profile(profile)

        inputs = [network.get_input(i) for i in range(network.num_inputs)]
        outputs = [network.get_output(i) for i in range(network.num_outputs)]
        print('Network Description:')
        for inp in inputs:
            print('input {} with shape {} and dtype is {}\n'.format(inp.name, inp.shape, inp.dtype))
        for out in outputs:
            print('output {} with shape {} and dtype is {}\n'.format(out.name, out.shape, out.dtype))

        half &= builder.platform_has_fast_fp16
        print('building FP{} engine in {}'.format(16 if half else 32, engine_path))
        if half:
            config.set_flag(trt.BuilderFlag.FP16)
        with builder.build_engine(network, config) as engine, open(engine_path, 'wb') as t:
            t.write(engine.serialize())
        print("max_batch_szie = {}".format(builder.max_batch_size))
        print("flag= {}".format(flag))
        print('export success, saved as {f} ({file_size(f):.1f} MB)')

    onnx_path = './model_data/arcface_mobilefacenet.onnx'
    engine_path = './model_data/arcface_mobilefacenet.engine'
    half = True
    verbose = True
    export_engine(onnx_path, engine_path, half, verbose=verbose)

其中需要重点注意的是,要使用动态batch完成engine的构建,必须要对profile 进行相应的设定,使得engine知道最小推理batch、最优推理batch以及最大推理batch。

2.3 arcface TensorRT快速推理

推理的代码还是相对比较简单的,简单总结如下:

  • 图像组合成相应的推理batch
  • 初始化TensorRT所需要的context
  • 给动态输入的batch和输出结果分配host以及device上的内存空间
  • 调用接口完成推理
  • 相关后处理代码

本文具体代码如下所示:

"""
An example that uses TensorRT's Python api to make inferences.
"""
import ctypes
import os
import shutil
import random
import sys
import threading
import time
import cv2
import numpy as np
import pycuda.autoinit
import pycuda.driver as cuda
import tensorrt as trt

LEN_ALL_RESULT = 128


def get_img_path_batches(batch_size, img_dir):
    ret = []
    batch = []
    for root, dirs, files in os.walk(img_dir):
        for name in files:
            suffix = os.path.splitext(name)[-1]
            if suffix in ['.jpg', '.png', '.JPG', '.jpeg']:
                if len(batch) == batch_size:
                    ret.append(batch)
                    batch = []
                batch.append(os.path.join(root, name))
    if len(batch) > 0:
        ret.append(batch)
    return ret


class ArcFaceTRT(object):
    """
     description: A arcface class that warps TensorRT ops, preprocess and postprocess ops.
    """

    def __init__(self, engine_file_path, batch_size=1):
        self.ctx = cuda.Device(0).make_context()
        stream = cuda.Stream()
        TRT_LOGGER = trt.Logger(trt.Logger.INFO)
        runtime = trt.Runtime(TRT_LOGGER)
        with open(engine_file_path, 'rb') as f:
            engine = runtime.deserialize_cuda_engine(f.read())
        context = engine.create_execution_context()
        context.set_binding_shape(0, (batch_size, 3, 112, 112))  # 这句非常重要!!!定义batch为动态维度

        #
        host_inputs = []
        cuda_inputs = []
        host_outputs = []
        cuda_outputs = []
        bindings = []

        for binding in engine:
            print('binding: ', binding, engine.get_tensor_shape(binding))
            ## 动态 batch
            dims = engine.get_tensor_shape(binding)
            if dims[0] == -1:
                dims[0] = batch_size

            # size = trt.volume(engine.get_tensor_shape(binding)) * engine.max_batch_size
            size = trt.volume(dims) * engine.max_batch_size
            dtype = trt.nptype(engine.get_tensor_dtype(binding))

            # allocate host and device buffers
            host_mem = cuda.pagelocked_empty(size, dtype)
            cuda_mem = cuda.mem_alloc(host_mem.nbytes)
            # append the device buffer to device bindings
            bindings.append(int(cuda_mem))
            # append to the appropriate list
            if engine.get_tensor_mode(binding).value == 1: ##  0 是NONE 1是INPUT 2是OUTPUT
            # if engine.binding_is_input(binding):
                self.input_w = engine.get_tensor_shape(binding)[-1]
                self.input_h = engine.get_tensor_shape(binding)[-2]
                host_inputs.append(host_mem)
                cuda_inputs.append(cuda_mem)
            else:
                host_outputs.append(host_mem)
                cuda_outputs.append(cuda_mem)

            # store
        self.stream = stream
        self.context = context
        self.engine = engine
        self.host_inputs = host_inputs
        self.cuda_inputs = cuda_inputs
        self.host_outputs = host_outputs
        self.cuda_outputs = cuda_outputs
        self.bindings = bindings
        self.batch_size = batch_size
        # self.batch_size = engine.max_batch_size

    def infer(self, raw_image_generator):
        threading.Thread.__init__(self)
        # Make self the active context, pushing it on top of the context stack.
        self.ctx.push()
        # Restore
        stream = self.stream
        context = self.context
        engine = self.engine
        host_inputs = self.host_inputs
        cuda_inputs = self.cuda_inputs
        host_outputs = self.host_outputs
        cuda_outputs = self.cuda_outputs
        bindings = self.bindings
        # Do image preprocess
        batch_image_raw = []
        batch_origin_h = []
        batch_origin_w = []
        batch_input_image = np.empty(shape=[self.batch_size, 3, self.input_h, self.input_w])

        # 组合为相应的batch进行处理
        for i, image_raw in enumerate(raw_image_generator):
            input_image, image_raw, origin_h, origin_w = self.preprocess_image(image_raw)
            batch_image_raw.append(image_raw)
            batch_origin_h.append(origin_h)
            batch_origin_w.append(origin_w)
            np.copyto(batch_input_image[i], input_image)
        batch_input_image = np.ascontiguousarray(batch_input_image)
        # Copy input image to host buffer
        np.copyto(host_inputs[0], batch_input_image.ravel())
        start = time.time()
        # Transfer input data  to the GPU.
        cuda.memcpy_htod_async(cuda_inputs[0], host_inputs[0], stream)
        # Run inference.
        context.execute_async_v2(bindings=bindings, stream_handle=stream.handle)
        # context.execute_async(batch_size=self.batch_size, bindings=bindings, stream_handle=stream.handle)
        # Transfer predictions back from the GPU.
        cuda.memcpy_dtoh_async(host_outputs[0], cuda_outputs[0], stream)
        # Synchronize the stream
        stream.synchronize()
        end = time.time()
        # Remove any context from the top of the context stack, deactivating it.
        self.ctx.pop()
        # Here we use the first row of output in that batch_size = 1
        output = host_outputs[0]
        # Do postprocess
        features = []
        for i in range(self.batch_size):
            feature = output[i * LEN_ALL_RESULT:(i + 1) * LEN_ALL_RESULT]
            features.append(feature)
            print(feature.shape)
        return batch_image_raw, end - start, features

    def destroy(self):
        # Remove any context from the top of the context stack, deactivating it.
        self.ctx.pop()

    def get_raw_image(self, image_path_batch):
        """
        description: Read an image from image path
        """
        for img_path in image_path_batch:
            yield cv2.imread(img_path)

    def get_vedio_frame(self, cap):
        for _ in range(self.batch_size):
            yield cap.read()

    def get_raw_image_zeros(self, image_path_batch=None):
        """
        description: Ready data for warmup
        """
        for _ in range(self.batch_size):
            yield np.zeros([self.input_h, self.input_w, 3], dtype=np.uint8)

    def preprocess_image(self, raw_bgr_image):
        """
        description: Convert BGR image to RGB,
                     resize and pad it to target size, normalize to [0,1],
                     transform to NCHW format.
        param:
            input_image_path: str, image path
        return:
            image:  the processed image
            image_raw: the original image
            h: original height
            w: original width
        """
        image_raw = raw_bgr_image
        h, w, c = image_raw.shape
        image = cv2.cvtColor(image_raw, cv2.COLOR_BGR2RGB)
        image = cv2.resize(image, (112, 112))
        image = image.astype(np.float32)
        image /= 255.0
        # HWC to CHW format:
        image = np.transpose(image, [2, 0, 1])
        # CHW to NCHW format
        image = np.expand_dims(image, axis=0)
        # Convert the image to row-major order, also known as "C order":
        image = np.ascontiguousarray(image)
        return image, image_raw, h, w


def img_infer(ArcFaceWraper, image_path_batch):
    batch_image_raw, use_time, res = ArcFaceWraper.infer(ArcFaceWraper.get_raw_image(image_path_batch))
    for i, feature in enumerate(res):
        print('input->{}, time->{:.2f}ms, feature shape = {}'.format(image_path_batch[i], use_time * 1000, feature.shape))


def vedio_infer(ArcFaceWraper, cap):
    batch_image_raw, use_time = ArcFaceWraper.infer(ArcFaceWraper.get_vedio_frame(cap))
    print('input->{}, time->{:.2f}ms, saving into output/'.format(1, use_time * 1000))
    cv2.namedWindow('vedio', cv2.WINDOW_NORMAL)
    cv2.imshow('vedio', batch_image_raw[0])
    cv2.waitKey(1)


def warmup(ArcFaceWraper):
    batch_image_raw, use_time, _ = ArcFaceWraper.infer(ArcFaceWraper.get_raw_image_zeros())
    print('warm_up->{}, time->{:.2f}ms'.format(batch_image_raw[0].shape, use_time * 1000))


if __name__ == '__main__':
    from tqdm import tqdm
    batch = 4
    # engine_file_path = r"D:\personal\project\code\arcface-pytorch-main\model_data\arcface_mobilefacenet.engine"
    engine_file_path = r"arcface\arcface_mobilefacenet.engine"
    arcface_wrapper = ArcFaceTRT(engine_file_path, batch_size=batch)
    try:
        print('batch size is', batch)
        image_dir = r"datasets"
        # image_path_batches = get_img_path_batches(arcface_wrapper.batch_size, image_dir)
        image_path_batches = get_img_path_batches(batch, image_dir)
        # warmup
        # for i in range(10):
        #     warmup(arcface_wrapper)

        for batch in tqdm(image_path_batches):
            img_infer(arcface_wrapper, batch)

    finally:
        # destroy the instance
        arcface_wrapper.destroy()

重点需要注意的是:
1、context需要batch动态维度,这也是进行动态batch推理的前提条件
2、针对输入的binding,需要将其dim[0]设置batch大小

2.4 测试效果

博主这边使用的是batch为4进行的推理,推理速度大概是6.98ms每个batch。
在这里插入图片描述
如果设置为batch为8,则推理时间如下:
在这里插入图片描述

也可以设置为1~16之间的任意数值进行推理,具体数值区间和你生成engine时候所设定的kmin和kmax对应的shape是相关联的,比起单张推理速度会提升一部分,但是不成相应比例。

3、cuda版本C++快速推理

本节主要包含使用C++实现arcface模型转化为onnx通用平台模型,然后转化为TensorRT所支持的加速engine全过程。

由于模型的.pth模型转换为ONNX模型和前述2.1节的内容是一致的,因此本部分从模型已经成功转换为ONNX模型开始阐述。

3.1 arcface ONNX模型转化为engine

转换为engine的方式其实和python的转化相类似,主要还都是利用C++的TensorRT相应的API进行编写的。
具体实现如下所示:

void CreateEngine::trtFromOnnx(const std::string onnx_path,const std::string engine_out_path, unsigned int max_batch_size,size_t workspace ,bool half)
{
	if (onnx_path.empty()) {
		printf("failed to export ONNX file\n");
	}
	printf("***************start to create model engine********************\n");
	IBuilder* builder = createInferBuilder(gLogger);
	IBuilderConfig* config = builder->createBuilderConfig();

	config->setMaxWorkspaceSize(static_cast<size_t>(workspace*1) << 30);
	NetworkDefinitionCreationFlags flag = (1U << int(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH));
	INetworkDefinition* network = builder->createNetworkV2(flag);
	
	IParser* parser = createParser(*network, gLogger);
	if (! parser->parseFromFile(onnx_path.c_str(), static_cast<int>(ILogger::Severity::kWARNING))) {
		//wrong information 
		for (int32_t i = 0; i < parser->getNbErrors(); i++)
		{
			std::cout << parser->getError(i)->desc() << std::endl;
		}
	}
	std::cout << "******************successfully parse the onnx model*********************" << std::endl;

	//danamic batch 
	auto profile = builder->createOptimizationProfile();
	auto input_tensor = network->getInput(0);
	auto input_dims = input_tensor->getDimensions();

	// 配置最小:kMIN、最优:kOPT、最大范围:kMAX       指的是BatchSize
	input_dims.d[0] = 1;
	profile->setDimensions(input_tensor->getName(), OptProfileSelector::kMIN, input_dims);
	profile->setDimensions(input_tensor->getName(), OptProfileSelector::kOPT, input_dims);
	input_dims.d[0] = max_batch_size;
	profile->setDimensions(input_tensor->getName(), OptProfileSelector::kMAX, input_dims);
	//TensorRT – Using PreviewFeaturekFASTER_DYNAMIC_SHAPES_0805 can help improve performance and resolve potential functional issues
	config->setPreviewFeature(PreviewFeature::kFASTER_DYNAMIC_SHAPES_0805, true);
	config->addOptimizationProfile(profile);

	//build engine
	half &= builder->platformHasFastFp16();
	if (half)
	{
		config->setFlag(nvinfer1::BuilderFlag::kFP16);
	}

	ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
	assert(engine != nullptr);

	IHostMemory* serialized_engine = engine->serialize();
	assert(serialized_engine != nullptr);

	// save engine
	std::ofstream p(engine_out_path, std::ios::binary);
	if (!p) {
		std::cerr << "could not open output engine path" << std::endl;
		assert(false);
	}
	p.write(reinterpret_cast<const char*>(serialized_engine->data()), serialized_engine->size());

	//release
	network->destroy();
	parser->destroy();
	engine->destroy();
	config->destroy();
	serialized_engine->destroy();
	builder->destroy();
	std::cout << "**************successed transfer onnx to trt engine***************" << std::endl;
}

实现和python一个样,主要是添加profile以及指定动态batch的最大、最小和最优值。由于kFASTER_DYNAMIC_SHAPES_0805可以提升性能,此处使用的时候对其进行了添加。

3.2 arcface TensorRT快速推理

推理大致过程如下:

  • 组装图片为相应的batch,并进行数据前处理
  • 分别分配host以及device上的内存
  • cuda加速推理获取结果
  • 结果解码+后处理计算相似度
  • 释放相应的内存
void ArcFaceInference::inference(std::vector<cv::Mat>& imgs, std::vector<cv::Mat>& res_batch)
{
	int batch = imgs.size();
	cudaStream_t stream;
	CUDA_CHECK(cudaStreamCreate(&stream));

	//input
	int input_numel = batch * 3 * imgs[0].cols * imgs[0].rows;
	float* cpu_input_buffer = nullptr;
	float* gpu_input_buffer = nullptr;
	CUDA_CHECK(cudaMallocHost((void**)(&cpu_input_buffer), input_numel * sizeof(float)));
	CUDA_CHECK(cudaMalloc((void**)(&gpu_input_buffer), input_numel * sizeof(float)));

	//output
	auto output_dims = this->initEngine->engine->getBindingDimensions(1);
	output_dims.d[0] = batch;
	int output_numel = output_dims.d[0] * output_dims.d[1];
	float* cpu_output_buffer = nullptr;
	float* gpu_output_buffer = nullptr;
	CUDA_CHECK(cudaMallocHost((void**)(&cpu_output_buffer), output_numel * sizeof(float)));
	CUDA_CHECK(cudaMalloc((void**)(&gpu_output_buffer), output_numel * sizeof(float)));

	// set input dim
	auto input_dims = this->initEngine->engine->getBindingDimensions(0);
	input_dims.d[0] = batch;
	this->initEngine->context->setBindingDimensions(0, input_dims);

	//batch process
	batchPreprocess(imgs,cpu_input_buffer);

	auto start = std::chrono::system_clock::now();
	std::cout << "************start to inference batch imgs********************" << std::endl;
	CUDA_CHECK(cudaMemcpyAsync(gpu_input_buffer, cpu_input_buffer, input_numel * sizeof(float), cudaMemcpyHostToDevice, stream));
	float* bindings[] = { gpu_input_buffer,gpu_output_buffer };
	bool success = this->initEngine->context->enqueueV2((void**)(bindings), stream, nullptr);
	CUDA_CHECK(cudaMemcpyAsync(cpu_output_buffer, gpu_output_buffer, output_numel * sizeof(float), cudaMemcpyDeviceToHost, stream));
	CUDA_CHECK(cudaStreamSynchronize(stream));
	auto end = std::chrono::system_clock::now();
	std::cout << "*************batch inference time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms" <<"*********************" << std::endl;
	//postprocess
	for (int  i = 0; i < batch; i++)
	{
		cv::Mat result = cv::Mat(1, output_dims.d[1], CV_32FC1, static_cast<float*>(&cpu_output_buffer[i * output_dims.d[1]]));
		res_batch.emplace_back(result.clone());
		// 计算相似度
		float max_similarity = 0.0;
		std::string name = "";
		for (std::map<std::string, cv::Mat>::iterator iter = obsInfo.begin(); iter != obsInfo.end(); ++iter){
			float similarity = getSimilarity(result.clone(), iter->second);
			if (similarity>max_similarity)
			{
				max_similarity = similarity;
				name = iter->first;
			}
		}
		if (!GETVECTOR)
		{
			printf("第%i张图与%s的相似都最高,相似度为:%f\n", i + 1, name.c_str(), max_similarity);
		}
		
	}
	//release
	CUDA_CHECK(cudaStreamDestroy(stream));
	CUDA_CHECK(cudaFreeHost(cpu_input_buffer));
	CUDA_CHECK(cudaFreeHost(cpu_output_buffer));
	CUDA_CHECK(cudaFree(gpu_input_buffer));
	CUDA_CHECK(cudaFree(gpu_output_buffer));
	
}

3.3 测试效果

测试以人脸检测中的五个类别数据,先分一部分数据,提取其相对应的平均特征并进行保存,测试推理时候,用推理获取的特征与既有的特征进行相似度比对,取相似度最高者为对应的类别。

具体结果如下:
在这里插入图片描述

4 总结

arcface部署实际上还是比较简单的,难点在于如何快速实现多batch推理,以及如何能够更有效的让新来的特征加入table中,以达到动态扩充的目的。
本篇文章通过实际使用案例,详述了Arcface动态batch推理的详细过程部署的,如有不足指出,请大家多多指教。

———END————

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

leetcode 2483. Minimum Penalty for a Shop(商店的最少代价)

字符串customers只包含’Y’和’N’两种字母, 表示 i 时刻商店是否有客人来。 如果商店在 i 时刻处于开门状态&#xff0c;Y’的代价是0&#xff0c;N’的代价是1.&#xff08;开门了却没有客人就算损失&#xff09;。 反之&#xff0c;在 i 时刻处于关门状态&#xff0c;N’的…

睿趣科技:开抖音小店挣钱吗到底

在当今数字化时代&#xff0c;社交媒体平台成为了创业者们寻找商机和赚钱的新途径。而抖音作为一款风靡全球的短视频分享平台&#xff0c;自然也成为了许多人开设小店、进行创业的选择之一。那么&#xff0c;开抖音小店能否真正实现盈利&#xff0c;成为了一个备受关注的话题。…

swaggo的一点小理解

如有错误&#xff0c;希望指出&#xff0c;谢谢&#xff01; 很低级的概念不清&#xff0c;大佬嘴下留情。 1.关于swag的注释 我的理解是这些注释是专门提供给Swagger UI界面测试使用的&#xff0c;根据注释内容告诉swag文档这个函数应该有哪些参数&#xff0c;从什么路由走&…

学会Mybatis框架:让你的开发事半功倍【五.Mybatis关系映射】

目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 导语 一、一对一的关系映射 1.表结构 2.resultMap配置 3.测试关系映射 二、一对多的关系映射 1.表结构 2.resultMap配置 3.测试关系映射 三、多对多的关系映射 1.表结构…

【微服务部署】一、使用docker-compose部署Jenkins、SonarQube、PostgreSQL

一、安装 1、编写docker-compose部署Postgres、SonarQube、Jenkins的yml文件jenkins-compose.yml Postgres&#xff1a;作为SonarQube的数据库存储SonarQube&#xff1a;代码质量检查Jenkins&#xff1a;jenkins/jenkins:lts镜像&#xff0c;jenkinsci/blueocean镜像缺少node…

51单片机项目(7)——基于51单片机的温湿度测量仿真

本次做的设计&#xff0c;是利用DHT11传感器&#xff0c;测量环境的温度以及湿度&#xff0c;同时具备温度报警的功能&#xff1a;利用两个按键&#xff0c;设置温度阈值的加和减&#xff0c;当所测温度大于温度阈值的时候&#xff0c;蜂鸣器就会响起&#xff0c;进行报警提示。…

C语言深入理解指针(非常详细)(二)

目录 指针运算指针-整数指针-指针指针的关系运算 野指针野指针成因指针未初始化指针越界访问指针指向的空间释放 如何规避野指针指针初始化注意指针越界指针不使用时就用NULL避免返回局部变量的地址 assert断言指针的使用和传址调用传址调用例子&#xff08;strlen函数的实现&a…

java反编译工具jd-gui使用

文章目录 一、JD-GUI介绍二、下载三、安装四、使用教程五、免责声明摘抄 一、JD-GUI介绍 JD-GUI是一个独立的图形实用程序&#xff0c;显示“.class”文件的Java源代码。 使用JD-GUI浏览重构的源代码&#xff0c;以便即时访问方法和字段。 二、下载 MAC安装包&#xff1a;ht…

链路聚合原理

文章目录 一、定义二、功能三、负载分担四、分类五、常用命令 首先可以看下思维导图&#xff0c;以便更好的理解接下来的内容。 一、定义 在网络中&#xff0c;端口聚合是一种将连接到同一台交换机的多个物理端口捆绑在一起&#xff0c;形成一个逻辑端口的技术。通过端口聚合&…

在 Spring Boot 中集成 MinIO 对象存储

MinIO 是一个开源的对象存储服务器&#xff0c;专注于高性能、分布式和兼容S3 API的存储解决方案。本文将介绍如何在 Spring Boot 应用程序中集成 MinIO&#xff0c;以便您可以轻松地将对象存储集成到您的应用中。 安装minio 拉取 minio Docker镜像 docker pull minio/minio创…

解决 .csv 文件上传到 pgsql 的字符报错问题

目录 背景问题解决办法 背景 上传 .csv 文件进行数据导入到 pg 时&#xff0c;报错显示如下&#xff1a; ods.tbl_inp_fee_detail.csv数据上传失败 报错信息:org.postgresql.util.PSQLException: ERROR: invalid byte sequence for encoding "UTF8": 0x00 Where: C…

多维时序 | Matlab实现GRU-Adaboost和GRU多变量时间序列预测对比

多维时序 | Matlab实现GRU-Adaboost和GRU多变量时间序列预测对比 目录 多维时序 | Matlab实现GRU-Adaboost和GRU多变量时间序列预测对比预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 多维时序 | Matlab实现GRU-Adaboost和GRU多变量时间序列预测对比 模型描述 M…

【python爬虫】10.指挥浏览器自动工作(selenium)

文章目录 前言selenium是什么怎么用设置浏览器引擎获取数据解析与提取数据自动操作浏览器 实操运用确认目标分析过程代码实现 本关总结 前言 上一关&#xff0c;我们认识了cookies和session。 分别学习了它们的用法&#xff0c;以及区别。 还做了一个项目&#xff1a;带着小…

Redis集群操作-----主从互换

一、将节点cluster1的主节点7000端口的redis关掉 [rootredis-cluster1 src]# ps -ef |grep redis 二、查看集群信息&#xff1a;

SpringBoot复习:(60)文件上传的自动配置类MultipartAutoConfiguration

可以看到&#xff0c;定义了一个类型为StandartServletMultipartResolver的bean 用来进行文件上传&#xff0c;定义了一个类型为MultipartConfigElement的bean用来进行上传相关的配置&#xff0c;其中使用了MultipartProperties中的属性&#xff0c;这个类的定义如下&#xff1…

Centos 6.5 升级到Centos7指导手册

一、背景 某业务系统因建设较早&#xff0c;使用的OS比较过时&#xff0c;还是centos6.5的系统&#xff0c;因国产化需要&#xff0c;需将该系统升级到BClinux 8.6&#xff0c;但官方显示不支持centos 6.x升级到8&#xff0c;需先将centos6.5升级到centos7的最新版&#xff0c…

ZDH-权限模块

本次介绍基于ZDH v5.1.2版本 目录 项目源码 预览地址 安装包下载地址 ZDH权限模块 ZDH权限模块-重要名词划分 ZDH权限模块-菜单管理 ZDH权限模块-角色管理 ZDH权限模块-用户配置 ZDH权限模块-权限申请 项目源码 zdh_web: GitHub - zhaoyachao/zdh_web: 大数据采集,抽…

高频面试题:如何分别用三种姿势实现三个线程交替打印0到100

最近面试遇到的一道题&#xff0c;需要三个线程交替打印0-100&#xff0c;当时对多线程并不是很熟悉因此没怎么写出来&#xff0c;网上搜了之后得到现 synchronized wait/notifyAll 实现思路&#xff1a;判断当前打印数字和线程数的取余&#xff0c;不等于当前线程则处于等待…

ORB-SLAM3复现过程中遇到的问题及解决办法

在复现过程中遇到的问题的解决过程 1. 版本检查1.1 Opencv版本的检测1.2 Eigen版本的检测1.3 查看Python版本1.4 其他 2. 编译过程中遇到的问题及解决办法2.1 ./build.sh遇到的问题2.2 ./build_ros.sh遇到的问题 因为环境比较干净&#xff0c;所以遇到的问题相对少一些&#xf…

多线程的五种“打开”方式

1 概念 1.1 线程是什么&#xff1f;&#xff1f; 线程&#xff08;Thread&#xff09;是计算机科学中的一个基本概念&#xff0c;它是进程&#xff08;Process&#xff09;中的一个执行单元&#xff0c;负责执行程序的指令序列。线程是操作系统能够进行调度和执行的最小单位。…