news 2026/9/10 16:08:15

Detectron2 数据增强系统实战指南:Augmentation / Transform / AugInput 架构、自定义增强与进阶用法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Detectron2 数据增强系统实战指南:Augmentation / Transform / AugInput 架构、自定义增强与进阶用法

Detectron2 数据增强系统实战指南:Augmentation / Transform / AugInput 架构、自定义增强与进阶用法

【免费下载链接】detectron2Detectron2 is a platform for object detection, segmentation and other visual recognition tasks.项目地址: https://gitcode.com/GitHub_Trending/de/detectron2

数据增强(Data Augmentation)是目标检测、实例分割等视觉模型训练中不可或缺的一环。本指南以 Detectron2 官方教程 docs/tutorials/augmentation.md 为核心骨架,深入剖析其增强系统的三大核心抽象 ——Augmentation(策略)、Transform(操作)、AugInput(输入容器),并结合仓库源码(detectron2/data/transforms/augmentation.py、augmentation_impl.py、transform.py)补充底层实现细节。读完本文,你将能够:在自定义数据加载器中组合多类型数据(图像 + 边界框 + 掩码)的增强流水线;编写全新的 Augmentation 与 Transform;掌握 keypoint 水平翻转、变换求逆、新数据类型注册等进阶技巧;并将自定义增强无缝接入默认的DatasetMapper训练管线。

设计目标:为什么 Detectron2 需要这样一套增强系统

在 docs/tutorials/augmentation.md 的开篇,官方明确了增强系统的四个核心目标:

  1. 允许同时增强多种数据类型(例如图像连同其边界框、掩码一起增强);
  2. 允许应用一串静态声明的增强序列(即代码中按顺序组合多个增强);
  3. 允许为增强添加自定义的新数据类型(如旋转边界框 rotated boxes、视频片段等);
  4. 允许对增强所施加的"操作"(operations)进行读取与操控(例如拿到实际生效的翻转操作,手动处理带语义的关键点标注)。

前两个目标覆盖了绝大多数常见用例,这也是 albumentations 等库已经具备的能力;后两个目标则为新的研究场景提供了灵活的扩展点,但也会给 API 带来一定复杂度 —— 这正是本教程要讲清楚的部分。从源码看,这一设计最终落地为三个互相协作的抽象类(详见 detectron2/data/transforms/augmentation.py 的__all__导出列表):

  • Augmentation:定义"策略"(policy),即决定对输入施加何种修改。调用其__call__(AugInput) -> Transform会在原地修改输入,并返回实际施加的操作。
  • Transform:实现真正的"操作"(operation),即执行数据的变换。它提供apply_imageapply_coordsapply_boxapply_segmentationapply_polygons等方法,分别定义如何变换每种数据类型。
  • AugInput:存放Augmentation所需的输入数据及其变换方式。对于常见场景,直接使用此类即可,因为不在AugInput中的额外数据可以用返回的Transform单独变换。

从代码层面看,这套"策略与操作分离"的设计有一个关键收益:Transform是确定性的。正如 augmentation.py 的类文档所强调的,"返回的Transform描述的是确定性变换,因此可以被重新施加到关联数据上"——例如一张图像的几何变换必须与其分割掩码同步进行。这意味着先由Augmentation随机生成一次Transform,再把这个Transform复用到图像、标注、mask 等多个数据上,保证它们变换完全一致。

基本用法:一条静态声明的多数据增强流水线

完整示例与运行机制

官方文档给出的基本用法(对应特性 1 与 2)如下:

from detectron2.data import transforms as T # Define a sequence of augmentations: augs = T.AugmentationList([ T.RandomBrightness(0.9, 1.1), T.RandomFlip(prob=0.5), T.RandomCrop("absolute", (640, 640)) ]) # type: T.Augmentation # Define the augmentation input ("image" required, others optional): input = T.AugInput(image, boxes=boxes, sem_seg=sem_seg) # Apply the augmentation: transform = augs(input) # type: T.Transform image_transformed = input.image # new image sem_seg_transformed = input.sem_seg # new semantic segmentation # For any extra data that needs to be augmented together, use transform, e.g.: image2_transformed = transform.apply_image(image2) polygons_transformed = transform.apply_polygons(polygons)

这里涉及三个基本概念,逐一拆解:

  • Augmentation定义"策略":它的__call__(AugInput) -> Transform方法会原地增强输入,并返回被施加的操作。注意它是"策略"而非"操作"——它只负责根据输入数据决定要做什么,具体怎么做由返回的Transform负责。
  • Transform实现"操作":它提供apply_imageapply_coords等方法来定义每种数据类型的变换方式。示例最后两行正是这一思想的体现:对于不在AugInput里的额外数据(第二张图、多边形标注等),直接用返回的transform去变换。
  • AugInput存储输入:它存放Augmentation需要的输入数据,以及这些数据应当如何被变换。直接使用这个类即可覆盖所有常见场景,因为AugInput之外的额外数据可以通过返回的transform变换(如上例所示)。

底层实现:AugmentationListAugInput.__call__的调用链

从源码看,AugmentationList本质是一个特殊的Augmentation子类(augmentation.py)。它的__init__会把列表中的每一项统一包装成Augmentation(若传入的是Transform,则通过_transform_to_aug包一层_TransformToAug),其__call__按顺序依次施加每个增强并收集所有Transform

def __call__(self, aug_input) -> TransformList: tfms = [] for x in self.augs: tfm = x(aug_input) tfms.append(tfm) return TransformList(tfms)

需要注意,AugmentationListget_transform方法被设计为不可调用(调用会报错),因为要施加第 k 个增强,必须先施加第 k-1 个增强,才能得到第 k 个增强所需的输入。

单次Augmentation.__call__的完整调用链(augmentation.py)是:

  1. _get_aug_input_args通过inspect.signature解析get_transform的参数名。若只有一个参数则默认为"image"(保证向后兼容),否则按参数名从AugInputgetattr取出对应属性;若属性不存在则抛出AttributeError,提示该增强需要什么输入。
  2. 调用self.get_transform(*args)执行策略,生成一个(或一组)Transform
  3. 校验返回值必须是TransformTransformList
  4. 调用aug_input.transform(tfm)原地变换AugInput中的所有属性。

AugInput(即StandardAugInput的别名,见 augmentation.py)标准地提供imageboxessem_seg三个属性,其transform方法对三个属性分别调用apply_imageapply_boxapply_segmentation(augmentation.py)。它还有便捷方法apply_augmentations,等价于AugmentationList(augmentations)(self);模块级函数apply_augmentations/apply_transform_gens则提供了兼容旧版TransformGenAPI 的入口,若传入的是裸 ndarray 会自动包成AugInput(augmentation.py)。

输入约定与数据格式

在自定义增强前,需要了解AugInputget_transform对数据格式的约定(augmentation.py):

  • image(H,W)(H,W,C)的 ndarray,uint8取值[0, 255],或浮点取值[0, 1]/[0, 255]
  • boxes(N,4)的 float32 ndarray,表示 N 个实例边界框,采用XYXY 绝对坐标格式;
  • sem_seg(H,W)的 uint8 ndarray,每个元素是像素的整数类别标签。

同时 augmentation.py 的_check_img_dtype会校验:输入必须是 ndarray,uint8或浮点类型,且维度为 2 或 3。除此之外的类型没有内建约定,detectron2 中也没有使用其他类型的内建Augmentation

编写新的 Augmentation

最简单的写法:只依赖图像

绝大多数二维增强只需要知道输入图像即可。官方文档给出的模板如下:

class MyColorAugmentation(T.Augmentation): def get_transform(self, image): r = np.random.rand(2) return T.ColorTransform(lambda x: x * r[0] + r[1] * 10) class MyCustomResize(T.Augmentation): def get_transform(self, image): old_h, old_w = image.shape[:2] new_h, new_w = int(old_h * np.random.rand()), int(old_w * 1.5) return T.ResizeTransform(old_h, old_w, new_h, new_w) augs = MyCustomResize() transform = augs(input)

这里出现的内建Transform来自 transform.py:

  • ColorTransform:光度变换的通用包装。它只影响颜色空间,不影响坐标空间——因此apply_coords直接原样返回坐标,apply_segmentation原样返回分割,inverse返回NoOpTransform(transform.py)。其子类PILColorTransform则接收一个作用于 PIL Image 的可调用对象。
  • ResizeTransform:图像缩放到目标尺寸。apply_image对 uint8 图像走 PIL 插值,对浮点图像走torch.nn.functional.interpolate(PIL 只支持 uint8);apply_coordsnew/old比例缩放坐标;apply_segmentation使用最近邻插值避免类别标签被污染;inverse()返回一个尺寸互换的ResizeTransform(transform.py)。

另有两个值得注意的内建TransformRotationTransform(cv2 实现,inverse()返回"反向旋转 + 裁剪"的TransformList,若expand=False则无法求逆并抛NotImplementedError)与ExtentTransform(基于 PIL 的子区域提取 + 缩放,transform.py)。

依赖多种输入:利用函数签名自动注入

Augmentation不仅能访问image——只要某个属性存在于给定的AugInput中,且它出现在get_transform的函数签名里,就会被自动注入(这正是前文_get_aug_input_argsinspect.signature解析参数名的原因)。官方示例:

class MyCustomCrop(T.Augmentation): def get_transform(self, image, sem_seg): # decide where to crop using both image and sem_seg return T.CropTransform(...) augs = MyCustomCrop() assert hasattr(input, "image") and hasattr(input, "sem_seg") transform = augs(input)

仓库内建增强中就有这类多输入依赖的实例,可以作为参考:

  • RandomCrop_CategoryAreaConstraint(augmentation_impl.py):get_transform(self, image, sem_seg)会结合语义分割真值寻找裁剪窗口,使得在裁剪后的分割图中没有任何单一类别占比超过single_category_max_area(可缓解语义分割训练中的类别失衡,最多尝试 10 次)。
  • MinIoURandomCrop(augmentation_impl.py):get_transform(self, image, boxes)基于边界框做随机裁剪,要求裁剪块与原图/框的最小 IoU 不低于从min_ious=(0.1, 0.3, 0.5, 0.7, 0.9)中随机选取的阈值,且保证框中心落在裁剪块内。

注意:自定义get_transform若使用*args/**kwargs变长参数,默认的__call__会直接抛出TypeError(augmentation.py)——此时需要自行重写__call__方法。另外,新增的变换操作也可以通过继承T.Transform来实现,T.Transform的基类定义在fvcore.transforms.transform(detectron2/data/transforms/init.py 将其作为基础导出)。

内建 Augmentation 速查表

augmentation_impl.py 提供了大量可直接使用的内建增强,其__all__列表如下:

增强类功能关键参数
RandomFlip按概率水平/垂直翻转prob(默认 0.5)、horizontalvertical(二者不能同时为 True)
Resize缩放到固定尺寸shape(h,w)元组或整数)、interp(PIL 插值方式,默认双线性)
ResizeShortestEdge保持长宽比缩放,短边对齐目标长度short_edge_lengthmax_sizesample_style"range""choice"
ResizeScale随机缩放目标尺寸后等比缩放图像(Google resize_and_crop 风格)min_scalemax_scaletarget_heighttarget_width
RandomResize从给定形状列表中随机选一个缩放shape_list(h,w)列表)
RandomRotation随机角度旋转angleexpandcentersample_style
RandomCrop随机裁剪crop_type"relative"/"relative_range"/"absolute"/"absolute_range")、crop_size
FixedSizeCrop裁剪 + 必要时填充到固定尺寸crop_sizepadpad_value(默认 128)、seg_pad_value(默认 255)
RandomCrop_CategoryAreaConstraint带类别面积约束的随机裁剪RandomCrop+single_category_max_areaignored_category
MinIoURandomCrop带最小 IoU 约束的随机裁剪min_iousmin_crop_sizemode_trialscrop_trials
RandomExtent随机子区域提取(可含图像外区域,填零)scale_rangeshift_range
RandomBrightness/RandomContrast随机亮度/对比度调整intensity_minintensity_max(=1 保持不变)
RandomSaturation随机饱和度调整(RGB)intensity_minintensity_max
RandomLightingAlexNet 式 PCA 光照抖动(RGB)scale(主成分权重标准差)
RandomApply以给定概率包一层地施加另一增强tfm_or_augprob

其中RandomCrop的四种crop_type语义(augmentation_impl.py)值得展开:"relative"裁剪(H*ch, W*cw)区域(比例需在(0,1]);"relative_range"[crop_size[0], 1][crop_size[1], 1]均匀采样比例后再相对裁剪;"absolute"直接裁剪(crop_size[0], crop_size[1])像素区域(不能大于输入);"absolute_range"[crop_size[0], min(H, crop_size[1])]内均匀采样高度、宽度后裁剪。RandomFlip则规定水平、垂直翻转不能同时开启,需用两个RandomFlip实例替代(augmentation_impl.py)。

进阶用法

以下用法由该系统架构天然支持,对标准用例通常无需改动,但对新的研究工作很有价值。

自定义变换策略:以关键点为例

由于Augmentation返回的是"操作"(T.Transform),用户可以据此对数据施加自定义的变换策略。关键点(keypoints)是最典型的例子:它们是(x, y)坐标,但由于携带语义含义(如"左眼""右眼"),变换起来并不简单,这种语义只有用户自己知道,因此用户可以查看返回的transform手动增强关键点。例如图像水平翻转时,需要交换"左眼"和"右眼"的标注(此逻辑默认包含在 detectron2 的默认数据加载器中):

# augs, input are defined as in previous examples transform = augs(input) # type: T.Transform keypoints_xy = transform.apply_coords(keypoints_xy) # transform the coordinates # get a list of all transforms that were applied transforms = T.TransformList([transform]).transforms # check if it is flipped for odd number of times do_hflip = sum(isinstance(t, T.HFlipTransform) for t in transforms) % 2 == 1 if do_hflip: keypoints_xy = keypoints_xy[flip_indices_mapping]

关键点另一个常见需求是"可见性"字段处理:一串增强可能把可见关键点变换到图像边界之外(如裁剪),之后又被其他增强拉回边界内(如填充)。如果希望把越界的关键点标为"不可见",可见性检查必须在每个变换步骤之后进行:

transform = augs(input) # type: T.TransformList assert isinstance(transform, T.TransformList) for t in transform.transforms: keypoints_xy = t.apply_coords(keypoints_xy) visibility &= (keypoints_xy >= [0, 0] & keypoints_xy <= [W, H]).all(axis=1)

(注:detectron2 自带的transform_keypoint_annotations函数选择将此类关键点标为"可见"。)

上述手动逻辑在仓库中的正式实现位于 detectron2/data/detection_utils.py 的transform_keypoint_annotations:先对坐标apply_coords,将越界点标记为visibility=0("unlabeled");再通过sum(isinstance(t, T.HFlipTransform) ...) % 2 == 1判断是否经历了奇数次水平翻转;若翻转,则依据keypoint_hflip_indices交换左右对应的关键点索引(例如 COCO 中"left_eye"与"right_eye"互换),最终按 COCO 约定把visibility==0的点坐标清零。keypoint_hflip_indices由 detection_utils.py 的create_keypoint_hflip_indices根据数据集元数据keypoint_nameskeypoint_flip_map生成,并在DatasetMapper.from_config中自动传入(dataset_mapper.py)。

几何求逆:把预测结果映射回原图

如果图像在推理前经过增强预处理,那么预测结果(如分割掩码)定位在增强后的图像上。若希望把结果映射回原图,可用inverse()API:

transform = augs(input) pred_mask = make_prediction(input.image) inv_transform = transform.inverse() pred_mask_orig = inv_transform.apply_segmentation(pred_mask)

inverse()Transform的可选能力,并非所有变换都支持:如ResizeTransform.inverse()返回尺寸互换的缩放(transform.py);RotationTransform.inverse()返回"反向旋转 + 裁剪"的组合(expand=False时因信息丢失抛NotImplementedError,transform.py);ColorTransform.inverse()返回NoOpTransform(光度变换不可逆,transform.py)。对TransformList求逆时,注意应对列表做反向组合(该逻辑由 fvcore 基类实现)。

注册新的数据类型

T.Transform内建支持图像、坐标、掩码、边界框、多边形等常见类型,并允许注册新数据类型。官方示例是注册旋转框的变换方法:

@T.HFlipTransform.register_type("rotated_boxes") def func(flip_transform: T.HFlipTransform, rotated_boxes: Any): # do the work return flipped_rotated_boxes t = HFlipTransform(width=800) transformed_rotated_boxes = t.apply_rotated_boxes(rotated_boxes) # func will be called

register_type机制在仓库中有真实用例:由于旋转边界框(rotated boxes,格式为(x_center, y_center, width, height, angle_degrees))是一种独立数据类型,transform.py 为其注册了两种内置变换——HFlip_rotated_box(翻转x_center并取反角度)与Resize_rotated_box(按ResizeTransform的缩放因子近似变换中心、宽高与角度),分别通过HFlipTransform.register_type("rotated_box", ...)ResizeTransform.register_type("rotated_box", ...)挂载。测试 tests/data/test_transforms.py 中的test_apply_rotated_boxes等用例即用于验证该机制的坐标变换正确性。注册完成后,用户即可通过transform.apply_rotated_box(boxes)统一调用。

扩展 T.AugInput

增强只能访问给定输入中存在的属性。T.AugInput(即StandardAugInput)定义了imageboxessem_seg三个属性,对常见增强策略已经足够;如果不够,就需要自定义实现。从源码看,自定义AugInput需要满足两个条件(augmentation.py):

  1. 属性访问getattr)的形式提供数据——例如某增强需要imagesem_seg参数,其输入就必须具备同名属性;
  2. 提供一个transform(tfm: Transform) -> None方法,用于原地变换所有属性。

此外,通过重写AugInputtransform()方法,还可以让不同字段以相互依赖的方式被增强(例如基于增强后的掩码后处理边界框)。这类用例不常见,但系统允许这样做。

把自定义增强接入训练管线:DatasetMapper 与配置

本教程聚焦于"编写新数据加载器时如何使用增强"以及"如何编写新增强"。如果你使用 detectron2 的默认数据加载器,它已经支持接收用户提供的自定义增强列表,详见 数据加载教程。

默认的映射器DatasetMapper(detectron2/data/dataset_mapper.py)展示了增强系统的完整落地方式:

  • 构造参数augmentations接收List[Union[T.Augmentation, T.Transform]],在__init__中被包装为T.AugmentationList(augmentations)
  • from_config(cfg, is_train)调用utils.build_augmentation(cfg, is_train)从配置构建默认增强:训练时使用ResizeShortestEdge(cfg.INPUT.MIN_SIZE_TRAIN, cfg.INPUT.MAX_SIZE_TRAIN, sample_style=cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING),若cfg.INPUT.RANDOM_FLIP != "none"则追加RandomFlip(水平/垂直取决于配置值);推理时固定sample_style="choice";当cfg.INPUT.CROP.ENABLED且为训练时,会在列表最前面插入RandomCrop(cfg.INPUT.CROP.TYPE, cfg.INPUT.CROP.SIZE)(detection_utils.py);
  • __call__中读取图像与语义分割真值后构造T.AugInput(image, sem_seg=sem_seg_gt),调用self.augmentations(aug_input)完成增强,再通过transform_instance_annotations(内部调用apply_boxapply_polygons/apply_segmentationtransform_keypoint_annotations,detection_utils.py)与annotations_to_instances把标注转成模型需要的Instances格式。

若想替换默认增强,只需向build_detection_train_loader传入自定义 mapper。来自 数据加载教程 的最小示例:

import detectron2.data.transforms as T from detectron2.data import DatasetMapper # the default mapper dataloader = build_detection_train_loader(cfg, mapper=DatasetMapper(cfg, is_train=True, augmentations=[ T.Resize((800, 800)) ])) # use this dataloader instead of the default

更彻底的自定义 mapper 写法(例如把所有图像缩放到固定尺寸并手动变换标注):

from detectron2.data import detection_utils as utils # Show how to implement a minimal mapper, similar to the default DatasetMapper def mapper(dataset_dict): dataset_dict = copy.deepcopy(dataset_dict) # it will be modified by code below # can use other ways to read image image = utils.read_image(dataset_dict["file_name"], format="BGR") # See "Data Augmentation" tutorial for details usage auginput = T.AugInput(image) transform = T.Resize((800, 800))(auginput) image = torch.from_numpy(auginput.image.transpose(2, 0, 1)) annos = [ utils.transform_instance_annotations(annotation, [transform], image.shape[1:]) for annotation in dataset_dict.pop("annotations") ] return { # create the format that the model expects "image": image, "instances": utils.annotations_to_instances(annos, image.shape[1:]) } dataloader = build_detection_train_loader(cfg, mapper=mapper)

这段代码清晰展示了本教程核心概念的组合方式:AugInput装图像 →Resize增强原地变换并返回Transform→ 用transform配合transform_instance_annotations同步变换边界框、掩码与关键点标注。从源码结构看,这套用法完全沿用了默认DatasetMapper的既有链路,因此自定义增强与默认训练流程的兼容性是有保证的。

小结

Detectron2 的增强系统通过三个职责分明的抽象解决了"多数据类型协同增强、静态增强序列、可扩展数据类型、操作可操控"四个设计目标:

  • Augmentation决定做什么(策略),Transform负责怎么做(操作),AugInput集中管理输入与原地变换
  • 策略与操作分离使得一次随机生成的Transform可以确定性地复用于图像、框、掩码、关键点等所有关联数据;
  • 编写新增强只需继承Augmentation并实现get_transform,参数名即决定了它依赖AugInput中的哪些字段;
  • 关键点翻转语义处理、inverse()求逆、register_type注册新数据类型、自定义AugInput等进阶能力为新的研究方向提供了充分的灵活性;
  • 默认训练管线(DatasetMapper+build_augmentation)已经完整集成了这套系统,用户通过mapper=参数传入自定义增强列表即可无缝替换,相关实现可对照 detectron2/data/dataset_mapper.py、detectron2/data/detection_utils.py 与 detectron2/data/transforms/ 目录深入阅读。

【免费下载链接】detectron2Detectron2 is a platform for object detection, segmentation and other visual recognition tasks.项目地址: https://gitcode.com/GitHub_Trending/de/detectron2

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 16:06:39

基于SocketCAN与InfluxDB的车载TBOX数据采集系统实战

简介&#xff1a;本资源为汽车T-Box数据采集与分析系统的完整工程实现&#xff0c;面向嵌入式开发、车联网及智能网联汽车方向的中高级工程师与高校研究者&#xff0c;聚焦解决车载CAN总线数据实时采集、多协议无线上传、云端接口对接及基础分析建模等核心问题。压缩包共205个文…

作者头像 李华
网站建设 2026/9/10 16:05:27

SSM框架在高校新生报到系统中的应用与优化

1. 项目背景与核心需求新生报到管理系统是高校信息化建设中的关键一环&#xff0c;传统纸质登记方式存在效率低下、数据易丢失、统计困难等问题。基于SSM框架开发的系统能实现以下核心功能&#xff1a;学生信息数字化录入&#xff08;支持批量导入&#xff09;宿舍分配自动化算…

作者头像 李华
网站建设 2026/9/10 16:04:23

多店铺电商销售数据能不能实时看到?3类工具盘点与选型指南(2026)

先说结论&#xff1a;多店铺的电商销售数据&#xff0c;可以实现“准实时”的跨店统一查看&#xff0c;但“实时”的颗粒度取决于数据来源与工具形态。市面上主流的电商多店铺数据工具&#xff0c;按功能定位大致可分为三类——平台原生数据工具“看单店”、ERP/进销存系统“管…

作者头像 李华
网站建设 2026/9/10 16:04:20

2026年电商运营必备的数据工具有哪些?

电商运营的数据工作&#xff0c;早已不只是打开店铺后台看看今天的订单。当一家店铺同时在淘宝、抖音、拼多多经营&#xff0c;广告分散在多个投放渠道&#xff0c;库存、财务又各自成体系时&#xff0c;运营真正需要的是一套能把分散数据整合起来、并回答经营问题的工具组合。…

作者头像 李华
网站建设 2026/9/10 16:00:24

Seaborn数据可视化:从入门到实战

1. 为什么选择Seaborn绘制统计图形&#xff1f;在数据可视化领域&#xff0c;Matplotlib作为Python的基础绘图库已经存在多年&#xff0c;但它的API设计对新手并不友好&#xff0c;需要编写大量代码才能获得专业级的统计图形。这正是Seaborn诞生的背景 - 它构建在Matplotlib之上…

作者头像 李华