2025年1月

文件上传

文件上传漏洞是由于对上传文件的内、类型没有做严格的过滤、检查,使得攻击者可以通过上传木马文件获取服务器的webshell文件

low

上传一个php文件,上传成功,并且可以在
WWW\DVWA\hackable\uploads
目录下找到该文件

此难度没有做任何过滤,所有文件都可以上传

源码审计

没有做任何过滤,很危险的行为

<?php

if( isset( $_POST[ 'Upload' ] ) ) { // 检查表单是否提交了"Upload"按钮
	// 定义目标上传路径
	$target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/"; // 设置目标路径为一个固定目录
	$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); // 将上传文件的基础名称附加到目标路径

	// 尝试将文件移动到上传文件夹
	if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { // 如果移动失败
		// 上传失败,显示错误信息
		$html .= '<pre>Your image was not uploaded.</pre>'; // 提示用户图像未上传
	}
	else { // 如果成功
		// 上传成功
		$html .= "<pre>{$target_path} successfully uploaded!</pre>"; // 提示用户图像成功上传,并显示路径
	}
}
?>

medium

先上传一个php文件

只允许上传
jpg/png
图片,那么就上传这两种图片

上传成功

源码审计

只允许上传文件类型
jpg/png
内容,以及文件大小
小于10000
字节,过滤并不严谨

<?php

if( isset( $_POST[ 'Upload' ] ) ) { // 检查表单是否提交了"Upload"按钮
	// 定义目标上传路径
	$target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/"; // 设置目标路径为一个固定目录
	$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); // 将上传文件的基础名称附加到目标路径

	// 文件信息
	$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; // 获取上传文件的名称
	$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; // 获取上传文件的类型
	$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; // 获取上传文件的大小

	// 检查是否为图像文件
	if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) && // 如果是JPEG或PNG格式
		( $uploaded_size < 100000 ) ) { // 并且文件大小小于100000字节(约100KB)

		// 尝试将文件移动到上传文件夹
		if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { // 如果移动失败
			// 上传失败,显示错误信息
			$html .= '<pre>Your image was not uploaded.</pre>'; // 提示用户图像未上传
		}
		else { // 如果成功
			// 上传成功
			$html .= "<pre>{$target_path} successfully uploaded!</pre>"; // 提示用户图像成功上传,并显示路径
		}
	}
	else { // 如果文件不符合条件
		// 提示无效文件
		$html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; // 提示用户只接受JPEG或PNG格式的图像
	}
}
?>

high

根据提示只允许上传图片文件,上传一个jpg图片结果上传失败

上传并抓包,发送到重放器

添加文件头即可,GIF89a

源码审计

限制了文件后缀及文件内容是否有效,对文件类型过滤不严谨,如果添加文件头,则会被解析为一个jpg文件,就可以正常上传

<?php

if( isset( $_POST[ 'Upload' ] ) ) { // 检查是否有提交的"Upload"表单
	// 定义目标写入路径
	$target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/"; // 设置目标路径为指定文件夹
	$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); // 将上传文件的基础名称附加到目标路径

	// 文件信息
	$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; // 获取上传文件的名称
	$uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1); // 提取上传文件的扩展名
	$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; // 获取上传文件的大小
	$uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ]; // 获取上传文件的临时存储路径

	// 检查文件是否为图像
	if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) && // 如果扩展名是jpg、jpeg或png(不区分大小写)
		( $uploaded_size < 100000 ) && // 并且文件大小小于100000字节(约100KB)
		getimagesize( $uploaded_tmp ) ) { // 并且临时文件是有效图像

		// 尝试将文件移动到上传文件夹
		if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) { // 如果移动失败
			// 上传失败,显示错误信息
			$html .= '<pre>Your image was not uploaded.</pre>'; // 提示用户图像未上传
		}
		else { // 如果成功
			// 上传成功
			$html .= "<pre>{$target_path} successfully uploaded!</pre>"; // 提示用户图像成功上传,并显示文件路径
		}
	}
	else { // 如果文件不符合条件
		// 提示无效文件
		$html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; // 提示用户只接受JPEG或PNG格式的图像
	}
}
?>

impossible

源码审计

非常严格的过滤,对上传的文件进行了重命名(搞了一个MD5的加密),还增加了token值的校验,对文件的内容也做了严格的检查。

<?php

if( isset( $_POST[ 'Upload' ] ) ) { // 检查是否提交了"Upload"表单
	// 检查反CSRF令牌
	checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); // 验证用户令牌是否与会话令牌匹配

	// 文件信息
	$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; // 获取上传文件的名称
	$uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1); // 提取上传文件的扩展名
	$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; // 获取上传文件的大小
	$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; // 获取上传文件的类型
	$uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ]; // 获取上传文件的临时存储路径

	// 设置目标写入路径
	$target_path   = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/'; // 目标上传路径
	//$target_file   = basename( $uploaded_name, '.' . $uploaded_ext ) . '-'; // 原来可以使用的文件名
	$target_file   =  md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; // 生成一个唯一的文件名
	$temp_file     = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) ); // 获取临时文件目录
	$temp_file    .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; // 生成临时文件的完整路径

	// 检查文件是否为图像
	if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) && // 检查扩展名
		( $uploaded_size < 100000 ) && // 检查文件大小
		( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) && // 检查文件类型
		getimagesize( $uploaded_tmp ) ) { // 检查文件是否为有效图像

		// 去除任何元数据,通过重新编码图像(推荐使用php-Imagick替代php-GD)
		if( $uploaded_type == 'image/jpeg' ) { // 如果文件类型为JPEG
			$img = imagecreatefromjpeg( $uploaded_tmp ); // 创建JPEG图像
			imagejpeg( $img, $temp_file, 100); // 以最高质量重新编码并保存到临时文件
		}
		else { // 如果文件类型为PNG
			$img = imagecreatefrompng( $uploaded_tmp ); // 创建PNG图像
			imagepng( $img, $temp_file, 9); // 以最高压缩率重新编码并保存到临时文件
		}
		imagedestroy( $img ); // 销毁图像资源以释放内存

		// 尝试将文件从临时文件夹移动到目标路径
		if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) { // 如果移动成功
			// 上传成功
			$html .= "<pre><a href='{$target_path}{$target_file}'>{$target_file}</a> successfully uploaded!</pre>"; // 显示上传成功的消息,并提供文件链接
		}
		else { // 如果移动失败
			// 上传失败
			$html .= '<pre>Your image was not uploaded.</pre>'; // 提示用户图像未上传
		}

		// 删除任何临时文件
		if( file_exists( $temp_file ) ) // 如果临时文件存在
			unlink( $temp_file ); // 删除临时文件
	}
	else { // 如果文件不符合条件
		// 提示无效文件
		$html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; // 提示用户只接受JPEG或PNG格式的图像
	}
}

// 生成反CSRF令牌
generateSessionToken();
?>

(2024年4月编写)

github地址

https://github.com/sherlockchou86/video_pipe_c

作者微信

zhzhi78
(备注 videopipe),拉群交流(1000人群),及时获取代码更新。

网站介绍

http://www.videopipe.cool/

配套教程

http://www.videopipe.cool/index.php/2024/09/11/videopipetutorials/

演示Sample

http://www.videopipe.cool/index.php/2024/08/21/videopipesamples/

0、一个引子

计算机视觉(Computer Vision,后简称CV)是人工智能领域中的一个细分场景。随着深度学习技术的飞速发展,基于神经网络的图像算法已经慢慢取代了传统图像算法,也越来越多地落地到实际应用场景中。下图列举了常见的神经网络图像算法:

图1 常见神经网络图像算法

神经网络的训练、调优离不开算法工程师,近几年在众多IT技术招聘岗位中,无论是薪资待遇还是招聘数量上,算法工程师岗位都是名列前茅。那么在这个调参(炼丹)火热的时代(公司保安大爷都知道什么叫“监督学习”),如何将训练好的神经网络图像算法模型快速部署落地到实际应用场景中呢?VideoPipe视频分析框架满足这一需求,它适用于大部分视频结构化应用场景,可以快速集成训练好的图像算法模型,包括但不限于:

  • 图像分类算法
    。给定的图片是小猫还是小狗?是白毛还是黑毛?
  • 目标检测算法
    。给定的图片中包含哪些目标(车、人、文字...)?目标坐标、尺寸是多少?
  • 图像分割算法
    。为给定的图片做像素级分割,不同区域显示不同颜色。
  • 图像特征编码算法
    。为给定的图片提取“指纹级”特征,可用于图片识别、对比、检索。
  • 图像生成算法
    。随机生成指定风格的图片,或为图片转换风格(比如将手机拍摄的人物照片转成卡通风格)。

VideoPipe视频分析框架可广泛应用于智慧交通、智能安防等领域,可快速构建
人脸识别

车牌识别

交通事件检测

以图搜图

OCR文字识别

AI换脸
等软件系统。下图是使用VideoPipe实现
口算练习检查
(仿作业帮APP)的功能截图:

图2 口算练习检查

图中绿色代表
正确
,红色代表
错误
(并在括号中给出了正确答案),橙色代表
格式不对
(比如表达式中左括号没有对应的右括号)。另外,图中上半部分的
管道
代表
口算练习检查
的核心环节。下面整理了作业帮APP中
口算练习检查
功能的完整流程,我们按照该流程可以完整复现作业帮APP中的功能:

  1. 用户打开作业帮APP,点击
    口算检查
    按钮,APP弹出相机界面准备拍照
  2. 用户将手机对准口算作业本,保持相机画面正对口算题目,点击
    拍照
    按钮
  3. 作业帮APP抓拍一张照片,通过http协议上传到作业帮服务器
  4. 服务器收到图片数据(编码数据),对其进行解码,生成图片
  5. 服务器调用OCR文字识别算法,对图片中的口算题目进行检测和识别,得到String类型字符串,类似
    1+1=2
  6. 服务器调用数学表达式解析库(一种可以对数学表达式进行解析和计算的工具库),计算
    =
    号左边的结果(这里结果是2),然后将左边结果和
    =
    号右边的值(学生答题结果,这里也是2)进行比较,给出对错结论
  7. 按照步骤6的方式检查整张图片中的口算表达式,最后计算总得分
  8. 服务器将检查结果(对、错、无效)用不同颜色叠加到原上传图片上
  9. 服务器将最终结果(含叠加结果的图片)返回给作业帮APP,APP呈现给用户

我们可以看到,
口算练习检查
整个流程包含了多个处理环节和技术栈,我用图片直观描述如下:

图3 口算练习检查流程

图中
虚线
部分全部可以借助VideoPipe来实现,
图片接收和解码

字符检测和识别

数学表达式解析和计算

检查结果叠加
,这些环节在VideoPipe中有节点类型一一与之对应:

  • vp_image_src_node
    节点:完成接收图片和解码工作
  • vp_ppocr_text_detector_node
    节点:完成字符检测和识别
  • vp_expr_check_node
    节点:完成数学表达式解析和计算
  • vp_expr_osd_node
    节点:完成检查结果叠加

VideoPipe可以帮助你实现
口算练习检查
的核心环节,当然VideoPipe能做的远不止于此,接下来我来对它做详细介绍。

1、VideoPipe介绍

一个用于视频(含图片,下同)分析的图像算法集成框架。它可以处理复杂任务,如流读取(从本地或网络)、解码、基于深度学习模型推理(分类/检测/特征提取/...)、跟踪、行为分析(逻辑处理)、数据叠加(OSD)、数据转发(如Kafka/Socket)、编码和流推送(RTMP或本地文件)。整个框架采用面向插件的编程风格,我们可以使用各种不同类型的插件,即框架中的
Node
类型,来构建不同类型的视频分析管道。

图4 VideoPipe工作管道(示意)

VideoPipe类似于英伟达的DeepStream和华为的mxVision,但它更易于使用,更具备可移植性,并且对于像GStreamer这样难以学习(编程风格或调试方面)的第三方模块依赖较少。该框架纯粹由原生C++ STL编写,只依赖主流的第三方库(如OpenCV),因此代码更易于在不同平台上移植。

注意:
VideoPipe是一个让计算机视觉领域中算法模型集成更加简单的框架,它并不是TensorFlow、TensorRT类似的深度学习框架。

2、主要功能

VideoPipe包含以下主要功能:

  • 流读取。支持主流的视频流协议,如udp(视频或图像)、rtsp、rtmp、文件(视频或图像)。
  • 视频解码。支持基于OpenCV/GStreamer的视频(图片)解码(支持硬件加速)。
  • 基于深度学习的算法推理。支持基于深度学习算法的多级推理,例如目标检测、图像分类、特征提取。你只需准备好模型并了解如何解析其输出即可。推理可以基于不同的后端实现,如
    OpenCV::DNN(默认)

    TensorRT

    PaddleInference

    ONNXRuntime
    等,任何你喜欢的都可以。
  • 屏幕显示(OSD)。支持可视化,如将模型输出结果绘制到帧上。
  • 数据代理。支持将结构化数据(json/xml/自定义格式)以kafka/Sokcet等方式推送到云端、文件或其他第三方平台。
  • 目标跟踪。支持目标追踪,例如IOU、SORT跟踪算法等。
  • 行为分析(BA)。支持基于跟踪的行为分析,例如越线、停车、违章等交通判断。
  • 录制。支持特定时间段的视频录制,特定帧的截图。
  • 视频编码。支持基于OpenCV/GStreamer的视频(图片)编码(支持硬件加速)。
  • 流推送。支持通过rtmp、rtsp(无需专门的rtsp服务器)、文件(视频/图像)、udp(仅限图像)、屏幕显示(GUI)进行流推送或结果展示。

3、主要特点

VideoPipe具备以下特点:

  1. 可视化管道,对程序调试非常有帮助。管道的运行状态会自动在屏幕上刷新,包括管道中每个连接点的fps、缓存大小、延迟等信息,你可以根据这些运行信息快速确定管道的瓶颈所在。
  2. 节点之间通过智能指针传递数据,默认情况下是浅拷贝,当数据在整个管道中流动时无需进行内容拷贝操作。当然,如果需要,你可以指定深拷贝,例如当管道具有多个分支时,你需要分别在两个不同的内容拷贝上进行操作。
  3. 你可以构建不同类型的管道,支持单通道或多通道的管道,管道中的各通道是独立的。每个通道可以拆分成多个分支并行处理不同的任务,之后进行数据同步,再合并回一个分支进行输出。
  4. 管道支持钩子(回调),你可以向管道注册回调以获取状态通知(参见第1项特点),例如实时获取某个连接点的fps。
  5. VideoPipe中已经内置了丰富的节点(Node)类型,所有节点都可以由用户重新实现,也可以根据你的实际需求实现更多节点类型。
  6. 支持动态操作管道,支持多线程并行操作,支持
    热插拔
    操作模式(管道无需先暂停,即插即用)。
  7. 整个框架主要由原生C++编写,可在所有平台上移植。

4、如何使用

4.1 快速开始

下面是一个如何构建Pipeline然后运行的Sample(请先修改代码中的相关文件路径):

#include "../nodes/vp_file_src_node.h"
#include "../nodes/infers/vp_yunet_face_detector_node.h"
#include "../nodes/infers/vp_sface_feature_encoder_node.h"
#include "../nodes/osd/vp_face_osd_node_v2.h"
#include "../nodes/vp_screen_des_node.h"
#include "../nodes/vp_rtmp_des_node.h"
#include "../utils/analysis_board/vp_analysis_board.h"

/*
* ## 1-1-N sample ##
* 1个视频输入,1个视频分析任务(人脸检测和识别),2个输出(屏幕输出/RTMP推流输出)
*/

int main() {
    VP_SET_LOG_INCLUDE_CODE_LOCATION(false);
    VP_SET_LOG_INCLUDE_THREAD_ID(false);
    VP_LOGGER_INIT();

    // 创建节点
    auto file_src_0 = std::make_shared<vp_nodes::vp_file_src_node>("file_src_0", 0, "./test_video/10.mp4", 0.6);
    auto yunet_face_detector_0 = std::make_shared<vp_nodes::vp_yunet_face_detector_node>("yunet_face_detector_0", "./models/face/face_detection_yunet_2022mar.onnx");
    auto sface_face_encoder_0 = std::make_shared<vp_nodes::vp_sface_feature_encoder_node>("sface_face_encoder_0", "./models/face/face_recognition_sface_2021dec.onnx");
    auto osd_0 = std::make_shared<vp_nodes::vp_face_osd_node_v2>("osd_0");
    auto screen_des_0 = std::make_shared<vp_nodes::vp_screen_des_node>("screen_des_0", 0);
    auto rtmp_des_0 = std::make_shared<vp_nodes::vp_rtmp_des_node>("rtmp_des_0", 0, "rtmp://192.168.77.60/live/10000");

    // 构建管道
    yunet_face_detector_0->attach_to({file_src_0});
    sface_face_encoder_0->attach_to({yunet_face_detector_0});
    osd_0->attach_to({sface_face_encoder_0});

    // 管道自动拆分
    screen_des_0->attach_to({osd_0});
    rtmp_des_0->attach_to({osd_0});

    // 启动管道
    file_src_0->start();

    // 可视化管道
    vp_utils::vp_analysis_board board({file_src_0});
    board.display();
}

上面代码运行后,会出现3个画面:

  1. 管道的运行状态图,状态自动刷新
  2. 屏幕显示结果(GUI)
  3. 播放器显示结果(RTMP)

图5 1-1-N_sample运行截图

在上面Sample中,VideoPipe从本地读取视频文件,然后检测其中的人脸、对每个人脸提取特征、比较2张人脸的相似度、将结果叠加到视频上,最后在屏幕中显示,并以RTMP的形式将视频推到服务器上供互联网其他用户播放。整个流程完全由VideoPipe帮忙完成,
插件式
编程风格,直观、灵活。

4.2 更多案例

github仓库中有接近40个Sample,涵盖
人脸识别

车辆检测分类

以图搜图

目标跟踪

交通事件检测

车牌识别

动态管道

图像分割
等等。下面是
拥堵检测

口算练习检查
的截图:

车牌识别

拥堵检测

更多案例请访问github仓库。

5、原理细节

下面给大家详细介绍框架实现原理和技术细节,对该部分感兴趣的小伙伴可以加我微信交流(见文章末尾)。

5.1 视频结构化应用的核心环节

视频结构化
是将非结构化数据(视频/图片)转换为结构化数据的过程。非结构化数据通常包括:

  • 视频
  • 图像
  • 音频
  • 自然语言文本

而结构化数据主要包括诸如 JSON、XML或数据库中的数据表等,
这些数据可以直接由机器(程序)处理
。具体到视频(含图片,下同)方面,结构化的过程主要涉及以下核心部分:

  • 读取流
    。从网络或本地机器获取视频流。
  • 解码
    。将字节流解码为帧,因为算法只能作用于图像。
  • 推理
    。对图像进行深度学习推理,如检测、分类或特征提取。
  • 跟踪
    。跟踪视频中的目标。
  • 行为分析/逻辑处理
    。分析目标的轨迹、属性。
  • OSD
    。在图像上显示结果,用于调试或得到直观效果。
  • 消息代理
    。将结构化数据推送到外部,供业务平台使用。
  • 编码
    。对包含结果的帧进行编码,以便传输、存储。
  • 推送流
    。将字节流推送到外部或直接保存。

上述每个环节对应
VideoPipe
中的一种插件类型,即代码中的
Node

5.2 VideoPipe中的Node

VideoPipe中的每个Node负责一种任务(严格遵循
单一职责原则
),例如解码或推理。我们可以将许多节点串在一起构建成管道,并让视频数据流经整个管道。每个Node内部都有两个队列,一个用于缓存上游节点推送的数据,另一个用于缓存等待被推送到下游节点的数据。我们可以在两个队列之间编写逻辑代码,这是典型的
生产者-消费者
模式。

默认情况下,生产者和消费者在节点内部使用单个线程工作,因此在处理复杂任务时(例如,在
vp_message_broker_node
中推送数据是一个耗时的操作),我们需要编写异步代码来避免阻塞管道。

VideoPipe中有三种类型的节点,分别是:

  • SRC节点
    :源节点,数据被创建的地方(内部只有一个队列,用于缓存被推送到下游节点的数据)。
  • MID节点
    :中间节点,数据将在此处理。
  • DES节点
    :目标节点,数据消失的地方(内部只有一个队列,用于缓存来自上游节点的数据)。

每个节点本身具有合并多个上游节点和拆分成多个下游节点的能力。注意,默认情况下节点在将数据从一个节点传输到另一个节点时使用浅拷贝和等值拷贝。如果您需要深拷贝或希望按通道索引传输数据(希望数据不混淆),则在分裂点添加一个
vp_split_node
类型节点。

5.3 VideoPipe中的数据流

视频是一种重量级数据,因此频繁进行深拷贝会降低管道的性能。实际上,VideoPipe中两个节点之间传递的数据默认使用智能指针,一旦数据由源节点创建,数据内容在整个管道中大多数时间不会被复制(但如果需要,我们可以指定深度拷贝模式,例如使用
vp_split_node
)。

视频由连续的帧组成,VideoPipe 逐帧处理这些帧,因此帧元数据中的帧索引也会连续增加。

5.4 VideoPipe中的钩子

钩子是一种机制,让主体在发生某些事件时通知检测者,VideoPipe也支持钩子。管道触发回调函数(
std::function
)与外部代码通信,例如实时推送管道自身的
fps

延迟

其他状态信息
。我们在编写回调函数内部代码时,不允许有阻塞出现,否则影响整个管道性能。

钩子有助于调试我们的应用程序,并快速找出整个管道中的瓶颈,VideoPipe框架中自带的可视化工具
vp_analysis_board
就依赖于钩子机制。

5.5 在VideoPipe中实现新的Node类型

vp_node
是VideoPipe中所有节点的基类。我们可以定义一个从
vp_node
派生的新节点类,并重写一些虚函数,如
handle_frame_meta

handle_control_meta

  • handle_frame_meta
    :处理流经当前节点的帧数据。
  • handle_control_meta
    :处理流经当前节点的控制指令数据。

帧数据指的是VideoPipe中的
vp_frame_meta
,其中包含与帧相关的数据,如
帧索引

数据缓冲区

原始宽度
等等。控制指令数据指的是VideoPipe中的
vp_control_meta
,其中包含与命令相关的数据,例如
记录视频

记录图像
等。

注意,并非所有流经当前节点的数据都应该被处理,我们只需要处理我们感兴趣的内容。

六、相似框架对比

VideoPipe主要用途与DeepStream/mxVision相似,用于快速构建视频分析应用系统。但三者之间存在一些差异:

序号 名称 是否开源 学习门槛 适用平台 性能 三方依赖
1 DeepStream 仅限英伟达
2 mxVision 仅限华为
3 VideoPipe 不限平台

DeepStream和mxVision(包括其他AI硬件厂家推出的类似框架)只可用于自家硬件平台,性能相对较高,VideoPipe由于要兼容各种不同硬件,数据共享方面弱于前者。VideoPipe学习门槛更低,核心代码完全开源,编码风格和代码调试方便均要优于其他类似框架(VideoPipe没有GObject相关内容)。下图显示视频分析过程中
数据共享程度
对整个处理管道性能的影响:

上图左侧数据来回在CPU和GPU之间传递,影响整个管道处理速度;右侧大部分操作全部在GPU侧完成,减少数据来回传递次数,数据共享程度更高,有利于管道处理性能。

配置解析主体方法

public Configuration parse() {  
    if (parsed) {  
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");  
    }  
    parsed = true;  
    //源码中没有这一句,只有 parseConfiguration(parser.evalNode("/configuration"));  
    //为了让读者看得更明晰,源码拆分为以下两句  
    XNode configurationNode = parser.evalNode("/configuration");  
    parseConfiguration(configurationNode);  
    return configuration;  
}  
/** 
 * 解析 "/configuration"节点下的子节点信息,然后将解析的结果设置到Configuration对象中 
 */  
private void parseConfiguration(XNode root) {  
    try {  
        //1.首先处理properties 节点     
        propertiesElement(root.evalNode("properties")); //issue #117 read properties first  
        //2.处理typeAliases  
        typeAliasesElement(root.evalNode("typeAliases"));  
        //3.处理插件  
        pluginElement(root.evalNode("plugins"));  
        //4.处理objectFactory  
        objectFactoryElement(root.evalNode("objectFactory"));  
        //5.objectWrapperFactory  
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));  
        //6.settings  
        settingsElement(root.evalNode("settings"));  
        //7.处理environments  
        environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631  
        //8.database  
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));  
        //9.typeHandlers  
        typeHandlerElement(root.evalNode("typeHandlers"));  
        //10.mappers  
        mapperElement(root.evalNode("mappers"));  
    } catch (Exception e) {  
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);  
    }  
} 

通过以上源码,就能看出,在mybatis的配置文件中:

  • configuration节点为根节点。
  • 在configuration节点之下,我们可以配置10个子节点, 分别为:properties、typeAliases、plugins、objectFactory、objectWrapperFactory、settings、environments、databaseIdProvider、typeHandlers、mappers。

配置文件元素

properties

<configuration>
    <!-- 方法一: 从外部指定properties配置文件, 除了使用resource属性指定外,还可通过url属性指定url  
        <properties resource="dbConfig.properties"></properties> 
    -->
    <!-- 方法二: 直接配置为xml -->
    <properties>
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/test1"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </properties>

那么,要是两种方法都同时配置了,那么最终会采用什么样的配置呢?

  1. 首先会先检查文件中的xml配置 和 外部指定的properties(也就是resource),如果两个同时配置了,那么就会报异常
  2. 接着会加载Java Configuration的配置
    1. 如果有Configuration的配置,那么最终会使用Configuration的配置
    2. 如果没有Configuration的配置,那么最终会使用上一步的xml的配置或resource配置

这是因为配置是存放在Properties,它继承自HashTable类,当依次将上述几种配置源put进去时,后加载的配置会覆盖先加载的配置。所以,最终应用配置时Configuration配置优先级最高,其次是另外两种中的一种。具体可以参考接下来的源码分析。

envirements

<environments default="development">
    <environment id="development">
        <!-- 
        JDBC–这个配置直接简单使用了JDBC的提交和回滚设置。它依赖于从数据源得到的连接来管理事务范围。
        MANAGED–这个配置几乎没做什么。它从来不提交或回滚一个连接。而它会让容器来管理事务的整个生命周期(比如Spring或JEE应用服务器的上下文)。
        -->
        <transactionManager type="JDBC"/>
        <!--
        UNPOOLED–这个数据源的实现是每次被请求时简单打开和关闭连接
        POOLED–mybatis实现的简单的数据库连接池类型,它使得数据库连接可被复用,不必在每次请求时都去创建一个物理的连接。
        JNDI – 通过jndi从tomcat之类的容器里获取数据源。
        -->
        <dataSource type="POOLED">
            <!--
            如果上面没有指定数据库配置的properties文件,那么此处可以这样直接配置 
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/test1"/>
            <property name="username" value="root"/>
            <property name="password" value="root"/>
            -->
         
            <!-- 上面指定了数据库配置文件, 配置文件里面也是对应的这四个属性 -->
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>  
        </dataSource>
    </environment>
    
    <!-- 我再指定一个environment -->
    <environment id="test">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <!-- 与上面的url不一样 -->
            <property name="url" value="jdbc:mysql://localhost:3306/demo"/>
            <property name="username" value="root"/>
            <property name="password" value="root"/>
        </dataSource>
    </environment>
</environments>

environments元素节点可以配置多个environment子节点, 怎么理解呢?

假如我们系统的开发环境和正式环境所用的数据库不一样(这是肯定的), 那么可以设置两个environment, 两个id分别对应开发环境(dev)和正式环境(final),那么通过配置environments的default属性就能选择对应的environment了, 例如,我将environments的deault属性的值配置为dev, 那么就会选择dev的environment。 那么这个是怎么实现的呢?

看源码: mybatis 是通过XMLConfigBuilder这个类在解析mybatis配置文件的,XMLConfigBuilder对于environments的解析:

public class XMLConfigBuilder extends BaseBuilder {

    private boolean parsed;
    // xml解析器
    private XPathParser parser;
    private String environment;
  
    
    // 看看解析enviroments元素节点的方法
    private void environmentsElement(XNode context) throws Exception {
        if (context != null) {
            if (environment == null) {
                //解析environments节点的default属性的值
                //例如: <environments default="development">
                environment = context.getStringAttribute("default");
            }
            //递归解析environments子节点
            for (XNode child : context.getChildren()) {
                //<environment id="development">, 只有enviroment节点有id属性,那么这个属性有何作用?
                //environments 节点下可以拥有多个 environment子节点
                //类似于这样: <environments default="development"><environment id="development">...</environment><environment id="test">...</environments>
                //意思就是可以对应多个环境,比如开发环境,测试环境等, 由environments的default属性去选择对应的enviroment
                String id = child.getStringAttribute("id");
                //isSpecial就是根据由environments的default属性去选择对应的enviroment
                if (isSpecifiedEnvironment(id)) {
                    //事务, mybatis有两种:JDBC 和 MANAGED, 配置为JDBC则直接使用JDBC的事务,配置为MANAGED则是将事务托管给容器, 
                    TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                    //enviroment节点下面就是dataSource节点了,解析dataSource节点(下面会贴出解析dataSource的具体方法)
                    DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                    DataSource dataSource = dsFactory.getDataSource();
                    Environment.Builder environmentBuilder = new Environment.Builder(id)
                          .transactionFactory(txFactory)
                          .dataSource(dataSource);
                    //将dataSource设置进configuration对象
                    configuration.setEnvironment(environmentBuilder.build());
                }
            }
        }
    }
    
    //dataSource的解析方法
    private DataSourceFactory dataSourceElement(XNode context) throws Exception {
        if (context != null) {
            //dataSource的连接池
            String type = context.getStringAttribute("type");
            //子节点 name, value属性set进一个properties对象
            Properties props = context.getChildrenAsProperties();
            //创建dataSourceFactory
            DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
            factory.setProperties(props);
            return factory;
        }
        throw new BuilderException("Environment declaration requires a DataSourceFactory.");
    } 
}

还有一个问题, 在配置dataSource的时候使用了 ${driver} 这种表达式, 那么这种形式是怎么解析的?其实,是通过PropertyParser这个类解析:

/**
 * 这个类解析${}这种形式的表达式
 */
public class PropertyParser {

    public static String parse(String string, Properties variables) {
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        return parser.parse(string);
    }

    private static class VariableTokenHandler implements TokenHandler {
        private Properties variables;

        public VariableTokenHandler(Properties variables) {
            this.variables = variables;
        }

        public String handleToken(String content) {
            if (variables != null && variables.containsKey(content)) {
                return variables.getProperty(content);
            }
            return "${" + content + "}";
        }
    }
}

以上就是对于properties 和 environments元素节点的分析,比较重要的都在对于源码的注释中标出。

typeAliases

typeAliases节点主要用来设置别名,其实这是挺好用的一个功能, 通过配置别名,我们不用再指定完整的包名,并且还能取别名。

例如: 我们在使用 com.demo.entity. UserEntity 的时候,我们可以直接配置一个别名user, 这样以后在配置文件中要使用到com.demo.entity.UserEntity的时候,直接使用User即可。

就以上例为例,我们来实现一下,看看typeAliases的配置方法:

<configuration>
    <typeAliases>
        <!--
        通过package, 可以直接指定package的名字, mybatis会自动扫描你指定包下面的javabean,
        并且默认设置一个别名,默认的名字为: javabean 的首字母小写的非限定类名来作为它的别名。
        也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user) 
        <package name="com.dy.entity"/>
        -->
        <typeAlias alias="UserEntity" type="com.dy.entity.User"/>
    </typeAliases>
  
    ......
  
</configuration>

再写一段测试代码,看看有没生效:(我只写一段伪代码)

Configuration con = sqlSessionFactory.getConfiguration();
Map<String, Class<?>> typeMap = con.getTypeAliasRegistry().getTypeAliases();
for(Entry<String, Class<?>> entry: typeMap.entrySet()) {
    System.out.println(entry.getKey() + " ================> " + entry.getValue().getSimpleName());
}

typeAliasesElement:

/**
 * 解析typeAliases节点
 */
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            //如果子节点是package, 那么就获取package节点的name属性, mybatis会扫描指定的package
            if ("package".equals(child.getName())) {
                String typeAliasPackage = child.getStringAttribute("name");
                //TypeAliasRegistry 负责管理别名, 这儿就是通过TypeAliasRegistry 进行别名注册, 下面就会看看TypeAliasRegistry源码
                configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
                //如果子节点是typeAlias节点,那么就获取alias属性和type的属性值
                String alias = child.getStringAttribute("alias");
                String type = child.getStringAttribute("type");
                try {
                    Class<?> clazz = Resources.classForName(type);
                    if (alias == null) {
                        typeAliasRegistry.registerAlias(clazz);
                    } else {
                        typeAliasRegistry.registerAlias(alias, clazz);
                    }
                } catch (ClassNotFoundException e) {
                    throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
                }
            }
        }
    }
}

重要的源码在这儿:TypeAliasRegistry.java

public class TypeAliasRegistry {
  
  //这就是核心所在啊, 原来别名就仅仅通过一个HashMap来实现, key为别名, value就是别名对应的类型(class对象)
  private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();

  /**
   * 以下就是mybatis默认为我们注册的别名
   */
  public TypeAliasRegistry() {
    registerAlias("string", String.class);

    registerAlias("byte", Byte.class);
    registerAlias("long", Long.class);
    registerAlias("short", Short.class);
    registerAlias("int", Integer.class);
    registerAlias("integer", Integer.class);
    registerAlias("double", Double.class);
    registerAlias("float", Float.class);
    registerAlias("boolean", Boolean.class);

    registerAlias("byte[]", Byte[].class);
    registerAlias("long[]", Long[].class);
    registerAlias("short[]", Short[].class);
    registerAlias("int[]", Integer[].class);
    registerAlias("integer[]", Integer[].class);
    registerAlias("double[]", Double[].class);
    registerAlias("float[]", Float[].class);
    registerAlias("boolean[]", Boolean[].class);

    registerAlias("_byte", byte.class);
    registerAlias("_long", long.class);
    registerAlias("_short", short.class);
    registerAlias("_int", int.class);
    registerAlias("_integer", int.class);
    registerAlias("_double", double.class);
    registerAlias("_float", float.class);
    registerAlias("_boolean", boolean.class);

    registerAlias("_byte[]", byte[].class);
    registerAlias("_long[]", long[].class);
    registerAlias("_short[]", short[].class);
    registerAlias("_int[]", int[].class);
    registerAlias("_integer[]", int[].class);
    registerAlias("_double[]", double[].class);
    registerAlias("_float[]", float[].class);
    registerAlias("_boolean[]", boolean[].class);

    registerAlias("date", Date.class);
    registerAlias("decimal", BigDecimal.class);
    registerAlias("bigdecimal", BigDecimal.class);
    registerAlias("biginteger", BigInteger.class);
    registerAlias("object", Object.class);

    registerAlias("date[]", Date[].class);
    registerAlias("decimal[]", BigDecimal[].class);
    registerAlias("bigdecimal[]", BigDecimal[].class);
    registerAlias("biginteger[]", BigInteger[].class);
    registerAlias("object[]", Object[].class);

    registerAlias("map", Map.class);
    registerAlias("hashmap", HashMap.class);
    registerAlias("list", List.class);
    registerAlias("arraylist", ArrayList.class);
    registerAlias("collection", Collection.class);
    registerAlias("iterator", Iterator.class);

    registerAlias("ResultSet", ResultSet.class);
  }

  
  /**
   * 处理别名, 直接从保存有别名的hashMap中取出即可
   */
  @SuppressWarnings("unchecked")
  public <T> Class<T> resolveAlias(String string) {
    try {
      if (string == null) return null;
      String key = string.toLowerCase(Locale.ENGLISH); // issue #748
      Class<T> value;
      if (TYPE_ALIASES.containsKey(key)) {
        value = (Class<T>) TYPE_ALIASES.get(key);
      } else {
        value = (Class<T>) Resources.classForName(string);
      }
      return value;
    } catch (ClassNotFoundException e) {
      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
  }
  
  /**
   * 配置文件中配置为package的时候, 会调用此方法,根据配置的报名去扫描javabean ,然后自动注册别名
   * 默认会使用 Bean 的首字母小写的非限定类名来作为它的别名
   * 也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user)
   */
  public void registerAliases(String packageName){
    registerAliases(packageName, Object.class);
  }

  public void registerAliases(String packageName, Class<?> superType){
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    for(Class<?> type : typeSet){
      // Ignore inner classes and interfaces (including package-info.java)
      // Skip also inner classes. See issue #6
      if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
        registerAlias(type);
      }
    }
  }

  public void registerAlias(Class<?> type) {
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      alias = aliasAnnotation.value();
    } 
    registerAlias(alias, type);
  }

  //这就是注册别名的本质方法, 其实就是向保存别名的hashMap新增值而已, 呵呵, 别名的实现太简单了,对吧
  public void registerAlias(String alias, Class<?> value) {
    if (alias == null) throw new TypeException("The parameter alias cannot be null");
    String key = alias.toLowerCase(Locale.ENGLISH); // issue #748
    if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
      throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
    }
    TYPE_ALIASES.put(key, value);
  }

  public void registerAlias(String alias, String value) {
    try {
      registerAlias(alias, Resources.classForName(value));
    } catch (ClassNotFoundException e) {
      throw new TypeException("Error registering type alias "+alias+" for "+value+". Cause: " + e, e);
    }
  }
  
  /**
   * 获取保存别名的HashMap, Configuration对象持有对TypeAliasRegistry的引用,因此,如果需要,我们可以通过Configuration对象获取
   */
  public Map<String, Class<?>> getTypeAliases() {
    return Collections.unmodifiableMap(TYPE_ALIASES);
  }

}

由源码可见,设置别名的原理就这么简单,Mybatis默认给我们设置了不少别名,在上面代码中都可以见到。

TypeHandler

Mybatis中的TypeHandler是什么?

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。Mybatis默认为我们实现了许多TypeHandler, 当我们没有配置指定TypeHandler时,Mybatis会根据参数或者返回结果的不同,默认为我们选择合适的TypeHandler处理。

那么,Mybatis为我们实现了哪些TypeHandler呢? 我们怎么自定义实现一个TypeHandler ? 这些都会在接下来的mybatis的源码中看到。

先看看配置:

<configuration>
    <typeHandlers>
      <!-- 
          当配置package的时候,mybatis会去配置的package扫描TypeHandler
          <package name="com.dy.demo"/>
       -->
      
      <!-- handler属性直接配置我们要指定的TypeHandler -->
      <typeHandler handler=""/>
      
      <!-- javaType 配置java类型,例如String, 如果配上javaType, 那么指定的typeHandler就只作用于指定的类型 -->
      <typeHandler javaType="" handler=""/>
      
      <!-- jdbcType 配置数据库基本数据类型,例如varchar, 如果配上jdbcType, 那么指定的typeHandler就只作用于指定的类型  -->
      <typeHandler jdbcType="" handler=""/>
      
      <!-- 也可两者都配置 -->
      <typeHandler javaType="" jdbcType="" handler=""/>
      
  </typeHandlers>
  
  ......
  
</configuration>

typeHandlerElement

老规矩,先从对xml的解析讲起

/**
 * 解析typeHandlers节点
 */
private void typeHandlerElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //子节点为package时,获取其name属性的值,然后自动扫描package下的自定义typeHandler
        if ("package".equals(child.getName())) {
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          //子节点为typeHandler时, 可以指定javaType属性, 也可以指定jdbcType, 也可两者都指定
          //javaType 是指定java类型
          //jdbcType 是指定jdbc类型(数据库类型: 如varchar)
          String javaTypeName = child.getStringAttribute("javaType");
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          //handler就是我们配置的typeHandler
          String handlerTypeName = child.getStringAttribute("handler");
          //resolveClass方法就是我们上篇文章所讲的TypeAliasRegistry里面处理别名的方法
          Class<?> javaTypeClass = resolveClass(javaTypeName);
          //JdbcType是一个枚举类型,resolveJdbcType方法是在获取枚举类型的值
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class<?> typeHandlerClass = resolveClass(handlerTypeName);
          //注册typeHandler, typeHandler通过TypeHandlerRegistry这个类管理
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
}

接下来看看TypeHandler的管理注册类:TypeHandlerRegistry.java

/**
 * typeHandler注册管理类
 */
public final class TypeHandlerRegistry {

  //源码一上来,二话不说,几个大大的HashMap就出现,这不又跟上次讲的typeAliases的注册类似么

  //基本数据类型与其包装类
  private static final Map<Class<?>, Class<?>> reversePrimitiveMap = new HashMap<Class<?>, Class<?>>() {
    private static final long serialVersionUID = 1L;
    {
      put(Byte.class, byte.class);
      put(Short.class, short.class);
      put(Integer.class, int.class);
      put(Long.class, long.class);
      put(Float.class, float.class);
      put(Double.class, double.class);
      put(Boolean.class, boolean.class);
      put(Character.class, char.class);
    }
  };

  //这几个MAP不用说就知道存的是什么东西吧,命名的好处
  private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler<?>>(JdbcType.class);
  private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<Type, Map<JdbcType, TypeHandler<?>>>();
  private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
  private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<Class<?>, TypeHandler<?>>();

  //就像上篇文章讲的typeAliases一样,mybatis也默认给我们注册了不少的typeHandler
  //具体如下
  public TypeHandlerRegistry() {
    register(Boolean.class, new BooleanTypeHandler());
    register(boolean.class, new BooleanTypeHandler());
    register(JdbcType.BOOLEAN, new BooleanTypeHandler());
    register(JdbcType.BIT, new BooleanTypeHandler());

    register(Byte.class, new ByteTypeHandler());
    register(byte.class, new ByteTypeHandler());
    register(JdbcType.TINYINT, new ByteTypeHandler());

    register(Short.class, new ShortTypeHandler());
    register(short.class, new ShortTypeHandler());
    register(JdbcType.SMALLINT, new ShortTypeHandler());

    register(Integer.class, new IntegerTypeHandler());
    register(int.class, new IntegerTypeHandler());
    register(JdbcType.INTEGER, new IntegerTypeHandler());

    register(Long.class, new LongTypeHandler());
    register(long.class, new LongTypeHandler());

    register(Float.class, new FloatTypeHandler());
    register(float.class, new FloatTypeHandler());
    register(JdbcType.FLOAT, new FloatTypeHandler());

    register(Double.class, new DoubleTypeHandler());
    register(double.class, new DoubleTypeHandler());
    register(JdbcType.DOUBLE, new DoubleTypeHandler());

    register(String.class, new StringTypeHandler());
    register(String.class, JdbcType.CHAR, new StringTypeHandler());
    register(String.class, JdbcType.CLOB, new ClobTypeHandler());
    register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
    register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler());
    register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
    register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
    register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
    register(JdbcType.CHAR, new StringTypeHandler());
    register(JdbcType.VARCHAR, new StringTypeHandler());
    register(JdbcType.CLOB, new ClobTypeHandler());
    register(JdbcType.LONGVARCHAR, new ClobTypeHandler());
    register(JdbcType.NVARCHAR, new NStringTypeHandler());
    register(JdbcType.NCHAR, new NStringTypeHandler());
    register(JdbcType.NCLOB, new NClobTypeHandler());

    register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
    register(JdbcType.ARRAY, new ArrayTypeHandler());

    register(BigInteger.class, new BigIntegerTypeHandler());
    register(JdbcType.BIGINT, new LongTypeHandler());

    register(BigDecimal.class, new BigDecimalTypeHandler());
    register(JdbcType.REAL, new BigDecimalTypeHandler());
    register(JdbcType.DECIMAL, new BigDecimalTypeHandler());
    register(JdbcType.NUMERIC, new BigDecimalTypeHandler());

    register(Byte[].class, new ByteObjectArrayTypeHandler());
    register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
    register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
    register(byte[].class, new ByteArrayTypeHandler());
    register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
    register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
    register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
    register(JdbcType.BLOB, new BlobTypeHandler());

    register(Object.class, UNKNOWN_TYPE_HANDLER);
    register(Object.class, JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
    register(JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);

    register(Date.class, new DateTypeHandler());
    register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
    register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
    register(JdbcType.TIMESTAMP, new DateTypeHandler());
    register(JdbcType.DATE, new DateOnlyTypeHandler());
    register(JdbcType.TIME, new TimeOnlyTypeHandler());

    register(java.sql.Date.class, new SqlDateTypeHandler());
    register(java.sql.Time.class, new SqlTimeTypeHandler());
    register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());

    // issue #273
    register(Character.class, new CharacterTypeHandler());
    register(char.class, new CharacterTypeHandler());
  }

  public boolean hasTypeHandler(Class<?> javaType) {
    return hasTypeHandler(javaType, null);
  }

  public boolean hasTypeHandler(TypeReference<?> javaTypeReference) {
    return hasTypeHandler(javaTypeReference, null);
  }

  public boolean hasTypeHandler(Class<?> javaType, JdbcType jdbcType) {
    return javaType != null && getTypeHandler((Type) javaType, jdbcType) != null;
  }

  public boolean hasTypeHandler(TypeReference<?> javaTypeReference, JdbcType jdbcType) {
    return javaTypeReference != null && getTypeHandler(javaTypeReference, jdbcType) != null;
  }

  public TypeHandler<?> getMappingTypeHandler(Class<? extends TypeHandler<?>> handlerType) {
    return ALL_TYPE_HANDLERS_MAP.get(handlerType);
  }

  public <T> TypeHandler<T> getTypeHandler(Class<T> type) {
    return getTypeHandler((Type) type, null);
  }

  public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference) {
    return getTypeHandler(javaTypeReference, null);
  }

  public TypeHandler<?> getTypeHandler(JdbcType jdbcType) {
    return JDBC_TYPE_HANDLER_MAP.get(jdbcType);
  }

  public <T> TypeHandler<T> getTypeHandler(Class<T> type, JdbcType jdbcType) {
    return getTypeHandler((Type) type, jdbcType);
  }

  public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference, JdbcType jdbcType) {
    return getTypeHandler(javaTypeReference.getRawType(), jdbcType);
  }

  private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
    TypeHandler<?> handler = null;
    if (jdbcHandlerMap != null) {
      handler = jdbcHandlerMap.get(jdbcType);
      if (handler == null) {
        handler = jdbcHandlerMap.get(null);
      }
    }
    if (handler == null && type != null && type instanceof Class && Enum.class.isAssignableFrom((Class<?>) type)) {
      handler = new EnumTypeHandler((Class<?>) type);
    }
    @SuppressWarnings("unchecked")
    // type drives generics here
    TypeHandler<T> returned = (TypeHandler<T>) handler;
    return returned;
  }

  public TypeHandler<Object> getUnknownTypeHandler() {
    return UNKNOWN_TYPE_HANDLER;
  }

  public void register(JdbcType jdbcType, TypeHandler<?> handler) {
    JDBC_TYPE_HANDLER_MAP.put(jdbcType, handler);
  }

  //
  // REGISTER INSTANCE
  //

  /**
   * 只配置了typeHandler, 没有配置jdbcType 或者javaType
   */
  @SuppressWarnings("unchecked")
  public <T> void register(TypeHandler<T> typeHandler) {
    boolean mappedTypeFound = false;
    //在自定义typeHandler的时候,可以加上注解MappedTypes 去指定关联的javaType
    //因此,此处需要扫描MappedTypes注解
    MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<?> handledType : mappedTypes.value()) {
        register(handledType, typeHandler);
        mappedTypeFound = true;
      }
    }
    // @since 3.1.0 - try to auto-discover the mapped type
    if (!mappedTypeFound && typeHandler instanceof TypeReference) {
      try {
        TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
        register(typeReference.getRawType(), typeHandler);
        mappedTypeFound = true;
      } catch (Throwable t) {
        // maybe users define the TypeReference with a different type and are not assignable, so just ignore it
      }
    }
    if (!mappedTypeFound) {
      register((Class<T>) null, typeHandler);
    }
  }

  /**
   * 配置了typeHandlerhe和javaType
   */
  public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
    register((Type) javaType, typeHandler);
  }

  private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
    //扫描注解MappedJdbcTypes
    MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypes != null) {
      for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
        register(javaType, handledJdbcType, typeHandler);
      }
      if (mappedJdbcTypes.includeNullJdbcType()) {
        register(javaType, null, typeHandler);
      }
    } else {
      register(javaType, null, typeHandler);
    }
  }

  public <T> void register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler) {
    register(javaTypeReference.getRawType(), handler);
  }

  /**
   * typeHandlerhe、javaType、jdbcType都配置了
   */
  public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) {
    register((Type) type, jdbcType, handler);
  }

  /**
   * 注册typeHandler的核心方法
   * 就是向Map新增数据而已
   */
  private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
      Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
      if (map == null) {
        map = new HashMap<JdbcType, TypeHandler<?>>();
        TYPE_HANDLER_MAP.put(javaType, map);
      }
      map.put(jdbcType, handler);
      if (reversePrimitiveMap.containsKey(javaType)) {
        register(reversePrimitiveMap.get(javaType), jdbcType, handler);
      }
    }
    ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
  }

  //
  // REGISTER CLASS
  //

  // Only handler type

  public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<?> javaTypeClass : mappedTypes.value()) {
        register(javaTypeClass, typeHandlerClass);
        mappedTypeFound = true;
      }
    }
    if (!mappedTypeFound) {
      register(getInstance(null, typeHandlerClass));
    }
  }

  // java type + handler type

  public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
  }

  // java type + jdbc type + handler type

  public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
    register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
  }

  // Construct a handler (used also from Builders)

  @SuppressWarnings("unchecked")
  public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    if (javaTypeClass != null) {
      try {
        Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
        return (TypeHandler<T>) c.newInstance(javaTypeClass);
      } catch (NoSuchMethodException ignored) {
        // ignored
      } catch (Exception e) {
        throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
      }
    }
    try {
      Constructor<?> c = typeHandlerClass.getConstructor();
      return (TypeHandler<T>) c.newInstance();
    } catch (Exception e) {
      throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
    }
  }

 
  /**
   * 根据指定的pacakge去扫描自定义的typeHander,然后注册
   */
  public void register(String packageName) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
    for (Class<?> type : handlerSet) {
      //Ignore inner classes and interfaces (including package-info.java) and abstract classes
      if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
        register(type);
      }
    }
  }
  
  // get information
  
  /**
   * 通过configuration对象可以获取已注册的所有typeHandler
   */
  public Collection<TypeHandler<?>> getTypeHandlers() {
    return Collections.unmodifiableCollection(ALL_TYPE_HANDLERS_MAP.values());
  }
  
}

由源码可以看到, mybatis为我们实现了那么多TypeHandler, 随便打开一个TypeHandler,看其源码,都可以看到,它继承自一个抽象类:BaseTypeHandler, 那么我们是不是也能通过继承BaseTypeHandler,从而实现自定义的TypeHandler ? 答案是肯定的,

演示自定义TypeHandler:

@MappedJdbcTypes(JdbcType.VARCHAR)  
//此处如果不用注解指定jdbcType, 那么,就可以在配置文件中通过"jdbcType"属性指定, 同理, javaType 也可通过 @MappedTypes指定
public class ExampleTypeHandler extends BaseTypeHandler<String> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
    ps.setString(i, parameter);
  }

  @Override
  public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
    return rs.getString(columnName);
  }

  @Override
  public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    return rs.getString(columnIndex);
  }

  @Override
  public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    return cs.getString(columnIndex);
  }
}

然后,就该配置自定义TypeHandler了:

<configuration>
  <typeHandlers>
      <!-- 由于自定义的TypeHandler在定义时已经通过注解指定了jdbcType, 所以此处不用再配置jdbcType -->
      <typeHandler handler="ExampleTypeHandler"/>
  </typeHandlers>
  
  ......
  
</configuration>

也就是说,我们在自定义TypeHandler的时候,可以在TypeHandler通过@MappedJdbcTypes指定jdbcType, 通过 @MappedTypes 指定javaType, 如果没有使用注解指定,那么我们就需要在配置文件中配置。

objectFactory

objectFactory是干什么的? 需要配置吗?

MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。默认情况下,我们不需要配置,mybatis会调用默认实现的objectFactory。 除非我们要自定义ObjectFactory的实现, 那么我们才需要去手动配置。

那么怎么自定义实现ObjectFactory? 怎么配置呢?自定义ObjectFactory只需要去继承DefaultObjectFactory(是ObjectFactory接口的实现类),并重写其方法即可。具体的,本处不多说,后面再具体讲解。

写好了ObjectFactory, 仅需做如下配置:

<configuration>
    ......
    <objectFactory type="org.mybatis.example.ExampleObjectFactory">
        <property name="someProperty" value="100"/>
    </objectFactory>
    ......
</configuration>

objectFactoryElement源码:

/**
 * objectFactory 节点解析
 */
private void objectFactoryElement(XNode context) throws Exception {
    if (context != null) {
      //读取type属性的值, 接下来进行实例化ObjectFactory, 并set进 configuration
      //到此,简单讲一下configuration这个对象,其实它里面主要保存的都是mybatis的配置
      String type = context.getStringAttribute("type");
      //读取propertie的值, 根据需要可以配置, mybatis默认实现的objectFactory没有使用properties
      Properties properties = context.getChildrenAsProperties();
      
      ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
      factory.setProperties(properties);
      configuration.setObjectFactory(factory);
    }
 }

plugins

plugin有何作用? 需要配置吗?

plugins 是一个可选配置。mybatis中的plugin其实就是个interceptor, 它可以拦截Executor 、ParameterHandler 、ResultSetHandler 、StatementHandler 的部分方法,处理我们自己的逻辑。Executor就是真正执行sql语句的东西, ParameterHandler 是处理我们传入参数的,还记得前面讲TypeHandler的时候提到过,mybatis默认帮我们实现了不少的typeHandler, 当我们不显示配置typeHandler的时候,mybatis会根据参数类型自动选择合适的typeHandler执行,其实就是ParameterHandler 在选择。ResultSetHandler 就是处理返回结果的。

怎么自定义plugin ? 怎么配置?要自定义一个plugin, 需要去实现Interceptor接口,这儿不细说,后面实战部分会详细讲解。定义好之后,配置如下:

<configuration>
    ......
    <plugins>
      <plugin interceptor="org.mybatis.example.ExamplePlugin">
        <property name="someProperty" value="100"/>
      </plugin>
    </plugins>
    ......
</configuration>

pluginElement源码:

/**
   * plugins 节点解析
   */
  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        //由此可见,我们在定义一个interceptor的时候,需要去实现Interceptor, 这儿先不具体讲,以后会详细讲解
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

mappers

mappers, 这下引出mybatis的核心之一了,mappers作用 ? 需要配置吗?

mappers 节点下,配置我们的mapper映射文件, 所谓的mapper映射文件,就是让mybatis 用来建立数据表和javabean映射的一个桥梁。在我们实际开发中,通常一个mapper文件对应一个dao接口, 这个mapper可以看做是dao的实现。所以,mappers必须配置。

<configuration>
    ......
    <mappers>
        <!-- 第一种方式:通过resource指定 -->
        <mapper resource="com/dy/dao/userDao.xml"/>
    
        <!-- 第二种方式, 通过class指定接口,进而将接口与对应的xml文件形成映射关系
             不过,使用这种方式必须保证 接口与mapper文件同名(不区分大小写), 
             我这儿接口是UserDao,那么意味着mapper文件为UserDao.xml 
        <mapper class="com.dy.dao.UserDao"/>
        -->
      
        <!-- 第三种方式,直接指定包,自动扫描,与方法二同理 
        <package name="com.dy.dao"/>
        -->
        <!-- 第四种方式:通过url指定mapper文件位置
        <mapper url="file://........"/>
        -->
    </mappers>
    ......
</configuration>

mapperElement源码:

 /**
   * mappers 节点解析
   * 这是mybatis的核心之一
   */
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //如果mappers节点的子节点是package, 那么就扫描package下的文件, 注入进configuration
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          //resource, url, class 三选一
          
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //mapper映射文件都是通过XMLMapperBuilder解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

settings

<settings> 
    <setting name="cacheEnabled" value="true"/> 
    <setting name="lazyLoadingEnabled" value="true"/> 
    <setting name="multipleResultSetsEnabled" value="true"/> 
    <setting name="useColumnLabel" value="true"/> 
    <setting name="useGeneratedKeys" value="false"/> 
    <setting name="enhancementEnabled" value="false"/> 
    <setting name="defaultExecutorType" value="SIMPLE"/> 
    <setting name="defaultStatementTimeout" value="25000"/> 
</settings>

setting节点里配置的值会直接改写Configuration对应的变量值,这些变量描述的是Mybatis的全局运行方式,如果对这些属性的含义不熟悉的话建议不要配置,使用默认值即可。

settingsElement:

private void settingsElement(XNode context) throws Exception {
    if (context != null) {
      Properties props = context.getChildrenAsProperties();
      // Check that all settings are known to the configuration class
      MetaClass metaConfig = MetaClass.forClass(Configuration.class);
      for (Object key : props.keySet()) {
        if (!metaConfig.hasSetter(String.valueOf(key))) {
          throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
        }
      }
      configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
      configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
      configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
      configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
      configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
      configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
      configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
      configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
      configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
      configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
      configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
      configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
      configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
      configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
      configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
      configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
      configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
      configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
      configuration.setLogPrefix(props.getProperty("logPrefix"));
      configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));
      configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
    }
}

在与大语言模型交互的时候,如果模型给出了错误的结论,不要着急否定大模型的能力,我们应当尝试重新构建查询,请求模型在提供它的最终答案之前进行一系列相关的推理。也就是说,如果给模型一个在短时间或用少量文字无法完成的任务,它可能会给出不那么完美甚至是错误的回答。其实这种情况对人类来说也是存在的。一个人解答复杂的数学问题时,如果没有充足时间进行计算,他们通常也不能给出正确的答案。因此,如果模型给出了错误的结论,我们可以指示模型花更多时间进行思考,以便最终得出令人满意的结果。
策略1:指定完成任务所需要的步骤
例:
针对以下由三个反引号包含的寓言,执行以下操作:
1.概括这则寓言的寓意
2.寓意给打工人的启示
3.寓意给学生的启示
```一只乌鸦坐在树上,整天无所事事。一只小兔子看见乌鸦,就问:“我能像你一样整天坐在那里,什么事也不干吗?”乌鸦答道:“当然可以啦,为什么不呢?”于是,兔子便坐在树下开始休息。突然,一只狼出现了,狼跳向兔子……抓住兔子并把它给吃了。```

例:
针对以下由三个反引号包含的寓言,使用以下格式完成<>中的要求:
1.寓意:<概括这则寓言的寓意>
2.相反故事:<编写一则100字左右的寓言表达相反的寓意>
3.翻译:<将寓意翻译成英语>
```一只乌鸦坐在树上,整天无所事事。一只小兔子看见乌鸦,就问:“我能像你一样整天坐在那里,什么事也不干吗?”乌鸦答道:“当然可以啦,为什么不呢?”于是,兔子便坐在树下开始休息。突然,一只狼出现了,狼跳向兔子……抓住兔子并把它给吃了。```

策略2:要求模型先给出自己的解法,然后再下结论
某些情况下,如果直接让模型给出结论,这个匆忙的结果可能是错误的。此时,明确要求模型先思考解决方案,然后再判断结论,通常模型会给出更合理的结果。
例:
以下是一个问题及某人的解答,请判断该人的答案是否正确。
问题:
某人在银行开立一年期定期存款10万元,年息1%。假设用户在前两年末到期时取出当年利息的一半,然后继续定存一年且年息不变,直至存满3年后全部取出,此时用户取出的金额是多少?
解:
第一年本金100000元,到期利息:100000*0.01=1000元,取500元
第二年本金100000+500=100500元,到期利息:100500*0.01=1005元
第三年本金100500+502.5=101002.5元,到期利息:1010.025元
故第三年到期全部取出的金额:101002.5+1010.025=102,012.525元

大模型迅速给出了自己的判断并且给出了它认为错误的理由,只可惜它并没有意识到它自己错了。下面我们使用本策略,让大模型先自己尝试解决问题,再进行判断。
例:
以下是一个问题及某人的解答,请完成以下要求:
1.先分析并解答计算这个问题,请特别注意存款人前两年取出了利息中的一半,而第三年全部取出。
2.尝试解释这个人解答中的式子的含义。
3.判断该人的答案是否正确。

问题:
某人在银行开立一年期定期存款10万元,年息1%。假设用户在前两年末到期时取出当年利息的一半,然后继续定存一年且年息不变,直至存满3年后全部取出,此时用户取出的金额是多少?
解:
第一年本金100000元,到期利息:100000*0.01=1000元,取500元
第二年本金100000+500=100500元,到期利息:100500*0.01=1005元
第三年本金100500+502.5=101002.5元,到期利息:1010.025元
故第三年到期全部取出的金额:101002.5+1010.025=102,012.525元

如果你在阅读这篇文章的时候,同时将上述示例输入大模型进行验证,你将发现你得到的结果也许不太一样。事实上,在不同的模型中,或者对同一模型在不同时间来运行上述示例结果都不太会一样。但是这里的提及的技巧总体而言还是对我们使用语言大模型有帮助的。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: A Simple Image Segmentation Framework via In-Context Examples

创新点


  • 探索了通用的分割模型,发现现有方法在上下文分割中面临任务模糊性的问题,因为并非所有的上下文示例都能准确传达任务信息。
  • 提出了一个利用上下文示例的简单图像分割框架
    SINE

    Segmentation framework via IN-context Examples
    ),利用了一个
    Transformer
    编码-解码结构,其中编码器提供高质量的图像表示,解码器则被设计为生成多个任务特定的输出掩码,以有效消除任务模糊性。
  • SINE
    引入了一个上下文交互模块,以补充上下文信息,并在目标图像与上下文示例之间产生关联,以及一个匹配
    Transformer
    ,使用固定匹配和匈牙利算法消除不同任务之间的差异。
  • 完善了当前的上下文图像分割评估系统,实验结果表明,
    SINE
    可以处理广泛的分割任务,包括少量样本的语义分割、少量样本的实例分割和视频目标分割。

内容概述


图像分割涉及在像素级别上定位和组织概念,比如语义分割、实例分割、全景分割、前景分割和交互分割。然而,现有的大多数分割方法都是针对特定任务量身定做的,无法应用于其他任务。

最近一些工作探索了通用分割模型,通过上下文学习解决多样且无限的分割任务。上下文分割模型需要理解上下文示例传达的任务和内容信息,并在目标图像上分割相关概念,但并不是所有的上下文示例都能准确传达任务信息。例如当提供一个特定个体的照片,是仅限于个体本身、涵盖所有人的实例分割,还是集中于语义分割?模糊的上下文示例可能使传统的上下文分割模型难以清晰地定义不同任务之间的边界,从而导致不期望的输出。

为了解决这个问题,论文提出了基于上下文示例的简单图像分割框架
SINE

Segmentation framework via IN-context Examples
)。受到
SAM
模型的启发,
SINE
预测针对不同复杂度任务定制的多个输出掩码。这些任务包括相同物体、实例到整体语义概念。
SINE
统一了现有的各种粒度的分割任务,旨在实现更广泛的任务泛化。


SegGPT
相比,
SINE
能够在可训练参数更少的情况下有效地解决上下文分割中的任务模糊性问题,而
SegGPT
仅输出语义分割结果。此外,论文进一步将少样本实例分割引入当前的评估系统,以便全面评估这些模型。

SINE


SINE
是一个基于查询的分割模型,遵循
DETR

Mask2Former
的设计。使用相同对象(
ID
)查询
\(\textbf{q}_{id}\)
来识别和定位目标图像中与参考图像中具有相同对应关系的对象,使用可学习的实例查询
\(\textbf{q}_{ins} \in \mathbb{R}^{S \times C}\)
来识别和定位目标图像中与参考图像具有相同语义标签的对象。

SINE
基于经典的
Transformer
结构,引入了一些针对上下文分割任务的有效设计,包括一个冻结的预训练图像编码器、一个上下文交互模块和一个轻量级匹配
Transformer
(
M-Former
) 解码器。

上下文交互

上下文交互的目的是补充上下文信息,并在参考图像特征和目标图像特征之间产生关联。

  • 掩码池化

为每个掩码分配不同的
ID
标签,将参考掩码
\(\textbf{m}_r\)
转换为
ID
掩码
\(\textbf{m}_{id} \in \mathbb{R}^{N \times H \times W}\)
,通过将具有相同类别标签的掩码合并来得到语义掩码
\(\textbf{m}_{sem} \in \mathbb{R}^{M \times H \times W}\)
,其中
\(N\)

\(M\)
分别是
ID
掩码和语义掩码的数量。

然后,使用这些掩码对参考特征
\(\textbf{F}_r\)
进行池化,获得提
ID
标记
\(\textbf{t}_{id} \in \mathbb{R}^{N \times C}\)
和语义标记
\(\textbf{t}_{sem} \in \mathbb{R}^{M \times C}\)

  • 上下文融合模块

上下文融合模块该模块是一个
Transformer
块,包括自注意力机制、交叉注意力机制和前馈网络,实现参考特征和目标特征之间的上下文关联:

\[\begin{equation}
\begin{split}
\left<\textbf{q}_{id}, \textbf{p}_{sem}, \textbf{F}_t^{'}\right> = InContextFusion\left(\textbf{t}_{id}, \textbf{t}_{sem}, \textbf{F}_{t} ;\theta \right),
\end{split}
\end{equation}
\]

这些标记 (
\(\textbf{t}_{id}\)

\(\textbf{t}_{sem}\)
) 和目标特征 (
\(\textbf{F}_{t}\)
) 通过这个共享模块进行融合,在交叉注意力中它们彼此作为键和值,从而可以获得增强后的目标特征
\(\textbf{F}_t^{'}\)

ID
查询
\(\textbf{q}_{id}\)
和语义原型
\(\textbf{p}_{sem}\)

匹配
Transformer

为了有效地进行上下文分割并消除任务模糊性,
M-Former
实现了一个双路径的
Transformer
解码器,共享自注意力层。一路径用于与查询(
\(\textbf{q}_{id}\)

\(\textbf{q}_{ins}\)
)交互,提取与目标图像中的上下文示例相关的特征。第二路径用于增强语义原型
\(\textbf{p}_{sem}\)
以实现更准确的匹配。这两条路径共享自注意力层,以便将语义从
\(\textbf{p}_{sem}\)
分配给
\(\textbf{q}_{ins}\)

M-Former
共有 N 个块,整体的过程如下:

\[\begin{equation}
\begin{split}
\left<\textbf{q}_{id}^l, \textbf{q}_{ins}^l, \textbf{p}_{sem}^l\right> = MFormer_l\left(\textbf{q}_{id}^{l-1}, \textbf{q}_{ins}^{l-1}, \textbf{p}_{sem}^{l-1} ; \theta^l, \textbf{F}_t^{'} \right),
\end{split}
\end{equation}
\]

对于实例分割,使用更新后的语义原型
\(\textbf{p}_{sem}\)
作为分类器,并让
\(\hat{\textbf{y}}_{ins}=\{\hat{y}_{ins}^i\}_{i=1}^S\)
表示
\(S\)
个实例预测的集合。使用匈牙利损失来学习
SINE
,通过计算预测
\(\hat{y}_{ins}^i\)

GT
\(y^j\)
之间的分配成本以解决匹配问题,即
\(-p_i(c^j)+\mathcal{L}_\text{mask}(\hat{m}_{ins}^i,m^j)\)
,其中
\((c^j, m^j)\)

GT
对象的类别和掩码,
\(c^j\)
可能为
\(\varnothing\)

\(p_i(c^j)\)
是第
\(i\)
个实例查询对应类别
\(c^j\)
的概率,
\(\hat{m}_{ins}^i\)
表示其预测的掩码。
\(\mathcal{L}_\text{mask}\)
是一种二元掩码损失和
Dice
损失:

\[\begin{equation}
\begin{split}
\mathcal{L}_{\text{Hungarian}}(\hat{\textbf{y}}_{ins}, \textbf{y}) = \sum\nolimits_{j=1}^S \left[-\log p_{\sigma(j)}(c^j)
+ \mathbb{1}_{c^j\neq\varnothing} \mathcal{L}_{\text{mask}}(\hat{m}^{\sigma(j)}_{ins},m^j) \right],
\end{split}
\end{equation}
\]

其中
\(\sigma(j)\)
表示二分匹配的结果索引。

为了赋予
SINE
预测同一对象的能力,使用图像中同一实例的不同裁剪视图作为参考-目标图像对。设
\(\hat{\textbf{y}}_{id}=\{\hat{y}_{id}^i\}_{i=1}^N\)
表示
\(N\)

ID
预测的集合。

由于参考
ID
和目标
ID
之间的关系是固定的且可以准确确定,可以在预测和
GT
之间执行固定匹配,损失可以写为:

\[\begin{equation}
\begin{split}
\mathcal{L}_{\text{ID}}(\hat{\textbf{y}}_{id}, \textbf{y}) = \sum\nolimits_{i=1}^N \left[-\log p_i(c^i)
+ \mathbb{1}_{c^i\neq\varnothing} \mathcal{L}_{\text{mask}}(\hat{m}^i_{id},m^i) \right],
\end{split}
\end{equation}
\]

其中
\((c^i, m^i)\)

GT
的类别和掩码,
\(c^i \in \{1, \varnothing\}\)

\(c^i=1\)
表示一个对象同时出现在参考图像和目标图像中。总损失为
\(\mathcal{L}=\mathcal{L}_{\text{Hungarian}}+\mathcal{L}_{\text{ID}}\)

一旦训练完成,
SINE
的全部能力在推理过程中得以释放,能够解决上下文示例中的模糊性并为不同的分割任务输出预测。

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.