Bladeren bron

mv generate_image() to paddlex/tools; yolov3 and maskrcnn support multi-channel input

FlyingQianMM 5 jaren geleden
bovenliggende
commit
e47160982d
58 gewijzigde bestanden met toevoegingen van 2064 en 397 verwijderingen
  1. 1 1
      deploy/cpp/demo/video_classifier.cpp
  2. 1 1
      deploy/cpp/demo/video_detector.cpp
  3. 1 1
      deploy/cpp/demo/video_segmenter.cpp
  4. 28 13
      deploy/cpp/src/transforms.cpp
  5. 28 14
      deploy/openvino/src/transforms.cpp
  6. 0 48
      docs/apis/analysis.md
  7. 18 0
      docs/apis/datasets.md
  8. BIN
      docs/apis/images/detection_analysis.jpg
  9. BIN
      docs/apis/images/insect_bbox-allclass-allarea.png
  10. 1 1
      docs/apis/index.rst
  11. 1 1
      docs/apis/models/classification.md
  12. 27 9
      docs/apis/models/detection.md
  13. 3 2
      docs/apis/models/instance_segmentation.md
  14. 14 3
      docs/apis/transforms/det_transforms.md
  15. 68 0
      docs/apis/visualize.md
  16. 1 0
      docs/examples/index.rst
  17. 99 0
      docs/examples/industrial_quality_inspection/README.md
  18. 97 0
      docs/examples/industrial_quality_inspection/accuracy_improvement.md
  19. 14 0
      docs/examples/industrial_quality_inspection/dataset.md
  20. 116 0
      docs/examples/industrial_quality_inspection/gpu_solution.md
  21. 102 0
      docs/examples/industrial_quality_inspection/tp_fp_list.md
  22. 99 0
      examples/industrial_quality_inspection/README.md
  23. 93 0
      examples/industrial_quality_inspection/accuracy_improvement.md
  24. 56 0
      examples/industrial_quality_inspection/cal_sensitivities_file.py
  25. 126 0
      examples/industrial_quality_inspection/cal_tp_fp.py
  26. 132 0
      examples/industrial_quality_inspection/compare.py
  27. 14 0
      examples/industrial_quality_inspection/dataset.md
  28. 26 0
      examples/industrial_quality_inspection/error_analysis.py
  29. 114 0
      examples/industrial_quality_inspection/gpu_solution.md
  30. BIN
      examples/industrial_quality_inspection/image/after_clahe.png
  31. BIN
      examples/industrial_quality_inspection/image/before_clahe.png
  32. BIN
      examples/industrial_quality_inspection/image/compare_budaodian-116.jpg
  33. BIN
      examples/industrial_quality_inspection/image/image-level_tp_fp.png
  34. BIN
      examples/industrial_quality_inspection/image/visualize_budaodian-116.jpg
  35. 36 0
      examples/industrial_quality_inspection/predict.py
  36. 102 0
      examples/industrial_quality_inspection/tp_fp_list.md
  37. 74 0
      examples/industrial_quality_inspection/train_rcnn.py
  38. 58 0
      examples/industrial_quality_inspection/train_yolov3.py
  39. 10 229
      paddlex/cv/datasets/voc.py
  40. 3 7
      paddlex/cv/models/base.py
  41. 67 8
      paddlex/cv/models/classifier.py
  42. 17 5
      paddlex/cv/models/deeplabv3p.py
  43. 51 15
      paddlex/cv/models/faster_rcnn.py
  44. 22 6
      paddlex/cv/models/mask_rcnn.py
  45. 27 9
      paddlex/cv/models/ppyolo.py
  46. 41 0
      paddlex/cv/models/utils/pretrain_weights.py
  47. 4 1
      paddlex/cv/models/yolo_v3.py
  48. 2 2
      paddlex/cv/nets/detection/bbox_head.py
  49. 8 3
      paddlex/cv/nets/detection/mask_rcnn.py
  50. 4 2
      paddlex/cv/nets/detection/rpn_head.py
  51. 8 3
      paddlex/cv/nets/detection/yolo_v3.py
  52. 3 3
      paddlex/cv/transforms/det_transforms.py
  53. 10 6
      paddlex/cv/transforms/seg_transforms.py
  54. 11 4
      paddlex/deploy.py
  55. 2 0
      paddlex/det.py
  56. 1 0
      paddlex/tools/__init__.py
  57. 15 0
      paddlex/tools/dataset_generate/__init__.py
  58. 208 0
      paddlex/tools/dataset_generate/det.py

+ 1 - 1
deploy/cpp/demo/video_classifier.cpp

@@ -103,7 +103,7 @@ int main(int argc, char** argv) {
     if (FLAGS_use_camera) {
       video_fourcc = 828601953;
     } else {
-      video_fourcc = static_cast<int>(capture.get(CV_CAP_PROP_FOURCC));
+      video_fourcc = CV_FOURCC('M', 'J', 'P', 'G');
     }
 
     if (FLAGS_use_camera) {

+ 1 - 1
deploy/cpp/demo/video_detector.cpp

@@ -104,7 +104,7 @@ int main(int argc, char** argv) {
     if (FLAGS_use_camera) {
       video_fourcc = 828601953;
     } else {
-      video_fourcc = static_cast<int>(capture.get(CV_CAP_PROP_FOURCC));
+      video_fourcc = CV_FOURCC('M', 'J', 'P', 'G');
     }
 
     if (FLAGS_use_camera) {

+ 1 - 1
deploy/cpp/demo/video_segmenter.cpp

@@ -103,7 +103,7 @@ int main(int argc, char** argv) {
     if (FLAGS_use_camera) {
       video_fourcc = 828601953;
     } else {
-      video_fourcc = static_cast<int>(capture.get(CV_CAP_PROP_FOURCC));
+      video_fourcc = CV_FOURCC('M', 'J', 'P', 'G');
     }
 
     if (FLAGS_use_camera) {

+ 28 - 13
deploy/cpp/src/transforms.cpp

@@ -36,9 +36,11 @@ bool Normalize::Run(cv::Mat* im, ImageBlob* data) {
 
   std::vector<cv::Mat> split_im;
   cv::split(*im, split_im);
+  #pragma omp parallel for num_threads(im->channels())
   for (int c = 0; c < im->channels(); c++) {
+    float range_val = max_val_[c] - min_val_[c];
     cv::subtract(split_im[c], cv::Scalar(min_val_[c]), split_im[c]);
-    cv::divide(split_im[c], cv::Scalar(range_val[c]), split_im[c]);
+    cv::divide(split_im[c], cv::Scalar(range_val), split_im[c]);
     cv::subtract(split_im[c], cv::Scalar(mean_[c]), split_im[c]);
     cv::divide(split_im[c], cv::Scalar(std_[c]), split_im[c]);
   }
@@ -116,19 +118,32 @@ bool Padding::Run(cv::Mat* im, ImageBlob* data) {
               << ", but they should be greater than 0." << std::endl;
     return false;
   }
-  std::vector<cv::Mat> padded_im_per_channel;
-  for (size_t i = 0; i < im->channels(); i++) {
-    const cv::Mat per_channel = cv::Mat(im->rows + padding_h,
-                                        im->cols + padding_w,
-                                        CV_32FC1,
-                                        cv::Scalar(im_value_[i]));
-    padded_im_per_channel.push_back(per_channel);
+  if (im->channels() < 4) {
+    cv::copyMakeBorder(
+    *im,
+    *im,
+    0,
+    padding_h,
+    0,
+    padding_w,
+    cv::BORDER_CONSTANT,
+    cv::Scalar(0));
+  } else {
+    std::vector<cv::Mat> padded_im_per_channel(im->channels());
+    #pragma omp parallel for num_threads(im->channels())
+    for (size_t i = 0; i < im->channels(); i++) {
+      const cv::Mat per_channel = cv::Mat(im->rows + padding_h,
+                                          im->cols + padding_w,
+                                          CV_32FC1,
+                                          cv::Scalar(im_value_[i]));
+      padded_im_per_channel[i] = per_channel;
+    }
+    cv::Mat padded_im;
+    cv::merge(padded_im_per_channel, padded_im);
+    cv::Rect im_roi = cv::Rect(0, 0, im->cols, im->rows);
+    im->copyTo(padded_im(im_roi));
+    *im = padded_im;
   }
-  cv::Mat padded_im;
-  cv::merge(padded_im_per_channel, padded_im);
-  cv::Rect im_roi = cv::Rect(0, 0, im->cols, im->rows);
-  im->copyTo(padded_im(im_roi));
-  *im = padded_im;
   data->new_im_size_[0] = im->rows;
   data->new_im_size_[1] = im->cols;
 

+ 28 - 14
deploy/openvino/src/transforms.cpp

@@ -38,9 +38,11 @@ bool Normalize::Run(cv::Mat* im, ImageBlob* data) {
 
   std::vector<cv::Mat> split_im;
   cv::split(*im, split_im);
+  #pragma omp parallel for num_threads(im->channels())
   for (int c = 0; c < im->channels(); c++) {
+    float range_val = max_val_[c] - min_val_[c];
     cv::subtract(split_im[c], cv::Scalar(min_val_[c]), split_im[c]);
-    cv::divide(split_im[c], cv::Scalar(range_val[c]), split_im[c]);
+    cv::divide(split_im[c], cv::Scalar(range_val), split_im[c]);
     cv::subtract(split_im[c], cv::Scalar(mean_[c]), split_im[c]);
     cv::divide(split_im[c], cv::Scalar(std_[c]), split_im[c]);
   }
@@ -120,19 +122,32 @@ bool Padding::Run(cv::Mat* im, ImageBlob* data) {
               << ", but they should be greater than 0." << std::endl;
     return false;
   }
-  std::vector<cv::Mat> padded_im_per_channel;
-  for (size_t i = 0; i < im->channels(); i++) {
-    const cv::Mat per_channel = cv::Mat(im->rows + padding_h,
-                                        im->cols + padding_w,
-                                        CV_32FC1,
-                                        cv::Scalar(im_value_[i]));
-    padded_im_per_channel.push_back(per_channel);
+  if (im->channels() < 4) {
+    cv::copyMakeBorder(
+    *im,
+    *im,
+    0,
+    padding_h,
+    0,
+    padding_w,
+    cv::BORDER_CONSTANT,
+    cv::Scalar(0));
+  } else {
+    std::vector<cv::Mat> padded_im_per_channel(im->channels());
+    #pragma omp parallel for num_threads(im->channels())
+    for (size_t i = 0; i < im->channels(); i++) {
+      const cv::Mat per_channel = cv::Mat(im->rows + padding_h,
+                                          im->cols + padding_w,
+                                          CV_32FC1,
+                                          cv::Scalar(im_value_[i]));
+      padded_im_per_channel[i] = per_channel;
+    }
+    cv::Mat padded_im;
+    cv::merge(padded_im_per_channel, padded_im);
+    cv::Rect im_roi = cv::Rect(0, 0, im->cols, im->rows);
+    im->copyTo(padded_im(im_roi));
+    *im = padded_im;
   }
-  cv::Mat padded_im;
-  cv::merge(padded_im_per_channel, padded_im);
-  cv::Rect im_roi = cv::Rect(0, 0, im->cols, im->rows);
-  im->copyTo(padded_im(im_roi));
-  *im = padded_im;
   data->new_im_size_[0] = im->rows;
   data->new_im_size_[1] = im->cols;
 
@@ -219,7 +234,6 @@ void Transforms::Init(
     if (name == "ArrangeYOLOv3") {
       continue;
     }
-    std::cout << "trans name: " << name << std::endl;
     std::shared_ptr<Transform> transform = CreateTransform(name);
     transform->Init(item.begin()->second);
     transforms_.push_back(transform);

+ 0 - 48
docs/apis/analysis.md

@@ -1,48 +0,0 @@
-# 数据集分析
-
-## paddlex.datasets.analysis.Seg
-```python
-paddlex.datasets.analysis.Seg(data_dir, file_list, label_list)
-```
-
-构建统计分析语义分类数据集的分析器。
-
-> **参数**
-> > * **data_dir** (str): 数据集所在的目录路径。  
-> > * **file_list** (str): 描述数据集图片文件和类别id的文件路径(文本内每行路径为相对`data_dir`的相对路径)。  
-> > * **label_list** (str): 描述数据集包含的类别信息文件路径。  
-
-### analysis
-```python
-analysis(self)
-```
-
-Seg分析器的分析接口,完成以下信息的分析统计:
-
-> * 图像数量
-> * 图像最大和最小的尺寸
-> * 图像通道数量
-> * 图像各通道的最小值和最大值
-> * 图像各通道的像素值分布
-> * 图像各通道归一化后的均值和方差
-> * 标注图中各类别的数量及比重
-
-[代码示例](https://github.com/PaddlePaddle/PaddleX/blob/develop/examples/multi-channel_remote_sensing/tools/analysis.py)
-
-[统计信息示例](../../examples/multi-channel_remote_sensing/analysis.html#id2)
-
-### cal_clipped_mean_std
-```python
-cal_clipped_mean_std(self, clip_min_value, clip_max_value, data_info_file)
-```
-
-Seg分析器用于计算图像截断后的均值和方差的接口。
-
-> **参数**
-> > * **clip_min_value** (list):  截断的下限,小于min_val的数值均设为min_val。
-> > * **clip_max_value** (list): 截断的上限,大于max_val的数值均设为max_val。
-> > * **data_info_file** (str): 在analysis()接口中保存的分析结果文件(名为`train_information.pkl`)的路径。
-
-[代码示例](https://github.com/PaddlePaddle/PaddleX/blob/develop/examples/multi-channel_remote_sensing/tools/cal_clipped_mean_std.py)
-
-[计算结果示例](../../examples/multi-channel_remote_sensing/analysis.html#id4)

+ 18 - 0
docs/apis/datasets.md

@@ -41,6 +41,15 @@ paddlex.datasets.VOCDetection(data_dir, file_list, label_list, transforms=None,
 > > * **parallel_method** (str): 数据集中样本在预处理过程中并行处理的方式,支持'thread'线程和'process'进程两种方式。默认为'process'(Windows和Mac下会强制使用thread,该参数无效)。  
 > > * **shuffle** (bool): 是否需要对数据集中样本打乱顺序。默认为False。  
 
+### add_negative_samples(self, image_dir)
+
+> **将背景图片加入训练**
+
+> > * **image_dir** (str): 背景图片所在的文件夹目录。
+
+> 示例:[代码文件](https://github.com/PaddlePaddle/PaddleX/tree/develop/examples/industrial_quality_inspection/train_rcnn.py#L45)
+
+
 ## paddlex.datasets.CocoDetection
 > **用于实例分割/目标检测模型**  
 ```
@@ -61,6 +70,15 @@ paddlex.datasets.CocoDetection(data_dir, ann_file, transforms=None, num_workers=
 > > * **parallel_method** (str): 数据集中样本在预处理过程中并行处理的方式,支持'thread'线程和'process'进程两种方式。默认为'process'(Windows和Mac下会强制使用thread,该参数无效)。  
 > > * **shuffle** (bool): 是否需要对数据集中样本打乱顺序。默认为False。  
 
+### add_negative_samples(self, image_dir)
+
+> **将背景图片加入训练**
+
+> > * **image_dir** (str): 背景图片所在的文件夹目录。
+
+> 示例:[代码文件](https://github.com/PaddlePaddle/PaddleX/tree/develop/examples/industrial_quality_inspection/train_rcnn.py#L45)
+
+
 ## paddlex.datasets.SegDataset
 > **用于语义分割模型**  
 ```

BIN
docs/apis/images/detection_analysis.jpg


BIN
docs/apis/images/insect_bbox-allclass-allarea.png


+ 1 - 1
docs/apis/index.rst

@@ -6,7 +6,7 @@ API接口说明
 
    transforms/index.rst
    datasets.md
-   analysis.md
+   tools.md
    models/index.rst
    slim.md
    visualize.md

+ 1 - 1
docs/apis/models/classification.md

@@ -27,7 +27,7 @@ train(self, num_epochs, train_dataset, train_batch_size=64, eval_dataset=None, s
 > > - **save_interval_epochs** (int): 模型保存间隔(单位:迭代轮数)。默认为1。
 > > - **log_interval_steps** (int): 训练日志输出间隔(单位:迭代步数)。默认为2。
 > > - **save_dir** (str): 模型保存路径。
-> > - **pretrain_weights** (str): 若指定为路径时,则加载路径下预训练模型;若为字符串'IMAGENET',则自动下载在ImageNet图片数据上预训练的模型权重;若为None,则不使用预训练模型。默认为'IMAGENET'。
+> > - **pretrain_weights** (str): 若指定为路径时,则加载路径下预训练模型;若为字符串'IMAGENET',则自动下载在ImageNet图片数据上预训练的模型权重;若为None,则不使用预训练模型。默认为'IMAGENET'。若模型为'ResNet50_vd',则默认下载百度自研10万类预训练模型,即默认为'BAIDU10W'。
 > > - **optimizer** (paddle.fluid.optimizer): 优化器。当该参数为None时,使用默认优化器:fluid.layers.piecewise_decay衰减策略,fluid.optimizer.Momentum优化方法。
 > > - **learning_rate** (float): 默认优化器的初始学习率。默认为0.025。
 > > - **warmup_steps** (int): 默认优化器的warmup步数,学习率将在设定的步数内,从warmup_start_lr线性增长至设定的learning_rate,默认为0。

+ 27 - 9
docs/apis/models/detection.md

@@ -3,7 +3,7 @@
 ## paddlex.det.PPYOLO
 
 ```python
-paddlex.det.PPYOLO(num_classes=80, backbone='ResNet50_vd_ssld', with_dcn_v2=True, anchors=None, anchor_masks=None, use_coord_conv=True, use_iou_aware=True, use_spp=True, use_drop_block=True, scale_x_y=1.05, ignore_threshold=0.7, label_smooth=False, use_iou_loss=True, use_matrix_nms=True, nms_score_threshold=0.01, nms_topk=1000, nms_keep_topk=100, nms_iou_threshold=0.45, train_random_shapes=[320, 352, 384, 416, 448, 480, 512, 544, 576, 608])
+paddlex.det.PPYOLO(num_classes=80, backbone='ResNet50_vd_ssld', with_dcn_v2=True, anchors=None, anchor_masks=None, use_coord_conv=True, use_iou_aware=True, use_spp=True, use_drop_block=True, scale_x_y=1.05, ignore_threshold=0.7, label_smooth=False, use_iou_loss=True, use_matrix_nms=True, nms_score_threshold=0.01, nms_topk=1000, nms_keep_topk=100, nms_iou_threshold=0.45, train_random_shapes=[320, 352, 384, 416, 448, 480, 512, 544, 576, 608], input_channel=3)
 ```
 
 > 构建PPYOLO检测器。**注意在PPYOLO,num_classes不需要包含背景类,如目标包括human、dog两种,则num_classes设为2即可,这里与FasterRCNN/MaskRCNN有差别**
@@ -32,6 +32,7 @@ paddlex.det.PPYOLO(num_classes=80, backbone='ResNet50_vd_ssld', with_dcn_v2=True
 > > - **nms_iou_threshold** (float): 进行NMS时,用于剔除检测框IOU的阈值。默认为0.45。
 > > - **label_smooth** (bool): 是否使用label smooth。默认值为False。
 > > - **train_random_shapes** (list|tuple): 训练时从列表中随机选择图像大小。默认值为[320, 352, 384, 416, 448, 480, 512, 544, 576, 608]。
+> > - **input_channel** (int): 输入图像的通道数量。默认为3。
 
 ### train
 
@@ -85,7 +86,7 @@ evaluate(self, eval_dataset, batch_size=1, epoch_id=None, metric=None, return_de
 > >
 >  **返回值**
 >
-> > - **tuple** (metrics, eval_details) | **dict** (metrics): 当`return_details`为True时,返回(metrics, eval_details),当`return_details`为False时,返回metrics。metrics为dict,包含关键字:'bbox_mmap'或者’bbox_map‘,分别表示平均准确率平均值在各个阈值下的结果取平均值的结果(mmAP)、平均准确率平均值(mAP)。eval_details为dict,包含关键字:'bbox',对应元素预测结果列表,每个预测结果由图像id、预测框类别id、预测框坐标、预测框得分;’gt‘:真实标注框相关信息。
+> > - **tuple** (metrics, eval_details) | **dict** (metrics): 当`return_details`为True时,返回(metrics, eval_details),当`return_details`为False时,返回metrics。metrics为dict,包含关键字:'bbox_mmap'或者’bbox_map‘,分别表示平均准确率平均值在各个阈值下的结果取平均值的结果(mmAP)、平均准确率平均值(mAP)。eval_details为dict,包含bbox和gt两个关键字。其中关键字bbox的键值是一个列表,列表中每个元素代表一个预测结果,一个预测结果是一个由图像id,预测框类别id, 预测框坐标,预测框得分组成的列表。而关键字gt的键值是真实标注框的相关信息。
 
 ### predict
 
@@ -93,7 +94,7 @@ evaluate(self, eval_dataset, batch_size=1, epoch_id=None, metric=None, return_de
 predict(self, img_file, transforms=None)
 ```
 
-> PPYOLO模型预测接口。需要注意的是,只有在训练过程中定义了eval_dataset,模型在保存时才会将预测时的图像处理流程保存在`YOLOv3.test_transforms`和`YOLOv3.eval_transforms`中。如未在训练时定义eval_dataset,那在调用预测`predict`接口时,用户需要再重新定义`test_transforms`传入给`predict`接口
+> PPYOLO模型预测接口。需要注意的是,只有在训练过程中定义了eval_dataset,模型在保存时才会将预测时的图像处理流程保存在`PPYOLO.test_transforms`和`PPYOLO.eval_transforms`中。如未在训练时定义eval_dataset,那在调用预测`predict`接口时,用户需要再重新定义`test_transforms`传入给`predict`接口
 
 > **参数**
 >
@@ -111,7 +112,7 @@ predict(self, img_file, transforms=None)
 batch_predict(self, img_file_list, transforms=None)
 ```
 
-> PPYOLO模型批量预测接口。需要注意的是,只有在训练过程中定义了eval_dataset,模型在保存时才会将预测时的图像处理流程保存在`YOLOv3.test_transforms`和`YOLOv3.eval_transforms`中。如未在训练时定义eval_dataset,那在调用预测`batch_predict`接口时,用户需要再重新定义`test_transforms`传入给`batch_predict`接口
+> PPYOLO模型批量预测接口。需要注意的是,只有在训练过程中定义了eval_dataset,模型在保存时才会将预测时的图像处理流程保存在`PPYOLO.test_transforms`和`PPYOLO.eval_transforms`中。如未在训练时定义eval_dataset,那在调用预测`batch_predict`接口时,用户需要再重新定义`test_transforms`传入给`batch_predict`接口
 
 > **参数**
 >
@@ -126,7 +127,7 @@ batch_predict(self, img_file_list, transforms=None)
 ## paddlex.det.YOLOv3
 
 ```python
-paddlex.det.YOLOv3(num_classes=80, backbone='MobileNetV1', anchors=None, anchor_masks=None, ignore_threshold=0.7, nms_score_threshold=0.01, nms_topk=1000, nms_keep_topk=100, nms_iou_threshold=0.45, label_smooth=False, train_random_shapes=[320, 352, 384, 416, 448, 480, 512, 544, 576, 608])
+paddlex.det.YOLOv3(num_classes=80, backbone='MobileNetV1', anchors=None, anchor_masks=None, ignore_threshold=0.7, nms_score_threshold=0.01, nms_topk=1000, nms_keep_topk=100, nms_iou_threshold=0.45, label_smooth=False, train_random_shapes=[320, 352, 384, 416, 448, 480, 512, 544, 576, 608], input_channel=3)
 ```
 
 > 构建YOLOv3检测器。**注意在YOLOv3,num_classes不需要包含背景类,如目标包括human、dog两种,则num_classes设为2即可,这里与FasterRCNN/MaskRCNN有差别**
@@ -147,6 +148,7 @@ paddlex.det.YOLOv3(num_classes=80, backbone='MobileNetV1', anchors=None, anchor_
 > > - **nms_iou_threshold** (float): 进行NMS时,用于剔除检测框IoU的阈值。默认为0.45。
 > > - **label_smooth** (bool): 是否使用label smooth。默认值为False。
 > > - **train_random_shapes** (list|tuple): 训练时从列表中随机选择图像大小。默认值为[320, 352, 384, 416, 448, 480, 512, 544, 576, 608]。
+> > - **input_channel** (int): 输入图像的通道数量。默认为3。
 
 ### train
 
@@ -198,7 +200,7 @@ evaluate(self, eval_dataset, batch_size=1, epoch_id=None, metric=None, return_de
 > >
 >  **返回值**
 >
-> > - **tuple** (metrics, eval_details) | **dict** (metrics): 当`return_details`为True时,返回(metrics, eval_details),当`return_details`为False时,返回metrics。metrics为dict,包含关键字:'bbox_mmap'或者’bbox_map‘,分别表示平均准确率平均值在各个阈值下的结果取平均值的结果(mmAP)、平均准确率平均值(mAP)。eval_details为dict,包含关键字:'bbox',对应元素预测结果列表,每个预测结果由图像id、预测框类别id、预测框坐标、预测框得分;’gt‘:真实标注框相关信息。
+> > - **tuple** (metrics, eval_details) | **dict** (metrics): 当`return_details`为True时,返回(metrics, eval_details),当`return_details`为False时,返回metrics。metrics为dict,包含关键字:'bbox_mmap'或者’bbox_map‘,分别表示平均准确率平均值在各个阈值下的结果取平均值的结果(mmAP)、平均准确率平均值(mAP)。eval_details为dict,包含bbox和gt两个关键字。其中关键字bbox的键值是一个列表,列表中每个元素代表一个预测结果,一个预测结果是一个由图像id,预测框类别id, 预测框坐标,预测框得分组成的列表。而关键字gt的键值是真实标注框的相关信息。
 
 ### predict
 
@@ -240,8 +242,7 @@ batch_predict(self, img_file_list, transforms=None)
 ## paddlex.det.FasterRCNN
 
 ```python
-paddlex.det.FasterRCNN(num_classes=81, backbone='ResNet50', with_fpn=True, aspect_ratios=[0.5, 1.0, 2.0], anchor_sizes=[32, 64, 128, 256, 512])
-
+paddlex.det.FasterRCNN(num_classes=81, backbone='ResNet50', with_fpn=True, aspect_ratios=[0.5, 1.0, 2.0], anchor_sizes=[32, 64, 128, 256, 512], with_dcn=False, rpn_cls_loss='SigmoidCrossEntropy', rpn_focal_loss_alpha=0.25, rpn_focal_loss_gamma=2, rcnn_bbox_loss='SmoothL1Loss', rcnn_nms='MultiClassNMS', keep_top_k=100, nms_threshold=0.5, score_threshold=0.05, softnms_sigma=0.5, bbox_assigner='BBoxAssigner', fpn_num_channels=256, input_channel=3, rpn_batch_size_per_im=256, rpn_fg_fraction=0.5, test_pre_nms_top_n=None, test_post_nms_top_n=1000)
 ```
 
 > 构建FasterRCNN检测器。 **注意在FasterRCNN中,num_classes需要设置为类别数+背景类,如目标包括human、dog两种,则num_classes需设为3,多的一种为背景background类别**
@@ -253,6 +254,23 @@ paddlex.det.FasterRCNN(num_classes=81, backbone='ResNet50', with_fpn=True, aspec
 > > - **with_fpn** (bool): 是否使用FPN结构。默认为True。
 > > - **aspect_ratios** (list): 生成anchor高宽比的可选值。默认为[0.5, 1.0, 2.0]。
 > > - **anchor_sizes** (list): 生成anchor大小的可选值。默认为[32, 64, 128, 256, 512]。
+> > - **with_dcn** (bool): backbone网络中是否使用deformable convolution network v2。默认为False。
+> > - **rpn_cls_loss** (str): RPN部分的分类损失函数,取值范围为['SigmoidCrossEntropy', 'SigmoidFocalLoss']。当遇到模型误检了很多背景区域时,可以考虑使用'SigmoidFocalLoss',并调整适合的`rpn_focal_loss_alpha`和`rpn_focal_loss_gamma`。默认为'SigmoidCrossEntropy'。
+> > - **rpn_focal_loss_alpha** (float):当RPN的分类损失函数设置为'SigmoidFocalLoss'时,用于调整正样本和负样本的比例因子,默认为0.25。当PN的分类损失函数设置为'SigmoidCrossEntropy'时,`rpn_focal_loss_alpha`的设置不生效。
+> > - **rpn_focal_loss_gamma** (float): 当RPN的分类损失函数设置为'SigmoidFocalLoss'时,用于调整易分样本和难分样本的比例因子,默认为2。当RPN的分类损失函数设置为'SigmoidCrossEntropy'时,`rpn_focal_loss_gamma`的设置不生效。
+> > - **rcnn_bbox_loss** (str): RCNN部分的位置回归损失函数,取值范围为['SmoothL1Loss', 'CIoULoss']。默认为'SmoothL1Loss'。
+> > - **rcnn_nms** (str): RCNN部分的非极大值抑制的计算方法,取值范围为['MultiClassNMS', 'MultiClassSoftNMS','MultiClassCiouNMS']。默认为'MultiClassNMS'。当选择'MultiClassNMS'时,可以将`keep_top_k`设置成100、`nms_threshold`设置成0.5、`score_threshold`设置成0.05。当选择'MultiClassSoftNMS'时,可以将`keep_top_k`设置为300、`score_threshold`设置为0.01、`softnms_sigma`设置为0.5。当选择'MultiClassCiouNMS'时,可以将`keep_top_k`设置为100、`score_threshold`设置成0.05、`nms_threshold`设置成0.5。
+> > - **keep_top_k** (int): RCNN部分在进行非极大值抑制计算后,每张图像保留最多保存`keep_top_k`个检测框。默认为100。
+> > - **nms_threshold** (float): RCNN部分在进行非极大值抑制时,用于剔除检测框所需的IoU阈值。当`rcnn_nms`设置为`MultiClassSoftNMS`时,`nms_threshold`的设置不生效。默认为0.5。
+> > - **score_threshold** (float): RCNN部分在进行非极大值抑制前,用于过滤掉低置信度边界框所需的置信度阈值。默认为0.05。
+> > - **softnms_sigma** (float): 当`rcnn_nms`设置为`MultiClassSoftNMS`时,用于调整被抑制的检测框的置信度,调整公式为`score = score * weights, weights = exp(-(iou * iou) / softnms_sigma)`。默认设为0.5。
+> > - **bbox_assigner** (str): 训练阶段,RCNN部分生成正负样本的采样方式。可选范围为['BBoxAssigner', 'LibraBBoxAssigner']。当目标物体的区域只占原始图像的一小部分时,可以考虑采用[LibraRCNN](https://arxiv.org/abs/1904.02701)中提出的IoU-balanced Sampling采样方式来获取更多的难分负样本,设置为'LibraBBoxAssigner'即可。默认为'BBoxAssigner'。
+> > - **fpn_num_channels** (int): FPN部分特征层的通道数量。默认为256。
+> > - **input_channel** (int): 输入图像的通道数量。默认为3。
+> > - **rpn_batch_size_per_im** (int): 训练阶段,RPN部分每张图片的正负样本的数量总和。默认为256。
+> > - **rpn_fg_fraction** (float): 训练阶段,RPN部分每张图片的正负样本数量总和中正样本的占比。默认为0.5。
+> > - **test_pre_nms_top_n** (int):预测阶段,RPN部分做非极大值抑制计算的候选框的数量。若设置为None, 有FPN结构的话,`test_pre_nms_top_n`会被设置成6000, 无FPN结构的话,`test_pre_nms_top_n`会被设置成1000。默认为None。
+> > - **test_post_nms_top_n** (int): 预测阶段,RPN部分做完非极大值抑制后保留的候选框的数量。默认为1000。
 
 ### train
 
@@ -302,7 +320,7 @@ evaluate(self, eval_dataset, batch_size=1, epoch_id=None, metric=None, return_de
 > >
 > **返回值**
 >
-> > - **tuple** (metrics, eval_details) | **dict** (metrics): 当`return_details`为True时,返回(metrics, eval_details),当`return_details`为False时,返回metrics。metrics为dict,包含关键字:'bbox_mmap'或者’bbox_map‘,分别表示平均准确率平均值在各个IoU阈值下的结果取平均值的结果(mmAP)、平均准确率平均值(mAP)。eval_details为dict,包含关键字:'bbox',对应元素预测结果列表,每个预测结果由图像id、预测框类别id、预测框坐标、预测框得分;’gt‘:真实标注框相关信息。
+> > - **tuple** (metrics, eval_details) | **dict** (metrics): 当`return_details`为True时,返回(metrics, eval_details),当`return_details`为False时,返回metrics。metrics为dict,包含关键字:'bbox_mmap'或者’bbox_map‘,分别表示平均准确率平均值在各个IoU阈值下的结果取平均值的结果(mmAP)、平均准确率平均值(mAP)。eval_details为dict,包含bbox和gt两个关键字。其中关键字bbox的键值是一个列表,列表中每个元素代表一个预测结果,一个预测结果是一个由图像id,预测框类别id, 预测框坐标,预测框得分组成的列表。而关键字gt的键值是真实标注框的相关信息。
 
 ### predict
 

+ 3 - 2
docs/apis/models/instance_segmentation.md

@@ -3,7 +3,7 @@
 ## MaskRCNN
 
 ```python
-paddlex.det.MaskRCNN(num_classes=81, backbone='ResNet50', with_fpn=True, aspect_ratios=[0.5, 1.0, 2.0], anchor_sizes=[32, 64, 128, 256, 512])
+paddlex.det.MaskRCNN(num_classes=81, backbone='ResNet50', with_fpn=True, aspect_ratios=[0.5, 1.0, 2.0], anchor_sizes=[32, 64, 128, 256, 512], input_channel=3)
 
 ```
 
@@ -16,6 +16,7 @@ paddlex.det.MaskRCNN(num_classes=81, backbone='ResNet50', with_fpn=True, aspect_
 > > - **with_fpn** (bool): 是否使用FPN结构。默认为True。
 > > - **aspect_ratios** (list): 生成anchor高宽比的可选值。默认为[0.5, 1.0, 2.0]。
 > > - **anchor_sizes** (list): 生成anchor大小的可选值。默认为[32, 64, 128, 256, 512]。
+> > - **input_channel** (int): 输入图像的通道数量。默认为3。
 
 #### train
 
@@ -65,7 +66,7 @@ evaluate(self, eval_dataset, batch_size=1, epoch_id=None, metric=None, return_de
 > >
 > **返回值**
 >
-> > - **tuple** (metrics, eval_details) | **dict** (metrics): 当`return_details`为True时,返回(metrics, eval_details),当return_details为False时,返回metrics。metrics为dict,包含关键字:'bbox_mmap'和'segm_mmap'或者’bbox_map‘和'segm_map',分别表示预测框和分割区域平均准确率平均值在各个IoU阈值下的结果取平均值的结果(mmAP)、平均准确率平均值(mAP)。eval_details为dict,包含关键字:'bbox',对应元素预测框结果列表,每个预测结果由图像id、预测框类别id、预测框坐标、预测框得分;'mask',对应元素预测区域结果列表,每个预测结果由图像id、预测区域类别id、预测区域坐标、预测区域得分;’gt‘:真实标注框和标注区域相关信息。
+> > - **tuple** (metrics, eval_details) | **dict** (metrics): 当`return_details`为True时,返回(metrics, eval_details),当return_details为False时,返回metrics。metrics为dict,包含关键字:'bbox_mmap'和'segm_mmap'或者’bbox_map‘和'segm_map',分别表示预测框和分割区域平均准确率平均值在各个IoU阈值下的结果取平均值的结果(mmAP)、平均准确率平均值(mAP)。eval_details为dict,包含`bbox`、`mask`和`gt`三个关键字。其中关键字`bbox`的键值是一个列表,列表中每个元素代表一个预测结果,一个预测结果是一个由图像id,预测框类别id, 预测框坐标,预测框得分组成的列表。关键字`mask`的键值是一个列表,列表中每个元素代表各预测框内物体的分割结果,分割结果由图像id、预测框类别id、表示预测框内各像素点是否属于物体的二值图、预测框得分。而关键字gt的键值是真实标注框的相关信息。
 
 #### predict
 

+ 14 - 3
docs/apis/transforms/det_transforms.md

@@ -32,13 +32,13 @@ paddlex.det.transforms.ResizeByShort(short_size=800, max_size=1333)
 
 根据图像的短边调整图像大小(resize)。  
 1. 获取图像的长边和短边长度。  
-2. 根据短边与short_size的比例,计算长边的目标长度,此时高、宽的resize比例为short_size/原图短边长度。  
+2. 根据短边与short_size的比例,计算长边的目标长度,此时高、宽的resize比例为short_size/原图短边长度。若short_size为数组,则随机从该数组中挑选一个数值作为short_size。
 3. 如果max_size>0,调整resize比例:
    如果长边的目标长度>max_size,则高、宽的resize比例为max_size/原图长边长度。
 4. 根据调整大小的比例对图像进行resize。
 
 ### 参数
-* **short_size** (int): 短边目标长度。默认为800。
+* **short_size** (int|list): 短边目标长度。默认为800。当需要做多尺度训练时,可以将`short_size`设置成数组,例如[500, 600, 700, 800]。
 * **max_size** (int): 长边目标长度的最大限制。默认为1333。
 
 ## Padding
@@ -122,7 +122,7 @@ paddlex.det.transforms.MixupImage(alpha=1.5, beta=1.5, mixup_epoch=-1)
 * **beta** (float): 随机beta分布的上限。默认为1.5。
 * **mixup_epoch** (int): 在前mixup_epoch轮使用mixup增强操作;当该参数为-1时,该策略不会生效。默认为-1。
 
-## RandomExpand
+## RandomExpand
 ```python
 paddlex.det.transforms.RandomExpand(ratio=4., prob=0.5, fill_value=[123.675, 116.28, 103.53])
 ```
@@ -168,6 +168,17 @@ paddlex.det.transforms.RandomCrop(aspect_ratio=[.5, 2.], thresholds=[.0, .1, .3,
 * **allow_no_crop** (bool): 是否允许未进行裁剪。默认值为True。
 * **cover_all_box** (bool): 是否要求所有的真实标注框都必须在裁剪区域内。默认值为False。
 
+## CLAHE
+```
+paddlex.det.transforms.CLAHE(clip_limit=2., tile_grid_size=(8, 8))
+```
+对图像进行对比度增强。
+
+### 参数
+
+* **clip_limit** (int|float): 颜色对比度的阈值,默认值为2.。
+* **tile_grid_size** (list|tuple): 进行像素均衡化的网格大小。默认值为(8, 8)。
+
 <!--
 ## ComposedRCNNTransforms
 ```python

+ 68 - 0
docs/apis/visualize.md

@@ -139,3 +139,71 @@ paddlex.transforms.visualize(dataset,
 >* **dataset** (paddlex.datasets): 数据集读取器。
 >* **img_count** (int): 需要进行数据预处理/增强的图像数目。默认为3。
 >* **save_dir** (str): 日志保存的路径。默认为'vdl_output'。
+
+## paddlex.det.coco_error_analysis
+> **分析模型预测错误的原因**
+
+```
+paddlex.det.coco_error_analysis(eval_details_file=None, gt=None, pred_bbox=None, pred_mask=None, save_dir='./output')
+```
+逐个分析模型预测错误的原因,并将分析结果以图表的形式展示。分析结果图表示例如下:
+
+![](images/detection_analysis.jpg)
+
+左图显示的是`person`类的分析结果,有图显示的是所有类别整体的分析结果。
+
+分析图表展示了7条Precision-Recall(PR)曲线,每一条曲线表示的Average Precision (AP)比它左边那条高,原因是逐步放宽了评估要求。以`person`类为例,各条PR曲线的评估要求解释如下:
+
+* C75: 在IoU设置为0.75时的PR曲线, AP为0.510。
+* C50: 在IoU设置为0.5时的PR曲线,AP为0.724。C50与C75之间的白色区域面积代表将IoU从0.75放宽至0.5带来的AP增益。
+* Loc: 在IoU设置为0.1时的PR曲线,AP为0.832。Loc与C50之间的蓝色区域面积代表将IoU从0.5放宽至0.1带来的AP增益。蓝色区域面积越大,表示越多的检测框位置不够精准。
+* Sim: 在Loc的基础上,如果检测框与真值框的类别不相同,但两者同属于一个亚类,则不认为该检测框是错误的,在这种评估要求下的PR曲线, AP为0.832。Sim与Loc之间的红色区域面积越大,表示子类间的混淆程度越高。
+* Oth: 在Sim的基础上,如果检测框与真值框的亚类不相同,则不认为该检测框是错误的,在这种评估要求下的PR曲线,AP为0.841。Oth与Sim之间的绿色区域面积越大,表示亚类间的混淆程度越高。
+* BG: 在Oth的基础上,背景区域上的检测框不认为是错误的,在这种评估要求下的PR曲线,AP为91.1。BG与Oth之间的紫色区域面积越大,表示背景区域被误检的数量越多。
+* FN: 在BG的基础上,漏检的真值框不认为是错误的,在这种评估要求下的PR曲线,AP为1.00。FN与BG之间的橙色区域面积越大,表示漏检的真值框数量越多。
+
+更为详细的说明参考[COCODataset官网给出分析工具说明](https://cocodataset.org/#detection-eval)
+
+### 参数
+> * **eval_details_file** (str): 模型评估结果的保存路径,包含真值信息和预测结果。默认值为None。
+> * **gt** (list): 数据集的真值信息。默认值为None。
+> * **pred_bbox** (list): 模型在数据集上的预测框。默认值为None。
+> * **pred_mask** (list): 模型在数据集上的预测mask。默认值为None。
+> * **iou_thresh** (float): 判断预测框或预测mask为真阳时的IoU阈值。默认值为0.5。
+> * **save_dir** (str): 可视化结果保存路径。默认值为'./'。
+
+**注意:**`eval_details_file`的优先级更高,只要`eval_details_file`不为None,就会从`eval_details_file`提取真值信息和预测结果做分析。当`eval_details_file`为None时,则用`gt`、`pred_mask`、`pred_mask`做分析。
+
+### 使用示例
+点击下载如下示例中的[模型](https://bj.bcebos.com/paddlex/models/insect_epoch_270.zip)和[数据集](https://bj.bcebos.com/paddlex/datasets/insect_det.tar.gz)
+
+> 方式一:分析训练过程中保存的模型文件夹中的评估结果文件`eval_details.json`,例如[模型](https://bj.bcebos.com/paddlex/models/insect_epoch_270.zip)中的`eval_details.json`。
+```
+import paddlex as pdx
+eval_details_file = 'insect_epoch_270/eval_details.json'
+pdx.det.coco_error_analysis(eval_details_file, save_dir='./insect')
+```
+> 方式二:分析模型评估函数返回的评估结果。
+
+```
+import os
+# 选择使用0号卡
+os.environ['CUDA_VISIBLE_DEVICES'] = '0'
+
+from paddlex.det import transforms
+import paddlex as pdx
+
+model = pdx.load_model('insect_epoch_270')
+eval_dataset = pdx.datasets.VOCDetection(
+    data_dir='insect_det',
+    file_list='insect_det/val_list.txt',
+    label_list='insect_det/labels.txt',
+    transforms=model.eval_transforms)
+metrics, evaluate_details = model.evaluate(eval_dataset, batch_size=8, return_details=True)
+gt = evaluate_details['gt']
+bbox = evaluate_details['bbox']
+pdx.det.coco_error_analysis(gt=gt, pred_bbox=bbox, save_dir='./insect')
+```
+所有类别整体的分析结果示例如下:
+
+![](./images/insect_bbox-allclass-allarea.png)

+ 1 - 0
docs/examples/index.rst

@@ -15,3 +15,4 @@ PaddleX精选飞桨视觉开发套件在产业实践中的成熟模型结构,
    multi-channel_remote_sensing/README.md
    remote_sensing.md
    change_detection.md
+   industrial_quality_inspection/README.md

+ 99 - 0
docs/examples/industrial_quality_inspection/README.md

@@ -0,0 +1,99 @@
+# 工业质检
+
+本案例面向工业质检场景里的铝材表面缺陷检测,提供了针对GPU端和CPU端两种部署场景下基于PaddleX的解决方案,希望通过梳理优化模型精度和性能的思路能帮助用户更高效地解决实际质检应用中的问题。
+
+## 1. GPU端解决方案
+
+### 1.1 数据集介绍
+
+本案例使用天池铝材表面缺陷检测初赛数据集,共有3005张图片,分别检测擦花、杂色、漏底、不导电、桔皮、喷流、漆泡、起坑、脏点和角位漏底10种缺陷,这10种缺陷的定义和示例可点击文档[天池铝材表面缺陷检测初赛数据集示例](./dataset.md)查看。
+
+将这3005张图片按9:1随机切分成2713张图片的训练集和292张图片的验证集。
+
+### 1.2 精度优化
+
+本小节侧重展示在模型迭代过程中优化精度的思路,在本案例中,有些优化策略获得了精度收益,而有些没有。在其他质检场景中,可根据实际情况尝试这些优化策略。点击文档[精度优化](./accuracy_improvement.md)查看。
+
+### 1.3 性能优化
+
+在完成模型精度优化之后,从以下两个方面对模型进行加速:
+
+#### (1) 减少FPN部分的通道数量
+
+将FPN部分的通道数量由原本的256减少至64,使用方式在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时设置参数`fpn_num_channels`为64即可,需要重新对模型进行训练。
+
+#### (2) 减少测试阶段的候选框数量
+
+将测试阶段RPN部分做非极大值抑制计算的候选框数量由原本的6000减少至500,将RPN部分做完非极大值抑制后保留的候选框数量由原本的1000减少至300。使用方式在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时设置参数`test_pre_nms_top_n`为500,`test_post_nms_top_n`为300。
+
+采用Fluid C++预测引擎在Tesla P40上测试模型的推理时间(输入数据拷贝至GPU的时间、计算时间、数据拷贝至CPU的时间),输入大小设置为800x1333,加速前后推理时间如下表所示:
+
+| 模型 | 推理时间 (ms/image)| VOC mAP (%) |
+| -- | -- | -- |
+| baseline | 66.51 | 88.87 |
+| + fpn channel=64 + test proposal=pre/post topk 500/300 | 46.08 | 87.72 |
+
+### 1.4 最终方案
+
+本案例面向GPU端的最终方案是选择二阶段检测模型FasterRCNN,其骨干网络选择加入了可变形卷积(DCN)的ResNet50_vd,训练时使用SSLD蒸馏方案训练得到的ResNet50_vd预训练模型,FPN部分的通道数量设置为64。使用复核过的数据集,训练阶段数据增强策略采用RandomHorizontalFlip、RandomDistort、RandomCrop,并加入背景图片。测试阶段的RPN部分做非极大值抑制计算的候选框数量由原本的6000减少至500、做完非极大值抑制后保留的候选框数量由原本的1000减少至300。模型在验证集上的VOC mAP为87.72%。
+
+在Tesla P40的Linux系统下,对于输入大小是800 x 1333的模型,图像预处理时长为30ms/image,模型的推理时间为46.08ms/image,包括输入数据拷贝至GPU的时间、计算时间、数据拷贝至CPU的时间。
+
+| 模型 | VOC mAP (%) | 推理时间 (ms/image)
+| -- | -- | -- |
+| FasterRCNN-ResNet50_vd_ssld | 81.05 | 48.62 |
+| + dcn | 88.09 | 66.51 |
+| + RandomHorizontalFlip/RandomDistort/RandomCrop | 90.23| 66.51 |
+| + background images | 88.87 | 66.51 |
+| + fpn channel=64 | 87.79 | 48.65 |
+| + test proposal=pre/post topk 500/300 | 87.72 | 46.08 |
+
+具体的训练和部署流程点击文档[GPU端最终解决方案](./gpu_solution.md)进行查看。
+
+## 2. CPU端解决方案
+
+为了实现高效的模型推理,面向CPU端的模型选择精度和效率皆优的单阶段检测模型YOLOv3,骨干网络选择基于PaddleClas中SSLD蒸馏方案训练得到的MobileNetv3_large。训练完成后,对模型做剪裁操作,以提升模型的性能。模型在验证集上的VOC mAP为79.02%。
+
+部署阶段,借助OpenVINO预测引擎完成在Intel(R) Core(TM) i9-9820X CPU @ 3.30GHz Windows系统下高效推理。对于输入大小是608 x 608的模型,图像预处理时长为38.69 ms/image,模型的推理时间为34.50ms/image,
+
+| 模型 | VOC mAP (%) | Inference Speed (ms/image)
+| -- | -- | -- |
+| YOLOv3-MobileNetv3_ssld | 78.52 | 56.71 |
+| pruned YOLOv3-MobileNetv3_ssld | 79.02 | 34.50 |
+
+### 模型训练
+
+[环境前置依赖](./gpu_solution.md#%E5%89%8D%E7%BD%AE%E4%BE%9D%E8%B5%96)、[下载PaddleX源码](./gpu_solution.md#1-%E4%B8%8B%E8%BD%BDpaddlex%E6%BA%90%E7%A0%81)、[下载数据集](./gpu_solution.md#2-%E4%B8%8B%E8%BD%BD%E6%95%B0%E6%8D%AE%E9%9B%86)与GPU端是一样的,可点击文档[GPU端最终解决方案](./gpu_solution.md)查看,在此不做赘述。
+
+如果不想再次训练模型,可以直接下载已经训练好的模型完成后面的模型测试和部署推理:
+
+```
+wget https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/models/yolov3_mobilenetv3_large_pruned.tar.gz
+tar xvf yolov3_mobilenetv3_large_pruned.tar.gz
+```
+
+运行以下代码进行模型训练,代码会自动下载数据集,如若事先下载了数据集,需将下载和解压铝材缺陷检测数据集的相关行注释掉。代码中默认使用0,1,2,3,4,5,6,7号GPU训练,可根据实际情况设置卡号并调整`batch_size`和`learning_rate`。
+
+```
+python train_yolov3.py
+```
+
+### 模型剪裁
+
+运行以下代码,计算在不同的精度损失下,模型各层的剪裁比例:
+
+```
+python cal_sensitivities_file.py
+```
+
+设置可允许的精度损失为0.05,对模型进行剪裁,剪裁后需要重新训练模型:
+
+```
+python train_pruned_yolov3.py
+```
+
+[分析预测错误的原因](./gpu_solution.md#4-%E5%88%86%E6%9E%90%E9%A2%84%E6%B5%8B%E9%94%99%E8%AF%AF%E7%9A%84%E5%8E%9F%E5%9B%A0)、[统计图片级召回率和误检率](./gpu_solution.md#5-%E7%BB%9F%E8%AE%A1%E5%9B%BE%E7%89%87%E7%BA%A7%E5%8F%AC%E5%9B%9E%E7%8E%87%E5%92%8C%E8%AF%AF%E6%A3%80%E7%8E%87)、[模型测试](./gpu_solution.md#6-%E6%A8%A1%E5%9E%8B%E6%B5%8B%E8%AF%95)这些步骤与GPU端是一样的,可点击文档[GPU端最终解决方案](./gpu_solution.md)查看,在此不做赘述。
+
+### 推理部署
+
+本案例采用C++部署方式,通过OpenVINO将模型部署在Intel(R) Core(TM) i9-9820X CPU @ 3.30GHz的Windows系统下,具体的部署流程请参考文档[PaddleX模型多端安全部署/OpenVINO部署](https://paddlex.readthedocs.io/zh_CN/develop/deploy/openvino/index.html)。

+ 97 - 0
docs/examples/industrial_quality_inspection/accuracy_improvement.md

@@ -0,0 +1,97 @@
+# 精度优化
+
+本小节侧重展示在模型迭代过程中优化精度的思路,在本案例中,有些优化策略获得了精度收益,而有些没有。在其他质检场景中,可根据实际情况尝试这些优化策略。
+
+## (1) 基线模型选择
+
+相较于单阶段检测模型,二阶段检测模型的精度更高但是速度更慢。考虑到是部署到GPU端,本案例选择二阶段检测模型FasterRCNN作为基线模型,其骨干网络选择ResNet50_vd,并使用基于PaddleClas中SSLD蒸馏方案训练得到的ResNet50_vd预训练模型(ImageNet1k验证集上Top1 Acc为82.39%)。训练完成后,模型在验证集上的精度VOC mAP为73.36%。
+
+## (2) 模型效果分析
+
+使用PaddleX提供的[paddlex.det.coco_error_analysis](https://paddlex.readthedocs.io/zh_CN/develop/apis/visualize.html#paddlex-det-coco-error-analysis)接口对模型在验证集上预测错误的原因进行分析,分析结果以图表的形式展示如下:
+
+| all classes| 擦花 | 杂色 | 漏底 | 不导电 | 桔皮 | 喷流 | 漆泡 | 起坑 | 脏点 | 角位漏底 |
+| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- |
+|![](https://agroup-bos-bj.cdn.bcebos.com/bj-972007ed33acba896af4aee11cda6abd00ce9ba3)|![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-91790922f4b137880a143f79134391657830c7d2)|![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-46e25934ea0bdc5a7f819bac853883d19e0edc5f)| ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-236eb142601c01ea3f239c771534813ad3fae439) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-a8872384daca0e499fbf0f281980d4a28a89bb97) | ![](https://agroup-bos-bj.cdn.bcebos.com/bj-068e9d36fa6a172215c93bafe00c56d55fb9890d) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-da89bff70e7100002993b4ca7000ba6028b7abf4) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-f1540c077a30b012da41077941e49235e0f844ed) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-44fbd0c3af5c833f80e19b8fee576c1c49464385) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-72dce9276ee20349a09a29aa3967dd79e31b9174) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-c173f25931c901ade26a2ceab99d8eae5310e0ec) |
+
+分析图表展示了7条Precision-Recall(PR)曲线,每一条曲线表示的Average Precision (AP)比它左边那条高,原因是逐步放宽了评估要求。以擦花类为例,各条PR曲线的评估要求解释如下:
+
+* C75: 在IoU设置为0.75时的PR曲线, AP为0.001。
+* C50: 在IoU设置为0.5时的PR曲线,AP为0.622。C50与C75之间的白色区域面积代表将IoU从0.75放宽至0.5带来的AP增益。
+* Loc: 在IoU设置为0.1时的PR曲线,AP为0.740。Loc与C50之间的蓝色区域面积代表将IoU从0.5放宽至0.1带来的AP增益。蓝色区域面积越大,表示越多的检测框位置不够精准。
+* Sim: 在Loc的基础上,如果检测框与真值框的类别不相同,但两者同属于一个亚类,则不认为该检测框是错误的,在这种评估要求下的PR曲线, AP为0.742。Sim与Loc之间的红色区域面积越大,表示子类间的混淆程度越高。VOC格式的数据集所有的类别都属于同一个亚类。
+* Oth: 在Sim的基础上,如果检测框与真值框的亚类不相同,则不认为该检测框是错误的,在这种评估要求下的PR曲线,AP为0.742。Oth与Sim之间的绿色区域面积越大,表示亚类间的混淆程度越高。VOC格式的数据集中所有的类别都属于同一个亚类,故不存在亚类间的混淆。
+* BG: 在Oth的基础上,背景区域上的检测框不认为是错误的,在这种评估要求下的PR曲线,AP为92.1。BG与Oth之间的紫色区域面积越大,表示背景区域被误检的数量越多。
+* FN: 在BG的基础上,漏检的真值框不认为是错误的,在这种评估要求下的PR曲线,AP为1.00。FN与BG之间的橙色区域面积越大,表示漏检的真值框数量越多。
+
+从分析图表中可以看出,杂色、桔皮、起坑三类检测效果较好,角位漏底存在少许检测框没有达到IoU 0.5的情况,问题较多的是擦花、不导电、喷流、漆泡、脏点。擦花类最严重的问题是误检、位置不精准、漏检,不导电类最严重的问题是漏检、位置不精准,喷流类和漆泡类最严重的问题是位置不精准、误检,脏点类最严重的问题是误检、漏检。为进一步理解造成这些问题的原因,将验证集上的预测结果进行了可视化,然后发现数据集标注存在以下问题:
+
+* 轻微的缺陷不视为缺陷,但轻微的界定不明确,有些轻微的缺陷被标注了,造成误检较多
+* 不导电、漏底、角位漏底外观极其相似,肉眼难以区分,导致这三类极其容易混淆而使得评估时误检或漏检的产生
+* 有些轻微的擦花和脏点被标注了,有些明显的反而没有被标注,造成了这两类误检和漏检情况都较为严重
+* 喷流和漆泡多为连续性的缺陷,一处喷流,其水平线上还会有其他喷流,一个气泡,其水平线上还会有一排气泡。但有时候把这些连续性的缺陷标注成一个目标,有时候又单独地标注不同的部分。导致模型有时候检测单独部分,有时候又检测整体,造成这两类位置不精准、误检较多。
+
+## (3) 数据复核
+
+为了减少原始数据标注的诸多问题对模型优化的影响,需要对数据进行复核。复核准则示例如下:
+
+* 擦花:不明显的擦花不标注,面状擦花以同一个框表示,条状擦花一条擦花以一个框表示
+* 漏底、角位漏底、不导电:三者过于相似,归为一类
+* 桔皮:忽略不是大颗粒状的表面
+* 喷流:明显是一条喷流的就用一个框表示,不是的话用多个框表示
+* 漆泡:不要单独标一个点,一连串点标一个框
+* 脏点:忽略轻微脏点
+
+对数据集复核并重新标注后,将FasterRCNN-ResNet50_vd_ssld重新在训练集上进行训练,模型在验证集上的VOC mAP为81.05%。
+
+## (4) 可变形卷积加入
+
+由于喷流、漆泡的形态不规则,导致这两类的很多预测框位置不精准。为了解决该问题,选择在骨干网络ResNet50_vd中使用可变形卷积(DCN)。重新训练后,模型在验证集上的VOC mAP为88.09%,喷流的VOC AP由57.3%提升至78.7%,漆泡的VOC AP由74.7%提升至96.7%。
+
+## (5) 数据增强选择
+
+在(4)的基础上,选择加入一些数据增强策略来进一步提升模型的精度。本案例选择同时使用[RandomHorizontalFlip](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#randomhorizontalflip)、[RandomDistort](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#randomdistort)、[RandomCrop](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#randomcrop)这三种数据增强方法,重新训练后的模型在验证集上的VOC mAP为90.23%。
+
+除此之外,还可以尝试的数据增强方式有[MultiScaleTraining](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#resizebyshort)、[RandomExpand](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#randomexpand)。本案例使用的铝材表面缺陷检测数据集中,同一个类别的尺度变化不大,使用MultiScaleTraining或者RandomExpand反而使得原始数据分布发生改变。对此,本案例也进行了实验验证,使用RandomHorizontalFlip + RandomDistort + RandomCrop + MultiScaleTraining数据增强方式训练得到的模型在验证集上的VOC mAP为87.15%,使用RandomHorizontalFlip + RandomDistort + RandomCrop + RandomExpand数据增强方式训练得到的模型在验证集上的的VOC mAP为88.56%。
+
+## (6) 背景图片加入
+
+本案例将数据集中提供的背景图片按9:1切分成了1116张、135张两部分,并使用(5)中训练好的模型在135张背景图片上进行测试,发现图片级误检率高达21.5%。为了降低模型的误检率,使用[paddlex.datasets.VOCDetection.add_negative_samples](https://paddlex.readthedocs.io/zh_CN/develop/apis/datasets.html#add-negative-samples)接口将1116张背景图片加入到原本的训练集中,重新训练后图片级误检率降低至4%。为了不让训练被背景图片主导,本案例通过将`train_list.txt`中的文件路径多写了一遍,从而增加有目标图片的占比。
+
+| 模型 | VOC mAP (%) | 有缺陷图片级召回率 | 背景图片级误检率 |
+| -- | -- | -- | -- |
+| FasterRCNN-ResNet50_vd_ssld + DCN + RandomHorizontalFlip + RandomDistort + RandomCrop | 90.23 | 95.5 | 21.5 |
+| FasterRCNN-ResNet50_vd_ssld + DCN + RandomHorizontalFlip + RandomDistort + RandomCrop + 背景图片 | 88.87 | 95.2 | 4 |
+
+【名词解释】
+
+* 图片级别的召回率:只要在有目标的图片上检测出目标(不论框的个数),该图片被认为召回。批量有目标图片中被召回图片所占的比例,即为图片级别的召回率。
+* 图片级别的误检率:只要在无目标的图片上检测出目标(不论框的个数),该图片被认为误检。批量无目标图片中被误检图片所占的比例,即为图片级别的误检率。
+
+## (7) 分类损失函数选择
+
+降低误检率的措施除了(6)中提到的将背景图片加入训练,还可以将RPN部分的分类损失函数选择为`SigmoidFocalLoss`,将更多的anchors加入训练,增加难分样本的在损失函数的比重进而降低误检率。在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时将参数`rpn_cls_loss`设置为'SigmoidFocalLoss',同时需要调整参数`rpn_focal_loss_alpha`、`rpn_focal_loss_gamma`、`rpn_batch_size_per_im`、`rpn_fg_fraction`的设置。
+
+## (8) 位置回归损失函数选择
+
+RCNN部分的位置回归损失函数除了'SmoothL1Loss'以外,还可以选择'CIoULoss',使用方式在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时设置参数`rcnn_bbox_loss`即可。在本案例中,选择'CIoULoss'并没有带来精度收益,故还是选择'SmoothL1Loss'。其他质检场景下,也可尝试使用'CIoULoss'。
+
+## (9) 正负样本采样方式选择
+
+当目标物体的区域只占图像的一小部分时,可以考虑采用[LibraRCNN](https://arxiv.org/abs/1904.02701)中提出的IoU-balanced Sampling采样方式来获取更多的难分负样本。使用方式在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时将参数`bbox_assigner`设置为'LibraBBoxAssigner'即可。
+
+## (10) 预处理对比度增强
+
+工业界常用灰度相机采集图片,会存在目标与周围背景对比度不明显而无法被检测出的情况。在这种情况下,可以在定义预处理的时候使用[paddlex.det.transforms.CLAHE](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#clahe)对灰度图像的对比度进行增强。
+
+灰度图:
+
+![](../../../examples/industrial_quality_inspection/image/before_clahe.png)
+
+对比度增加后的灰度图:
+
+![](../../../examples/industrial_quality_inspection/image/after_clahe.png) |
+
+## (11) 样本生成
+
+对于数量较少的类别或者小目标,可以通过把这些目标物体粘贴在背景图片上来生成新的图片和标注文件,并把这些新的样本加入到训练中从而提升模型精度。目前PaddleX提供了实现该功能的接口,详细见[paddlex.det.paste_objects](https://paddlex.readthedocs.io/zh_CN/develop/apis/tools.html#paddlex-det-paste-objects),需要注意的是,前景目标颜色与背景颜色差异较大时生成的新图片才会比较逼真。

+ 14 - 0
docs/examples/industrial_quality_inspection/dataset.md

@@ -0,0 +1,14 @@
+# 天池铝材表面缺陷检测初赛数据集示例
+
+| 序号 | 瑕疵名称 | 瑕疵成因 | 瑕疵图片示例 | 图片数量 |
+| -- | -- | -- | -- | -- |
+| 1 | 擦花(擦伤)| 表面处理(喷涂)后有轻微擦到其它的东西,造成痕迹 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-be236181808cd68e3ad941efdc16f11d4a04dd35) | 128 |
+| 2 | 杂色 | 喷涂换颜料的时候,装颜料的容器未清洗干净,造成喷涂时有少量其它颜色掺入 |![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-58d7fe6d1a0b72cfd735fa9e192f4f04e58c0901) |365 |
+| 3 | 漏底 | 喷粉效果不好,铝材大量底色露出 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-16d7005e897900aa916ea639cc49810fe77fa982) | 538 |
+| 4 | 不导电 | 直接喷不到铝材表面上去 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-3c886ff349280c22796a46f296fedd3296ae4120) | 390 |
+|5 | 桔皮 | 表面处理后涂层表面粗糙,大颗粒 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-726ce1ab1ac7aa47dd4c8bff89df85b4e3b7ae4d) | 173 |
+| 6 | 喷流| 喷涂时油漆稀从上流下来,有流动痕迹 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-1b245e25169d618a083669acde2c293bb548bea6) | 86 |
+| 7 |漆泡 | 喷涂后表面起泡,小而多| ![](https://agroup-bos-bj.cdn.bcebos.com/bj-00ed3f730ce41f7f18a4d6a5402fd3e61bfa0db9) | 82 |
+| 8 | 起坑 | 型材模具问题,做出来的型材一整条都有一条凹下去的部分 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-739abb8a6d9144560290cd8f2eaa05b3aa183e17) | 407 |
+| 9 | 脏点 | 表面处理时,有灰尘或一些脏东西未能擦掉,导致涂层有颗粒比较突出 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-487a749bfbb149294f540cc3710aacee30e154f2) | 261 |
+| 10 | 角位漏底 | 在型材角落出现的露底 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-91a9fe0f3a69f1b4ab6006bae870e5f687fab111) | 346 |

+ 116 - 0
docs/examples/industrial_quality_inspection/gpu_solution.md

@@ -0,0 +1,116 @@
+# GPU端最终解决方案
+
+本案例面向GPU端的最终方案是选择二阶段检测模型FasterRCNN,其骨干网络选择加入了可变形卷积(DCN)的ResNet50_vd,训练时使用SSLD蒸馏方案训练得到的ResNet50_vd预训练模型,FPN部分的通道数量设置为64,训练阶段数据增强策略采用RandomHorizontalFlip、RandomDistort、RandomCrop,并加入背景图片,测试阶段的RPN部分做非极大值抑制计算的候选框数量由原本的6000减少至500、做完非极大值抑制后保留的候选框数量由原本的1000减少至300。
+
+在Tesla P40的Linux系统下,对于输入大小是800 x 1333的模型,图像预处理时长为30ms/image,模型的推理时间为46.08ms/image,包括输入数据拷贝至GPU的时间、计算时间、数据拷贝至CPU的时间。
+
+| 模型 | VOC mAP (%) | 推理时间 (ms/image)
+| -- | -- | -- |
+| FasterRCNN-ResNet50_vd_ssld | 81.05 | 48.62 |
+| + dcn | 88.09 | 66.51 |
+| + RandomHorizontalFlip/RandomDistort/RandomCrop | 90.23| 66.51 |
+| + background images | 88.87 | 66.51 |
+| + fpn channel=64 | 87.79 | 48.65 |
+| + test proposal=pre/post topk 500/300 | 87.72 | 46.08 |
+
+## 前置依赖
+
+* Paddle paddle >= 1.8.0
+* Python >= 3.5
+* PaddleX >= 1.3.0
+
+## 模型训练
+
+### (1) 下载PaddleX源码
+
+```
+git clone https://github.com/PaddlePaddle/PaddleX
+
+cd PaddleX/examples/industrial_quality_inspection
+```
+
+### (2) 下载数据集
+
+因数据集较大,可运行以下代码提前将数据集下载并解压。训练代码中也会自动下载数据集,所以这一步不是必须的。
+
+```
+wget https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/datasets/aluminum_inspection.tar.gz
+tar xvf aluminum_inspection.tar.gz
+```
+### (3) 下载预先训练好的模型
+
+如果不想再次训练模型,可以直接下载已经训练好的模型完成后面的模型测试和部署推理:
+
+```
+wget https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/models/faster_rcnn_r50_vd_dcn.tar.gz
+tar xvf faster_rcnn_r50_vd_dcn.tar.gz
+```
+### (4) 训练
+
+运行以下代码进行模型训练,代码会自动下载数据集,如若事先下载了数据集,需将下载和解压铝材缺陷检测数据集的相关行注释掉。代码中默认使用0,1,2,3,4号GPU训练,可根据实际情况设置卡号并调整`batch_size`和`learning_rate`。
+
+```
+python train_rcnn.py
+```
+
+### (5) 分析预测错误的原因
+
+在模型迭代过程中,运行以下代码可完成模型效果的分析并生成分析结果图表:
+
+```
+python error_analysis.py
+```
+
+可参考[性能优化部分的模型效果分析](./accuracy_improvement.md#2-%E6%A8%A1%E5%9E%8B%E6%95%88%E6%9E%9C%E5%88%86%E6%9E%90)来理解当前模型预测错误的原因。
+
+运行以下代码,生成可视化真值和预测结果的对比图以进一步理解模型效果,代码中的置信度阈值可根据实际情况进行调整。
+
+```
+python compare.py
+```
+
+![](../../../examples/industrial_quality_inspection/image/compare_budaodian-116.jpg)
+
+左边是可视化真值,右边是可视化预测结果。
+
+### (6) 统计图片级召回率和误检率
+
+模型迭代完成后,计算不同置信度阈值下[图片级召回率](./accuracy_improvement.md#6-%E8%83%8C%E6%99%AF%E5%9B%BE%E7%89%87%E5%8A%A0%E5%85%A5)和[图片级误检率](./accuracy_improvement.md#6-%E8%83%8C%E6%99%AF%E5%9B%BE%E7%89%87%E5%8A%A0%E5%85%A5),找到符合要求的召回率和误检率,对应的置信度阈值用于后续模型预测阶段。
+
+```
+python cal_tp_fp.py
+```
+
+执行后会生成图表`image-level_tp_fp.png`和文件`tp_fp_list.txt`,示意如下:
+
+图表`image-level_tp_fp.png`:
+
+![](../../../examples/industrial_quality_inspection/image/image-level_tp_fp.png)
+
+文件[tp_fp_list.txt](tp_fp_list.md)
+
+图表`image-level_tp_fp.png`中左边子图,横坐标表示不同置信度阈值下计算得到的图片级召回率,纵坐标表示各图片级召回率对应的图片级误检率。右边子图横坐标表示图片级召回率,纵坐标表示各图片级召回率对应的置信度阈值。从图表中可较为直观地看出当前模型的图片级召回率和误检率的量级,从文件`tp_fp_list.txt`可以查到具体数值,例如在图片级召回率/图片级误检率为[0.9589,0.0074]这一组符合要求,就将对应的置信度阈值0.90选取为后续预测推理的阈值。
+
+### (7) 模型测试
+
+测试集因没有标注文件,这里单独下载测试集图片:
+
+```
+wget https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/datasets/aluminum_inspection_test.tar.gz
+tar xvf aluminum_inspection_test.tar.gz
+```
+
+加载训练好的模型,使用(5)选取的置信度阈值0.90对验证集图片或者测试集图片进行预测:
+
+```
+python predict.py
+```
+可视化预测结果示例如下:
+
+![](../../../examples/industrial_quality_inspection/image/visualize_budaodian-116.jpg)
+
+## 推理部署
+
+本案例采用C++部署方式将模型部署在Tesla P40的Linux系统下,具体的C++部署流程请参考文档[PaddleX模型多端安全部署/C++部署](https://paddlex.readthedocs.io/zh_CN/develop/deploy/server/cpp/index.html)。
+
+对于输入大小是800 x 1333的模型,图像预处理时长为30ms/image。值得一提的是预处理中的Normalize操作比较耗时,因此在设置预处理操作时,可以先进行Resize操作再做Normalize。模型的推理时间为46.08ms/image,包括输入数据拷贝至GPU的时间、计算时间、数据拷贝至CPU的时间。

+ 102 - 0
docs/examples/industrial_quality_inspection/tp_fp_list.md

@@ -0,0 +1,102 @@
+| score | recall rate | false-positive rate |
+| -- | -- | -- |
+| 0.000000 | 0.982877 | 0.022222 |
+| 0.010000 | 0.982877 | 0.022222 |
+| 0.020000 | 0.982877 | 0.022222 |
+| 0.030000 | 0.982877 | 0.022222 |
+| 0.040000 | 0.982877 | 0.022222 |
+| 0.050000 | 0.982877 | 0.022222 |
+| 0.060000 | 0.982877 | 0.022222 |
+| 0.070000 | 0.979452 | 0.022222 |
+| 0.080000 | 0.979452 | 0.022222 |
+| 0.090000 | 0.979452 | 0.022222 |
+| 0.100000 | 0.979452 | 0.022222 |
+| 0.110000 | 0.979452 | 0.022222 |
+| 0.120000 | 0.979452 | 0.022222 |
+| 0.130000 | 0.979452 | 0.022222 |
+| 0.140000 | 0.976027 | 0.022222 |
+| 0.150000 | 0.976027 | 0.022222 |
+| 0.160000 | 0.976027 | 0.022222 |
+| 0.170000 | 0.976027 | 0.022222 |
+| 0.180000 | 0.976027 | 0.022222 |
+| 0.190000 | 0.976027 | 0.022222 |
+| 0.200000 | 0.976027 | 0.022222 |
+| 0.210000 | 0.976027 | 0.022222 |
+| 0.220000 | 0.976027 | 0.022222 |
+| 0.230000 | 0.976027 | 0.022222 |
+| 0.240000 | 0.976027 | 0.022222 |
+| 0.250000 | 0.976027 | 0.022222 |
+| 0.260000 | 0.976027 | 0.014815 |
+| 0.270000 | 0.976027 | 0.014815 |
+| 0.280000 | 0.976027 | 0.014815 |
+| 0.290000 | 0.976027 | 0.014815 |
+| 0.300000 | 0.976027 | 0.014815 |
+| 0.310000 | 0.976027 | 0.014815 |
+| 0.320000 | 0.976027 | 0.014815 |
+| 0.330000 | 0.976027 | 0.014815 |
+| 0.340000 | 0.976027 | 0.014815 |
+| 0.350000 | 0.976027 | 0.014815 |
+| 0.360000 | 0.976027 | 0.014815 |
+| 0.370000 | 0.976027 | 0.014815 |
+| 0.380000 | 0.976027 | 0.014815 |
+| 0.390000 | 0.976027 | 0.014815 |
+| 0.400000 | 0.976027 | 0.014815 |
+| 0.410000 | 0.976027 | 0.014815 |
+| 0.420000 | 0.976027 | 0.014815 |
+| 0.430000 | 0.976027 | 0.014815 |
+| 0.440000 | 0.972603 | 0.014815 |
+| 0.450000 | 0.972603 | 0.014815 |
+| 0.460000 | 0.972603 | 0.014815 |
+| 0.470000 | 0.972603 | 0.014815 |
+| 0.480000 | 0.972603 | 0.014815 |
+| 0.490000 | 0.972603 | 0.014815 |
+| 0.500000 | 0.972603 | 0.014815 |
+| 0.510000 | 0.972603 | 0.014815 |
+| 0.520000 | 0.972603 | 0.014815 |
+| 0.530000 | 0.972603 | 0.014815 |
+| 0.540000 | 0.972603 | 0.014815 |
+| 0.550000 | 0.972603 | 0.014815 |
+| 0.560000 | 0.969178 | 0.014815 |
+| 0.570000 | 0.969178 | 0.014815 |
+| 0.580000 | 0.969178 | 0.014815 |
+| 0.590000 | 0.969178 | 0.014815 |
+| 0.600000 | 0.969178 | 0.014815 |
+| 0.610000 | 0.969178 | 0.014815 |
+| 0.620000 | 0.969178 | 0.014815 |
+| 0.630000 | 0.969178 | 0.014815 |
+| 0.640000 | 0.969178 | 0.014815 |
+| 0.650000 | 0.969178 | 0.014815 |
+| 0.660000 | 0.969178 | 0.014815 |
+| 0.670000 | 0.969178 | 0.014815 |
+| 0.680000 | 0.969178 | 0.014815 |
+| 0.690000 | 0.969178 | 0.014815 |
+| 0.700000 | 0.969178 | 0.014815 |
+| 0.710000 | 0.969178 | 0.014815 |
+| 0.720000 | 0.969178 | 0.014815 |
+| 0.730000 | 0.969178 | 0.014815 |
+| 0.740000 | 0.969178 | 0.014815 |
+| 0.750000 | 0.969178 | 0.014815 |
+| 0.760000 | 0.969178 | 0.014815 |
+| 0.770000 | 0.965753 | 0.014815 |
+| 0.780000 | 0.965753 | 0.014815 |
+| 0.790000 | 0.965753 | 0.014815 |
+| 0.800000 | 0.962329 | 0.014815 |
+| 0.810000 | 0.962329 | 0.014815 |
+| 0.820000 | 0.962329 | 0.014815 |
+| 0.830000 | 0.962329 | 0.014815 |
+| 0.840000 | 0.962329 | 0.014815 |
+| 0.850000 | 0.958904 | 0.014815 |
+| 0.860000 | 0.958904 | 0.014815 |
+| 0.870000 | 0.958904 | 0.014815 |
+| 0.880000 | 0.958904 | 0.014815 |
+| 0.890000 | 0.958904 | 0.014815 |
+| 0.900000 | 0.958904 | 0.007407 |
+| 0.910000 | 0.958904 | 0.007407 |
+| 0.920000 | 0.958904 | 0.007407 |
+| 0.930000 | 0.955479 | 0.007407 |
+| 0.940000 | 0.955479 | 0.007407 |
+| 0.950000 | 0.955479 | 0.007407 |
+| 0.960000 | 0.955479 | 0.007407 |
+| 0.970000 | 0.955479 | 0.007407 |
+| 0.980000 | 0.941781 | 0.000000 |
+| 0.990000 | 0.893836 | 0.000000 |

+ 99 - 0
examples/industrial_quality_inspection/README.md

@@ -0,0 +1,99 @@
+# 工业质检
+
+本案例面向工业质检场景里的铝材表面缺陷检测,提供了针对GPU端和CPU端两种部署场景下基于PaddleX的解决方案,希望通过梳理优化模型精度和性能的思路能帮助用户更高效地解决实际质检应用中的问题。
+
+## 1. GPU端解决方案
+
+### 1.1 数据集介绍
+
+本案例使用天池铝材表面缺陷检测初赛数据集,共有3005张图片,分别检测擦花、杂色、漏底、不导电、桔皮、喷流、漆泡、起坑、脏点和角位漏底10种缺陷,这10种缺陷的定义和示例可点击文档[天池铝材表面缺陷检测初赛数据集示例](./dataset.md)查看。
+
+将这3005张图片按9:1随机切分成2713张图片的训练集和292张图片的验证集。
+
+### 1.2 精度优化
+
+本小节侧重展示在模型迭代过程中优化精度的思路,在本案例中,有些优化策略获得了精度收益,而有些没有。在其他质检场景中,可根据实际情况尝试这些优化策略。点击文档[精度优化](./accuracy_improvement.md)查看。
+
+### 1.3 性能优化
+
+在完成模型精度优化之后,从以下两个方面对模型进行加速:
+
+#### (1) 减少FPN部分的通道数量
+
+将FPN部分的通道数量由原本的256减少至64,使用方式在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时设置参数`fpn_num_channels`为64即可,需要重新对模型进行训练。
+
+#### (2) 减少测试阶段的候选框数量
+
+将测试阶段RPN部分做非极大值抑制计算的候选框数量由原本的6000减少至500,将RPN部分做完非极大值抑制后保留的候选框数量由原本的1000减少至300。使用方式在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时设置参数`test_pre_nms_top_n`为500,`test_post_nms_top_n`为300。
+
+采用Fluid C++预测引擎在Tesla P40上测试模型的推理时间(输入数据拷贝至GPU的时间、计算时间、数据拷贝至CPU的时间),输入大小设置为800x1333,加速前后推理时间如下表所示:
+
+| 模型 | 推理时间 (ms/image)| VOC mAP (%) |
+| -- | -- | -- |
+| baseline | 66.51 | 88.87 |
+| + fpn channel=64 + test proposal=pre/post topk 500/300 | 46.08 | 87.72 |
+
+### 1.4 最终方案
+
+本案例面向GPU端的最终方案是选择二阶段检测模型FasterRCNN,其骨干网络选择加入了可变形卷积(DCN)的ResNet50_vd,训练时使用SSLD蒸馏方案训练得到的ResNet50_vd预训练模型,FPN部分的通道数量设置为64。使用复核过的数据集,训练阶段数据增强策略采用RandomHorizontalFlip、RandomDistort、RandomCrop,并加入背景图片。测试阶段的RPN部分做非极大值抑制计算的候选框数量由原本的6000减少至500、做完非极大值抑制后保留的候选框数量由原本的1000减少至300。模型在验证集上的VOC mAP为87.72%。
+
+在Tesla P40的Linux系统下,对于输入大小是800 x 1333的模型,图像预处理时长为30ms/image,模型的推理时间为46.08ms/image,包括输入数据拷贝至GPU的时间、计算时间、数据拷贝至CPU的时间。
+
+| 模型 | VOC mAP (%) | 推理时间 (ms/image)
+| -- | -- | -- |
+| FasterRCNN-ResNet50_vd_ssld | 81.05 | 48.62 |
+| + dcn | 88.09 | 66.51 |
+| + RandomHorizontalFlip/RandomDistort/RandomCrop | 90.23| 66.51 |
+| + background images | 88.87 | 66.51 |
+| + fpn channel=64 | 87.79 | 48.65 |
+| + test proposal=pre/post topk 500/300 | 87.72 | 46.08 |
+
+具体的训练和部署流程点击文档[GPU端最终解决方案](./gpu_solution.md)进行查看。
+
+## 2. CPU端解决方案
+
+为了实现高效的模型推理,面向CPU端的模型选择精度和效率皆优的单阶段检测模型YOLOv3,骨干网络选择基于PaddleClas中SSLD蒸馏方案训练得到的MobileNetv3_large。训练完成后,对模型做剪裁操作,以提升模型的性能。模型在验证集上的VOC mAP为79.02%。
+
+部署阶段,借助OpenVINO预测引擎完成在Intel(R) Core(TM) i9-9820X CPU @ 3.30GHz Windows系统下高效推理。对于输入大小是608 x 608的模型,图像预处理时长为38.69 ms/image,模型的推理时间为34.50ms/image。
+
+| 模型 | VOC mAP (%) | Inference Speed (ms/image)
+| -- | -- | -- |
+| YOLOv3-MobileNetv3_ssld | 78.52 | 56.71 |
+| pruned YOLOv3-MobileNetv3_ssld | 79.02 | 34.50 |
+
+### 模型训练
+
+[环境前置依赖](./gpu_solution.md#%E5%89%8D%E7%BD%AE%E4%BE%9D%E8%B5%96)、[下载PaddleX源码](./gpu_solution.md#1-%E4%B8%8B%E8%BD%BDpaddlex%E6%BA%90%E7%A0%81)、[下载数据集](./gpu_solution.md#2-%E4%B8%8B%E8%BD%BD%E6%95%B0%E6%8D%AE%E9%9B%86)与GPU端是一样的,可点击文档[GPU端最终解决方案](./gpu_solution.md)查看,在此不做赘述。
+
+如果不想再次训练模型,可以直接下载已经训练好的模型完成后面的模型测试和部署推理:
+
+```
+wget https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/models/yolov3_mobilenetv3_large_pruned.tar.gz
+tar xvf yolov3_mobilenetv3_large_pruned.tar.gz
+```
+
+运行以下代码进行模型训练,代码会自动下载数据集,如若事先下载了数据集,需将下载和解压铝材缺陷检测数据集的相关行注释掉。代码中默认使用0,1,2,3,4,5,6,7号GPU训练,可根据实际情况设置卡号并调整`batch_size`和`learning_rate`。
+
+```
+python train_yolov3.py
+```
+
+### 模型剪裁
+
+运行以下代码,计算在不同的精度损失下,模型各层的剪裁比例:
+
+```
+python cal_sensitivities_file.py
+```
+
+设置可允许的精度损失为0.05,对模型进行剪裁,剪裁后需要重新训练模型:
+
+```
+python train_pruned_yolov3.py
+```
+
+[分析预测错误的原因](./gpu_solution.md#4-%E5%88%86%E6%9E%90%E9%A2%84%E6%B5%8B%E9%94%99%E8%AF%AF%E7%9A%84%E5%8E%9F%E5%9B%A0)、[统计图片级召回率和误检率](./gpu_solution.md#5-%E7%BB%9F%E8%AE%A1%E5%9B%BE%E7%89%87%E7%BA%A7%E5%8F%AC%E5%9B%9E%E7%8E%87%E5%92%8C%E8%AF%AF%E6%A3%80%E7%8E%87)、[模型测试](./gpu_solution.md#6-%E6%A8%A1%E5%9E%8B%E6%B5%8B%E8%AF%95)这些步骤与GPU端是一样的,可点击文档[GPU端最终解决方案](./gpu_solution.md)查看,在此不做赘述。
+
+### 推理部署
+
+本案例采用C++部署方式,通过OpenVINO将模型部署在Intel(R) Core(TM) i9-9820X CPU @ 3.30GHz的Windows系统下,具体的部署流程请参考文档[PaddleX模型多端安全部署/OpenVINO部署](https://paddlex.readthedocs.io/zh_CN/develop/deploy/openvino/index.html)。

+ 93 - 0
examples/industrial_quality_inspection/accuracy_improvement.md

@@ -0,0 +1,93 @@
+# 精度优化
+
+本小节侧重展示在模型迭代过程中优化精度的思路,在本案例中,有些优化策略获得了精度收益,而有些没有。在其他质检场景中,可根据实际情况尝试这些优化策略。
+
+## (1) 基线模型选择
+
+相较于单阶段检测模型,二阶段检测模型的精度更高但是速度更慢。考虑到是部署到GPU端,本案例选择二阶段检测模型FasterRCNN作为基线模型,其骨干网络选择ResNet50_vd,并使用基于PaddleClas中SSLD蒸馏方案训练得到的ResNet50_vd预训练模型(ImageNet1k验证集上Top1 Acc为82.39%)。训练完成后,模型在验证集上的精度VOC mAP为73.36%。
+
+## (2) 模型效果分析
+
+使用PaddleX提供的[paddlex.det.coco_error_analysis](https://paddlex.readthedocs.io/zh_CN/develop/apis/visualize.html#paddlex-det-coco-error-analysis)接口对模型在验证集上预测错误的原因进行分析,分析结果以图表的形式展示如下:
+
+| all classes| 擦花 | 杂色 | 漏底 | 不导电 | 桔皮 | 喷流 | 漆泡 | 起坑 | 脏点 | 角位漏底 |
+| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- |
+|![](https://agroup-bos-bj.cdn.bcebos.com/bj-972007ed33acba896af4aee11cda6abd00ce9ba3)|![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-91790922f4b137880a143f79134391657830c7d2)|![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-46e25934ea0bdc5a7f819bac853883d19e0edc5f)| ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-236eb142601c01ea3f239c771534813ad3fae439) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-a8872384daca0e499fbf0f281980d4a28a89bb97) | ![](https://agroup-bos-bj.cdn.bcebos.com/bj-068e9d36fa6a172215c93bafe00c56d55fb9890d) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-da89bff70e7100002993b4ca7000ba6028b7abf4) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-f1540c077a30b012da41077941e49235e0f844ed) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-44fbd0c3af5c833f80e19b8fee576c1c49464385) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-72dce9276ee20349a09a29aa3967dd79e31b9174) | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-c173f25931c901ade26a2ceab99d8eae5310e0ec) |
+
+分析图表展示了7条Precision-Recall(PR)曲线,每一条曲线表示的Average Precision (AP)比它左边那条高,原因是逐步放宽了评估要求。以擦花类为例,各条PR曲线的评估要求解释如下:
+
+* C75: 在IoU设置为0.75时的PR曲线, AP为0.001。
+* C50: 在IoU设置为0.5时的PR曲线,AP为0.622。C50与C75之间的白色区域面积代表将IoU从0.75放宽至0.5带来的AP增益。
+* Loc: 在IoU设置为0.1时的PR曲线,AP为0.740。Loc与C50之间的蓝色区域面积代表将IoU从0.5放宽至0.1带来的AP增益。蓝色区域面积越大,表示越多的检测框位置不够精准。
+* Sim: 在Loc的基础上,如果检测框与真值框的类别不相同,但两者同属于一个亚类,则不认为该检测框是错误的,在这种评估要求下的PR曲线, AP为0.742。Sim与Loc之间的红色区域面积越大,表示子类间的混淆程度越高。VOC格式的数据集所有的类别都属于同一个亚类。
+* Oth: 在Sim的基础上,如果检测框与真值框的亚类不相同,则不认为该检测框是错误的,在这种评估要求下的PR曲线,AP为0.742。Oth与Sim之间的绿色区域面积越大,表示亚类间的混淆程度越高。VOC格式的数据集中所有的类别都属于同一个亚类,故不存在亚类间的混淆。
+* BG: 在Oth的基础上,背景区域上的检测框不认为是错误的,在这种评估要求下的PR曲线,AP为92.1。BG与Oth之间的紫色区域面积越大,表示背景区域被误检的数量越多。
+* FN: 在BG的基础上,漏检的真值框不认为是错误的,在这种评估要求下的PR曲线,AP为1.00。FN与BG之间的橙色区域面积越大,表示漏检的真值框数量越多。
+
+从分析图表中可以看出,杂色、桔皮、起坑三类检测效果较好,角位漏底存在少许检测框没有达到IoU 0.5的情况,问题较多的是擦花、不导电、喷流、漆泡、脏点。擦花类最严重的问题是误检、位置不精准、漏检,不导电类最严重的问题是漏检、位置不精准,喷流类和漆泡类最严重的问题是位置不精准、误检,脏点类最严重的问题是误检、漏检。为进一步理解造成这些问题的原因,将验证集上的预测结果进行了可视化,然后发现数据集标注存在以下问题:
+
+* 轻微的缺陷不视为缺陷,但轻微的界定不明确,有些轻微的缺陷被标注了,造成误检较多
+* 不导电、漏底、角位漏底外观极其相似,肉眼难以区分,导致这三类极其容易混淆而使得评估时误检或漏检的产生
+* 有些轻微的擦花和脏点被标注了,有些明显的反而没有被标注,造成了这两类误检和漏检情况都较为严重
+* 喷流和漆泡多为连续性的缺陷,一处喷流,其水平线上还会有其他喷流,一个气泡,其水平线上还会有一排气泡。但有时候把这些连续性的缺陷标注成一个目标,有时候又单独地标注不同的部分。导致模型有时候检测单独部分,有时候又检测整体,造成这两类位置不精准、误检较多。
+
+## (3) 数据复核
+
+为了减少原始数据标注的诸多问题对模型优化的影响,需要对数据进行复核。复核准则示例如下:
+
+* 擦花:不明显的擦花不标注,面状擦花以同一个框表示,条状擦花一条擦花以一个框表示
+* 漏底、角位漏底、不导电:三者过于相似,归为一类
+* 桔皮:忽略不是大颗粒状的表面
+* 喷流:明显是一条喷流的就用一个框表示,不是的话用多个框表示
+* 漆泡:不要单独标一个点,一连串点标一个框
+* 脏点:忽略轻微脏点
+
+对数据集复核并重新标注后,将FasterRCNN-ResNet50_vd_ssld重新在训练集上进行训练,模型在验证集上的VOC mAP为81.05%。
+
+## (4) 可变形卷积加入
+
+由于喷流、漆泡的形态不规则,导致这两类的很多预测框位置不精准。为了解决该问题,选择在骨干网络ResNet50_vd中使用可变形卷积(DCN)。重新训练后,模型在验证集上的VOC mAP为88.09%,喷流的VOC AP由57.3%提升至78.7%,漆泡的VOC AP由74.7%提升至96.7%。
+
+## (5) 数据增强选择
+
+在(4)的基础上,选择加入一些数据增强策略来进一步提升模型的精度。本案例选择同时使用[RandomHorizontalFlip](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#randomhorizontalflip)、[RandomDistort](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#randomdistort)、[RandomCrop](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#randomcrop)这三种数据增强方法,重新训练后的模型在验证集上的VOC mAP为90.23%。
+
+除此之外,还可以尝试的数据增强方式有[MultiScaleTraining](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#resizebyshort)、[RandomExpand](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#randomexpand)。本案例使用的铝材表面缺陷检测数据集中,同一个类别的尺度变化不大,使用MultiScaleTraining或者RandomExpand反而使得原始数据分布发生改变。对此,本案例也进行了实验验证,使用RandomHorizontalFlip + RandomDistort + RandomCrop + MultiScaleTraining数据增强方式训练得到的模型在验证集上的VOC mAP为87.15%,使用RandomHorizontalFlip + RandomDistort + RandomCrop + RandomExpand数据增强方式训练得到的模型在验证集上的的VOC mAP为88.56%。
+
+## (6) 背景图片加入
+
+本案例将数据集中提供的背景图片按9:1切分成了1116张、135张两部分,并使用(5)中训练好的模型在135张背景图片上进行测试,发现图片级误检率高达21.5%。为了降低模型的误检率,使用[paddlex.datasets.VOCDetection.add_negative_samples](https://paddlex.readthedocs.io/zh_CN/develop/apis/datasets.html#add-negative-samples)接口将1116张背景图片加入到原本的训练集中,重新训练后图片级误检率降低至4%。为了不让训练被背景图片主导,本案例通过将`train_list.txt`中的文件路径多写了一遍,从而增加有目标图片的占比。
+
+| 模型 | VOC mAP (%) | 有缺陷图片级召回率 | 背景图片级误检率 |
+| -- | -- | -- | -- |
+| FasterRCNN-ResNet50_vd_ssld + DCN + RandomHorizontalFlip + RandomDistort + RandomCrop | 90.23 | 95.5 | 21.5 |
+| FasterRCNN-ResNet50_vd_ssld + DCN + RandomHorizontalFlip + RandomDistort + RandomCrop + 背景图片 | 88.87 | 95.2 | 4 |
+
+【名词解释】
+
+* 图片级别的召回率:只要在有目标的图片上检测出目标(不论框的个数),该图片被认为召回。批量有目标图片中被召回图片所占的比例,即为图片级别的召回率。
+* 图片级别的误检率:只要在无目标的图片上检测出目标(不论框的个数),该图片被认为误检。批量无目标图片中被误检图片所占的比例,即为图片级别的误检率。
+
+## (7) 分类损失函数选择
+
+降低误检率的措施除了(6)中提到的将背景图片加入训练,还可以将RPN部分的分类损失函数选择为`SigmoidFocalLoss`,将更多的anchors加入训练,增加难分样本的在损失函数的比重进而降低误检率。在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时将参数`rpn_cls_loss`设置为'SigmoidFocalLoss',同时需要调整参数`rpn_focal_loss_alpha`、`rpn_focal_loss_gamma`、`rpn_batch_size_per_im`、`rpn_fg_fraction`的设置。
+
+## (8) 位置回归损失函数选择
+
+RCNN部分的位置回归损失函数除了'SmoothL1Loss'以外,还可以选择'CIoULoss',使用方式在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时设置参数`rcnn_bbox_loss`即可。在本案例中,选择'CIoULoss'并没有带来精度收益,故还是选择'SmoothL1Loss'。其他质检场景下,也可尝试使用'CIoULoss'。
+
+## (9) 正负样本采样方式选择
+
+当目标物体的区域只占图像的一小部分时,可以考虑采用[LibraRCNN](https://arxiv.org/abs/1904.02701)中提出的IoU-balanced Sampling采样方式来获取更多的难分负样本。使用方式在定义模型[FasterRCNN](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn)类时将参数`bbox_assigner`设置为'LibraBBoxAssigner'即可。
+
+## (10) 预处理对比度增强
+
+工业界常用灰度相机采集图片,会存在目标与周围背景对比度不明显而无法被检测出的情况。在这种情况下,可以在定义预处理的时候使用[paddlex.det.transforms.CLAHE](https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html#clahe)对灰度图像的对比度进行增强。
+
+| 灰度图 | 对比度增加后的灰度图 |
+| -- | -- |
+| ![](./image/before_clahe.png) | ![](./image/after_clahe.png) |
+
+## (11) 样本生成
+
+对于数量较少的类别或者小目标,可以通过把这些目标物体粘贴在背景图片上来生成新的图片和标注文件,并把这些新的样本加入到训练中从而提升模型精度。目前PaddleX提供了实现该功能的接口,详细见[paddlex.det.paste_objects](https://paddlex.readthedocs.io/zh_CN/develop/apis/tools.html#paddlex-det-paste-objects),需要注意的是,前景目标颜色与背景颜色差异较大时生成的新图片才会比较逼真。

+ 56 - 0
examples/industrial_quality_inspection/cal_sensitivities_file.py

@@ -0,0 +1,56 @@
+#copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
+#
+#Licensed under the Apache License, Version 2.0 (the "License");
+#you may not use this file except in compliance with the License.
+#You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#Unless required by applicable law or agreed to in writing, software
+#distributed under the License is distributed on an "AS IS" BASIS,
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#See the License for the specific language governing permissions and
+#limitations under the License.
+
+import argparse
+import os
+# 选择使用0号卡
+os.environ['CUDA_VISIBLE_DEVICES'] = '0'
+import paddlex as pdx
+
+
+def cal_sensitivies_file(model_dir, dataset, save_file):
+    # 加载模型
+    model = pdx.load_model(model_dir)
+
+    # 定义验证所用的数据集
+    eval_dataset = pdx.datasets.VOCDetection(
+        data_dir=dataset,
+        file_list=os.path.join(dataset, 'val_list.txt'),
+        label_list=os.path.join(dataset, 'labels.txt'),
+        transforms=model.eval_transforms)
+
+    pdx.slim.cal_params_sensitivities(
+        model, save_file, eval_dataset, batch_size=8)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "--model_dir",
+        default="./output/yolov3_mobilenetv3/best_model",
+        type=str,
+        help="The model path.")
+    parser.add_argument(
+        "--dataset",
+        default="./aluminum_inspection",
+        type=str,
+        help="The model path.")
+    parser.add_argument(
+        "--save_file",
+        default="./sensitivities.data",
+        type=str,
+        help="The sensitivities file path.")
+
+    args = parser.parse_args()
+    cal_sensitivies_file(args.model_dir, args.dataset, args.save_file)

+ 126 - 0
examples/industrial_quality_inspection/cal_tp_fp.py

@@ -0,0 +1,126 @@
+# coding: utf8
+# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 环境变量配置,用于控制是否使用GPU
+# 说明文档:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html#gpu
+import os
+os.environ['CUDA_VISIBLE_DEVICES'] = '0'
+
+import os.path as osp
+import numpy as np
+import matplotlib
+matplotlib.use('Agg')
+import matplotlib.pyplot as plt
+import paddlex as pdx
+
+data_dir = 'aluminum_inspection/'
+positive_file_list = 'aluminum_inspection/val_list.txt'
+negative_dir = 'aluminum_inspection/val_wu_xia_ci'
+model_dir = 'output/faster_rcnn_r50_vd_dcn/best_model/'
+save_dir = 'visualize/faster_rcnn_r50_vd_dcn'
+if not osp.exists(save_dir):
+    os.makedirs(save_dir)
+
+tp = np.zeros((101, 1))
+fp = np.zeros((101, 1))
+
+# 导入模型
+model = pdx.load_model(model_dir)
+
+# 计算图片级召回率
+print(
+    "Begin to calculate image-level recall rate of positive images. Please wait for a moment..."
+)
+positive_num = 0
+with open(positive_file_list, 'r') as fr:
+    while True:
+        line = fr.readline()
+        if not line:
+            break
+        img_file, xml_file = [osp.join(data_dir, x) \
+                for x in line.strip().split()[:2]]
+        if not osp.exists(img_file):
+            continue
+        if not osp.exists(xml_file):
+            continue
+
+        positive_num += 1
+        results = model.predict(img_file)
+        scores = list()
+        for res in results:
+            scores.append(res['score'])
+        if len(scores) > 0:
+            tp[0:int(np.round(max(scores) / 0.01)), 0] += 1
+tp = tp / positive_num
+
+# 计算图片级误检率
+print(
+    "Begin to calculate image-level false-positive rate of background images. Please wait for a moment..."
+)
+negative_num = 0
+for file in os.listdir(negative_dir):
+    file = osp.join(negative_dir, file)
+    results = model.predict(file)
+    negative_num += 1
+    scores = list()
+    for res in results:
+        scores.append(res['score'])
+    if len(scores) > 0:
+        fp[0:int(np.round(max(scores) / 0.01)), 0] += 1
+fp = fp / negative_num
+
+# 保存结果
+tp_fp_list_file = osp.join(save_dir, 'tp_fp_list.txt')
+with open(tp_fp_list_file, 'w') as f:
+    f.write("| score | recall rate | false-positive rate |\n")
+    f.write("| -- | -- | -- |\n")
+    for i in range(100):
+        f.write("| {:2f} | {:2f} | {:2f} |\n".format(0.01 * i, tp[i, 0], fp[
+            i, 0]))
+print("The numerical score-recall_rate-false_positive_rate is saved as {}".
+      format(tp_fp_list_file))
+
+plt.subplot(1, 2, 1)
+plt.title("image-level false_positive-recall")
+plt.xlabel("recall")
+plt.ylabel("false_positive")
+plt.xlim(0, 1)
+plt.ylim(0, 1)
+plt.grid(linestyle='--', linewidth=1)
+plt.plot([0, 1], [0, 1], 'r--', linewidth=1)
+my_x_ticks = np.arange(0, 1, 0.1)
+my_y_ticks = np.arange(0, 1, 0.1)
+plt.xticks(my_x_ticks, fontsize=5)
+plt.yticks(my_y_ticks, fontsize=5)
+plt.plot(tp, fp, color='b', label="image level", linewidth=1)
+plt.legend(loc="lower left", fontsize=5)
+
+plt.subplot(1, 2, 2)
+plt.title("score-recall")
+plt.xlabel('recall')
+plt.ylabel('score')
+plt.xlim(0, 1)
+plt.ylim(0, 1)
+plt.grid(linestyle='--', linewidth=1)
+plt.xticks(my_x_ticks, fontsize=5)
+plt.yticks(my_y_ticks, fontsize=5)
+plt.plot(
+    tp, np.arange(0, 1.01, 0.01), color='b', label="image level", linewidth=1)
+plt.legend(loc="lower left", fontsize=5)
+tp_fp_chart_file = os.path.join(save_dir, "image-level_tp_fp.png")
+plt.savefig(tp_fp_chart_file, dpi=800)
+plt.close()
+print("The diagrammatic score-recall_rate-false_positive_rate is saved as {}".
+      format(tp_fp_chart_file))

+ 132 - 0
examples/industrial_quality_inspection/compare.py

@@ -0,0 +1,132 @@
+# coding: utf8
+# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 环境变量配置,用于控制是否使用GPU
+# 说明文档:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html#gpu
+import os
+os.environ['CUDA_VISIBLE_DEVICES'] = '0'
+
+import os.path as osp
+import cv2
+import re
+import xml.etree.ElementTree as ET
+import paddlex as pdx
+
+data_dir = 'aluminum_inspection/'
+file_list = 'aluminum_inspection/val_list.txt'
+model_dir = 'output/faster_rcnn_r50_vd_dcn/best_model/'
+save_dir = './visualize/compare'
+# 设置置信度阈值
+score_threshold = 0.1
+
+if not os.path.exists(save_dir):
+    os.makedirs(save_dir)
+
+model = pdx.load_model(model_dir)
+
+with open(file_list, 'r') as fr:
+    while True:
+        line = fr.readline()
+        if not line:
+            break
+        img_file, xml_file = [osp.join(data_dir, x) \
+                for x in line.strip().split()[:2]]
+        if not osp.exists(img_file):
+            continue
+        if not osp.exists(xml_file):
+            continue
+
+        res = model.predict(img_file)
+        det_vis = pdx.det.visualize(
+            img_file, res, threshold=score_threshold, save_dir=None)
+
+        tree = ET.parse(xml_file)
+        pattern = re.compile('<object>', re.IGNORECASE)
+        obj_match = pattern.findall(str(ET.tostringlist(tree.getroot())))
+        if len(obj_match) == 0:
+            continue
+        obj_tag = obj_match[0][1:-1]
+        objs = tree.findall(obj_tag)
+        pattern = re.compile('<size>', re.IGNORECASE)
+        size_tag = pattern.findall(str(ET.tostringlist(tree.getroot())))[0][1:
+                                                                            -1]
+        size_element = tree.find(size_tag)
+        pattern = re.compile('<width>', re.IGNORECASE)
+        width_tag = pattern.findall(str(ET.tostringlist(size_element)))[0][1:
+                                                                           -1]
+        im_w = float(size_element.find(width_tag).text)
+        pattern = re.compile('<height>', re.IGNORECASE)
+        height_tag = pattern.findall(str(ET.tostringlist(size_element)))[0][1:
+                                                                            -1]
+        im_h = float(size_element.find(height_tag).text)
+        gt_bbox = []
+        gt_class = []
+        for i, obj in enumerate(objs):
+            pattern = re.compile('<name>', re.IGNORECASE)
+            name_tag = pattern.findall(str(ET.tostringlist(obj)))[0][1:-1]
+            cname = obj.find(name_tag).text.strip()
+            gt_class.append(cname)
+            pattern = re.compile('<difficult>', re.IGNORECASE)
+            diff_tag = pattern.findall(str(ET.tostringlist(obj)))[0][1:-1]
+            try:
+                _difficult = int(obj.find(diff_tag).text)
+            except Exception:
+                _difficult = 0
+            pattern = re.compile('<bndbox>', re.IGNORECASE)
+            box_tag = pattern.findall(str(ET.tostringlist(obj)))[0][1:-1]
+            box_element = obj.find(box_tag)
+            pattern = re.compile('<xmin>', re.IGNORECASE)
+            xmin_tag = pattern.findall(str(ET.tostringlist(box_element)))[0][
+                1:-1]
+            x1 = float(box_element.find(xmin_tag).text)
+            pattern = re.compile('<ymin>', re.IGNORECASE)
+            ymin_tag = pattern.findall(str(ET.tostringlist(box_element)))[0][
+                1:-1]
+            y1 = float(box_element.find(ymin_tag).text)
+            pattern = re.compile('<xmax>', re.IGNORECASE)
+            xmax_tag = pattern.findall(str(ET.tostringlist(box_element)))[0][
+                1:-1]
+            x2 = float(box_element.find(xmax_tag).text)
+            pattern = re.compile('<ymax>', re.IGNORECASE)
+            ymax_tag = pattern.findall(str(ET.tostringlist(box_element)))[0][
+                1:-1]
+            y2 = float(box_element.find(ymax_tag).text)
+            x1 = max(0, x1)
+            y1 = max(0, y1)
+            if im_w > 0.5 and im_h > 0.5:
+                x2 = min(im_w - 1, x2)
+                y2 = min(im_h - 1, y2)
+            gt_bbox.append([x1, y1, x2, y2])
+        gts = []
+        for bbox, name in zip(gt_bbox, gt_class):
+            x1, y1, x2, y2 = bbox
+            w = x2 - x1 + 1
+            h = y2 - y1 + 1
+            gt = {
+                'category_id': 0,
+                'category': name,
+                'bbox': [x1, y1, w, h],
+                'score': 1
+            }
+            gts.append(gt)
+        gt_vis = pdx.det.visualize(
+            img_file, gts, threshold=score_threshold, save_dir=None)
+        vis = cv2.hconcat([gt_vis, det_vis])
+        cv2.imwrite(os.path.join(save_dir, os.path.split(img_file)[-1]), vis)
+        print('The comparison has been made for {}'.format(img_file))
+
+print(
+    "The visualized ground-truths and predictions are saved in {}. Ground-truth is on the left, prediciton is on the right".
+    format(save_dir))

+ 14 - 0
examples/industrial_quality_inspection/dataset.md

@@ -0,0 +1,14 @@
+# 天池铝材表面缺陷检测初赛数据集示例
+
+| 序号 | 瑕疵名称 | 瑕疵成因 | 瑕疵图片示例 | 图片数量 |
+| -- | -- | -- | -- | -- |
+| 1 | 擦花(擦伤)| 表面处理(喷涂)后有轻微擦到其它的东西,造成痕迹 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-be236181808cd68e3ad941efdc16f11d4a04dd35) | 128 |
+| 2 | 杂色 | 喷涂换颜料的时候,装颜料的容器未清洗干净,造成喷涂时有少量其它颜色掺入 |![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-58d7fe6d1a0b72cfd735fa9e192f4f04e58c0901) |365 |
+| 3 | 漏底 | 喷粉效果不好,铝材大量底色露出 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-16d7005e897900aa916ea639cc49810fe77fa982) | 538 |
+| 4 | 不导电 | 直接喷不到铝材表面上去 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-3c886ff349280c22796a46f296fedd3296ae4120) | 390 |
+|5 | 桔皮 | 表面处理后涂层表面粗糙,大颗粒 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-726ce1ab1ac7aa47dd4c8bff89df85b4e3b7ae4d) | 173 |
+| 6 | 喷流| 喷涂时油漆稀从上流下来,有流动痕迹 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-1b245e25169d618a083669acde2c293bb548bea6) | 86 |
+| 7 |漆泡 | 喷涂后表面起泡,小而多| ![](https://agroup-bos-bj.cdn.bcebos.com/bj-00ed3f730ce41f7f18a4d6a5402fd3e61bfa0db9) | 82 |
+| 8 | 起坑 | 型材模具问题,做出来的型材一整条都有一条凹下去的部分 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-739abb8a6d9144560290cd8f2eaa05b3aa183e17) | 407 |
+| 9 | 脏点 | 表面处理时,有灰尘或一些脏东西未能擦掉,导致涂层有颗粒比较突出 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-487a749bfbb149294f540cc3710aacee30e154f2) | 261 |
+| 10 | 角位漏底 | 在型材角落出现的露底 | ![图片](https://agroup-bos-bj.cdn.bcebos.com/bj-91a9fe0f3a69f1b4ab6006bae870e5f687fab111) | 346 |

+ 26 - 0
examples/industrial_quality_inspection/error_analysis.py

@@ -0,0 +1,26 @@
+# coding: utf8
+# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import os.path as osp
+import paddlex as pdx
+
+model_dir = 'output/faster_rcnn_r50_vd_dcn/best_model/'
+save_dir = 'visualize/faster_rcnn_r50_vd_dcn'
+if not osp.exists(save_dir):
+    os.makedirs(save_dir)
+
+eval_details_file = osp.join(model_dir, 'eval_details.json')
+pdx.det.coco_error_analysis(eval_details_file, save_dir=save_dir)

+ 114 - 0
examples/industrial_quality_inspection/gpu_solution.md

@@ -0,0 +1,114 @@
+# GPU端最终解决方案
+
+本案例面向GPU端的最终方案是选择二阶段检测模型FasterRCNN,其骨干网络选择加入了可变形卷积(DCN)的ResNet50_vd,训练时使用SSLD蒸馏方案训练得到的ResNet50_vd预训练模型,FPN部分的通道数量设置为64,训练阶段数据增强策略采用RandomHorizontalFlip、RandomDistort、RandomCrop,并加入背景图片,测试阶段的RPN部分做非极大值抑制计算的候选框数量由原本的6000减少至500、做完非极大值抑制后保留的候选框数量由原本的1000减少至300。
+
+在Tesla P40的Linux系统下,对于输入大小是800 x 1333的模型,图像预处理时长为30ms/image,模型的推理时间为46.08ms/image,包括输入数据拷贝至GPU的时间、计算时间、数据拷贝至CPU的时间。
+
+| 模型 | VOC mAP (%) | 推理时间 (ms/image)
+| -- | -- | -- |
+| FasterRCNN-ResNet50_vd_ssld | 81.05 | 48.62 |
+| + dcn | 88.09 | 66.51 |
+| + RandomHorizontalFlip/RandomDistort/RandomCrop | 90.23| 66.51 |
+| + background images | 88.87 | 66.51 |
+| + fpn channel=64 | 87.79 | 48.65 |
+| + test proposal=pre/post topk 500/300 | 87.72 | 46.08 |
+
+## 前置依赖
+
+* Paddle paddle >= 1.8.0
+* Python >= 3.5
+* PaddleX >= 1.3.0
+
+## 模型训练
+
+### (1) 下载PaddleX源码
+
+```
+git clone https://github.com/PaddlePaddle/PaddleX
+
+cd PaddleX/examples/industrial_quality_inspection
+```
+
+### (2) 下载数据集
+
+因数据集较大,可运行以下代码提前将数据集下载并解压。训练代码中也会自动下载数据集,所以这一步不是必须的。
+
+```
+wget https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/datasets/aluminum_inspection.tar.gz
+tar xvf aluminum_inspection.tar.gz
+```
+### (3) 下载预先训练好的模型
+
+如果不想再次训练模型,可以直接下载已经训练好的模型完成后面的模型测试和部署推理:
+
+```
+wget https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/models/faster_rcnn_r50_vd_dcn.tar.gz
+tar xvf faster_rcnn_r50_vd_dcn.tar.gz
+```
+### (4) 训练
+
+运行以下代码进行模型训练,代码会自动下载数据集,如若事先下载了数据集,需将下载和解压铝材缺陷检测数据集的相关行注释掉。代码中默认使用0,1,2,3,4号GPU训练,可根据实际情况设置卡号并调整`batch_size`和`learning_rate`。
+
+```
+python train_rcnn.py
+```
+
+### (5) 分析预测错误的原因
+
+在模型迭代过程中,运行以下代码可完成模型效果的分析并生成分析结果图表:
+
+```
+python error_analysis.py
+```
+
+可参考[性能优化部分的模型效果分析](./accuracy_improvement.md#2-%E6%A8%A1%E5%9E%8B%E6%95%88%E6%9E%9C%E5%88%86%E6%9E%90)来理解当前模型预测错误的原因。
+
+运行以下代码,生成可视化真值和预测结果的对比图以进一步理解模型效果,代码中的置信度阈值可根据实际情况进行调整。
+
+```
+python compare.py
+```
+
+![](image/compare_budaodian-116.jpg)
+
+左边是可视化真值,右边是可视化预测结果。
+
+### (6) 统计图片级召回率和误检率
+
+模型迭代完成后,计算不同置信度阈值下[图片级召回率](./accuracy_improvement.md#6-%E8%83%8C%E6%99%AF%E5%9B%BE%E7%89%87%E5%8A%A0%E5%85%A5)和[图片级误检率](./accuracy_improvement.md#6-%E8%83%8C%E6%99%AF%E5%9B%BE%E7%89%87%E5%8A%A0%E5%85%A5),找到符合要求的召回率和误检率,对应的置信度阈值用于后续模型预测阶段。
+
+```
+python cal_tp_fp.py
+```
+
+执行后会生成图表`image-level_tp_fp.png`和文件`tp_fp_list.txt`,示意如下:
+
+| 图表`image-level_tp_fp.png` | 文件`tp_fp_list.txt` |
+| -- | -- |
+| ![](./image/image-level_tp_fp.png) | [tp_fp_list.txt](tp_fp_list.md) |
+
+图表`image-level_tp_fp.png`中左边子图,横坐标表示不同置信度阈值下计算得到的图片级召回率,纵坐标表示各图片级召回率对应的图片级误检率。右边子图横坐标表示图片级召回率,纵坐标表示各图片级召回率对应的置信度阈值。从图表中可较为直观地看出当前模型的图片级召回率和误检率的量级,从文件`tp_fp_list.txt`可以查到具体数值,例如在图片级召回率/图片级误检率为[0.9589,0.0074]这一组符合要求,就将对应的置信度阈值0.90选取为后续预测推理的阈值。
+
+### (7) 模型测试
+
+测试集因没有标注文件,这里单独下载测试集图片:
+
+```
+wget https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/datasets/aluminum_inspection_test.tar.gz
+tar xvf aluminum_inspection_test.tar.gz
+```
+
+加载训练好的模型,使用(5)选取的置信度阈值0.90对验证集图片或者测试集图片进行预测:
+
+```
+python predict.py
+```
+可视化预测结果示例如下:
+
+![](image/visualize_budaodian-116.jpg)
+
+## 推理部署
+
+本案例采用C++部署方式将模型部署在Tesla P40的Linux系统下,具体的C++部署流程请参考文档[PaddleX模型多端安全部署/C++部署](https://paddlex.readthedocs.io/zh_CN/develop/deploy/server/cpp/index.html)。
+
+对于输入大小是800 x 1333的模型,图像预处理时长为30ms/image。值得一提的是预处理中的Normalize操作比较耗时,因此在设置预处理操作时,可以先进行Resize操作再做Normalize。模型的推理时间为46.08ms/image,包括输入数据拷贝至GPU的时间、计算时间、数据拷贝至CPU的时间。

BIN
examples/industrial_quality_inspection/image/after_clahe.png


BIN
examples/industrial_quality_inspection/image/before_clahe.png


BIN
examples/industrial_quality_inspection/image/compare_budaodian-116.jpg


BIN
examples/industrial_quality_inspection/image/image-level_tp_fp.png


BIN
examples/industrial_quality_inspection/image/visualize_budaodian-116.jpg


+ 36 - 0
examples/industrial_quality_inspection/predict.py

@@ -0,0 +1,36 @@
+# coding: utf8
+# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 环境变量配置,用于控制是否使用GPU
+# 说明文档:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html#gpu
+import os
+os.environ['CUDA_VISIBLE_DEVICES'] = '0'
+
+import os.path as osp
+import paddlex as pdx
+
+img_file = 'aluminum_inspection/JPEGImages/budaodian-116.jpg'
+model_dir = 'output/faster_rcnn_r50_vd_dcn/best_model/'
+save_dir = './visualize/predict'
+# 设置置信度阈值
+score_threshold = 0.9
+
+if not os.path.exists(save_dir):
+    os.makedirs(save_dir)
+
+model = pdx.load_model(model_dir)
+res = model.predict(img_file)
+det_vis = pdx.det.visualize(
+    img_file, res, threshold=score_threshold, save_dir=save_dir)

+ 102 - 0
examples/industrial_quality_inspection/tp_fp_list.md

@@ -0,0 +1,102 @@
+| score | recall rate | false-positive rate |
+| -- | -- | -- |
+| 0.000000 | 0.982877 | 0.022222 |
+| 0.010000 | 0.982877 | 0.022222 |
+| 0.020000 | 0.982877 | 0.022222 |
+| 0.030000 | 0.982877 | 0.022222 |
+| 0.040000 | 0.982877 | 0.022222 |
+| 0.050000 | 0.982877 | 0.022222 |
+| 0.060000 | 0.982877 | 0.022222 |
+| 0.070000 | 0.979452 | 0.022222 |
+| 0.080000 | 0.979452 | 0.022222 |
+| 0.090000 | 0.979452 | 0.022222 |
+| 0.100000 | 0.979452 | 0.022222 |
+| 0.110000 | 0.979452 | 0.022222 |
+| 0.120000 | 0.979452 | 0.022222 |
+| 0.130000 | 0.979452 | 0.022222 |
+| 0.140000 | 0.976027 | 0.022222 |
+| 0.150000 | 0.976027 | 0.022222 |
+| 0.160000 | 0.976027 | 0.022222 |
+| 0.170000 | 0.976027 | 0.022222 |
+| 0.180000 | 0.976027 | 0.022222 |
+| 0.190000 | 0.976027 | 0.022222 |
+| 0.200000 | 0.976027 | 0.022222 |
+| 0.210000 | 0.976027 | 0.022222 |
+| 0.220000 | 0.976027 | 0.022222 |
+| 0.230000 | 0.976027 | 0.022222 |
+| 0.240000 | 0.976027 | 0.022222 |
+| 0.250000 | 0.976027 | 0.022222 |
+| 0.260000 | 0.976027 | 0.014815 |
+| 0.270000 | 0.976027 | 0.014815 |
+| 0.280000 | 0.976027 | 0.014815 |
+| 0.290000 | 0.976027 | 0.014815 |
+| 0.300000 | 0.976027 | 0.014815 |
+| 0.310000 | 0.976027 | 0.014815 |
+| 0.320000 | 0.976027 | 0.014815 |
+| 0.330000 | 0.976027 | 0.014815 |
+| 0.340000 | 0.976027 | 0.014815 |
+| 0.350000 | 0.976027 | 0.014815 |
+| 0.360000 | 0.976027 | 0.014815 |
+| 0.370000 | 0.976027 | 0.014815 |
+| 0.380000 | 0.976027 | 0.014815 |
+| 0.390000 | 0.976027 | 0.014815 |
+| 0.400000 | 0.976027 | 0.014815 |
+| 0.410000 | 0.976027 | 0.014815 |
+| 0.420000 | 0.976027 | 0.014815 |
+| 0.430000 | 0.976027 | 0.014815 |
+| 0.440000 | 0.972603 | 0.014815 |
+| 0.450000 | 0.972603 | 0.014815 |
+| 0.460000 | 0.972603 | 0.014815 |
+| 0.470000 | 0.972603 | 0.014815 |
+| 0.480000 | 0.972603 | 0.014815 |
+| 0.490000 | 0.972603 | 0.014815 |
+| 0.500000 | 0.972603 | 0.014815 |
+| 0.510000 | 0.972603 | 0.014815 |
+| 0.520000 | 0.972603 | 0.014815 |
+| 0.530000 | 0.972603 | 0.014815 |
+| 0.540000 | 0.972603 | 0.014815 |
+| 0.550000 | 0.972603 | 0.014815 |
+| 0.560000 | 0.969178 | 0.014815 |
+| 0.570000 | 0.969178 | 0.014815 |
+| 0.580000 | 0.969178 | 0.014815 |
+| 0.590000 | 0.969178 | 0.014815 |
+| 0.600000 | 0.969178 | 0.014815 |
+| 0.610000 | 0.969178 | 0.014815 |
+| 0.620000 | 0.969178 | 0.014815 |
+| 0.630000 | 0.969178 | 0.014815 |
+| 0.640000 | 0.969178 | 0.014815 |
+| 0.650000 | 0.969178 | 0.014815 |
+| 0.660000 | 0.969178 | 0.014815 |
+| 0.670000 | 0.969178 | 0.014815 |
+| 0.680000 | 0.969178 | 0.014815 |
+| 0.690000 | 0.969178 | 0.014815 |
+| 0.700000 | 0.969178 | 0.014815 |
+| 0.710000 | 0.969178 | 0.014815 |
+| 0.720000 | 0.969178 | 0.014815 |
+| 0.730000 | 0.969178 | 0.014815 |
+| 0.740000 | 0.969178 | 0.014815 |
+| 0.750000 | 0.969178 | 0.014815 |
+| 0.760000 | 0.969178 | 0.014815 |
+| 0.770000 | 0.965753 | 0.014815 |
+| 0.780000 | 0.965753 | 0.014815 |
+| 0.790000 | 0.965753 | 0.014815 |
+| 0.800000 | 0.962329 | 0.014815 |
+| 0.810000 | 0.962329 | 0.014815 |
+| 0.820000 | 0.962329 | 0.014815 |
+| 0.830000 | 0.962329 | 0.014815 |
+| 0.840000 | 0.962329 | 0.014815 |
+| 0.850000 | 0.958904 | 0.014815 |
+| 0.860000 | 0.958904 | 0.014815 |
+| 0.870000 | 0.958904 | 0.014815 |
+| 0.880000 | 0.958904 | 0.014815 |
+| 0.890000 | 0.958904 | 0.014815 |
+| 0.900000 | 0.958904 | 0.007407 |
+| 0.910000 | 0.958904 | 0.007407 |
+| 0.920000 | 0.958904 | 0.007407 |
+| 0.930000 | 0.955479 | 0.007407 |
+| 0.940000 | 0.955479 | 0.007407 |
+| 0.950000 | 0.955479 | 0.007407 |
+| 0.960000 | 0.955479 | 0.007407 |
+| 0.970000 | 0.955479 | 0.007407 |
+| 0.980000 | 0.941781 | 0.000000 |
+| 0.990000 | 0.893836 | 0.000000 |

+ 74 - 0
examples/industrial_quality_inspection/train_rcnn.py

@@ -0,0 +1,74 @@
+# 环境变量配置,用于控制是否使用GPU
+# 说明文档:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html#gpu
+import os
+os.environ['CUDA_VISIBLE_DEVICES'] = '0,1,2,3,4'
+
+from paddlex.det import transforms
+import paddlex as pdx
+
+# 下载和解压铝材缺陷检测数据集
+aluminum_dataset = 'https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/datasets/aluminum_inspection.tar.gz'
+pdx.utils.download_and_decompress(aluminum_dataset, path='./')
+
+# API说明 https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html
+train_transforms = transforms.Compose([
+    transforms.RandomDistort(), transforms.RandomCrop(),
+    transforms.RandomHorizontalFlip(), transforms.ResizeByShort(
+        short_size=[800], max_size=1333), transforms.Normalize(
+            mean=[0.5], std=[0.5]), transforms.Padding(coarsest_stride=32)
+])
+
+eval_transforms = transforms.Compose([
+    transforms.ResizeByShort(
+        short_size=800, max_size=1333),
+    transforms.Normalize(),
+    transforms.Padding(coarsest_stride=32),
+])
+
+# 定义训练和验证所用的数据集
+# API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/datasets.html#paddlex-datasets-vocdetection
+train_dataset = pdx.datasets.VOCDetection(
+    data_dir='aluminum_inspection',
+    file_list='aluminum_inspection/train_list.txt',
+    label_list='aluminum_inspection/labels.txt',
+    transforms=train_transforms,
+    num_workers=8,
+    shuffle=True)
+eval_dataset = pdx.datasets.VOCDetection(
+    data_dir='aluminum_inspection',
+    file_list='aluminum_inspection/val_list.txt',
+    label_list='aluminum_inspection/labels.txt',
+    num_workers=8,
+    transforms=eval_transforms)
+
+# 把背景图片加入训练集中
+train_dataset.add_negative_samples(
+    image_dir='./aluminum_inspection/train_wu_xia_ci')
+
+# 初始化模型,并进行训练
+# 可使用VisualDL查看训练指标,参考https://paddlex.readthedocs.io/zh_CN/develop/train/visualdl.html
+# num_classes 需要设置为包含背景类的类别数,即: 目标类别数量 + 1
+num_classes = len(train_dataset.labels) + 1
+
+# API说明: https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-fasterrcnn
+model = pdx.det.FasterRCNN(
+    num_classes=num_classes,
+    backbone='ResNet50_vd_ssld',
+    with_dcn=True,
+    fpn_num_channels=64,
+    with_fpn=True,
+    test_pre_nms_top_n=500,
+    test_post_nms_top_n=300)
+
+# API说明: https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#id1
+# 各参数介绍与调整说明:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html
+model.train(
+    num_epochs=80,
+    train_dataset=train_dataset,
+    train_batch_size=10,
+    eval_dataset=eval_dataset,
+    learning_rate=0.0125,
+    lr_decay_epochs=[60, 70],
+    warmup_steps=1000,
+    save_dir='output/faster_rcnn_r50_vd_dcn',
+    use_vdl=True)

+ 58 - 0
examples/industrial_quality_inspection/train_yolov3.py

@@ -0,0 +1,58 @@
+# 环境变量配置,用于控制是否使用GPU
+# 说明文档:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html#gpu
+import os
+os.environ['CUDA_VISIBLE_DEVICES'] = '0,1,2,3,4,5,6,7'
+
+from paddlex.det import transforms
+import paddlex as pdx
+
+# 下载和解压铝材缺陷检测数据集
+aluminum_dataset = 'https://bj.bcebos.com/paddlex/examples/industrial_quality_inspection/datasets/aluminum_inspection.tar.gz'
+pdx.utils.download_and_decompress(aluminum_dataset, path='./')
+
+# 定义训练和验证时的transforms
+# API说明 https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/det_transforms.html
+train_transforms = transforms.Compose([
+    transforms.MixupImage(mixup_epoch=250), transforms.RandomDistort(),
+    transforms.RandomExpand(), transforms.RandomCrop(), transforms.Resize(
+        target_size=608, interp='RANDOM'), transforms.RandomHorizontalFlip(),
+    transforms.Normalize()
+])
+
+eval_transforms = transforms.Compose([
+    transforms.Resize(
+        target_size=608, interp='CUBIC'), transforms.Normalize()
+])
+
+# 定义训练和验证所用的数据集
+# API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/datasets.html#paddlex-datasets-vocdetection
+train_dataset = pdx.datasets.VOCDetection(
+    data_dir='aluminum_inspection',
+    file_list='aluminum_inspection/train_list.txt',
+    label_list='aluminum_inspection/labels.txt',
+    transforms=train_transforms,
+    shuffle=True)
+eval_dataset = pdx.datasets.VOCDetection(
+    data_dir='aluminum_inspection',
+    file_list='aluminum_inspection/val_list.txt',
+    label_list='aluminum_inspection/labels.txt',
+    transforms=eval_transforms)
+
+# 初始化模型,并进行训练
+# 可使用VisualDL查看训练指标,参考https://paddlex.readthedocs.io/zh_CN/develop/train/visualdl.html
+num_classes = len(train_dataset.labels)
+
+# API说明: https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#paddlex-det-yolov3
+model = pdx.det.YOLOv3(num_classes=num_classes, backbone='MobileNetV3_large')
+
+# API说明: https://paddlex.readthedocs.io/zh_CN/develop/apis/models/detection.html#train
+# 各参数介绍与调整说明:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html
+model.train(
+    num_epochs=400,
+    train_dataset=train_dataset,
+    train_batch_size=64,
+    eval_dataset=eval_dataset,
+    learning_rate=0.001,
+    lr_decay_epochs=[240, 320],
+    save_dir='output/yolov3_mobilenetv3',
+    use_vdl=True)

+ 10 - 229
paddlex/cv/datasets/voc.py

@@ -19,8 +19,6 @@ import os.path as osp
 import random
 import re
 import numpy as np
-import cv2
-import json
 from collections import OrderedDict
 import xml.etree.ElementTree as ET
 import paddlex.utils.logging as logging
@@ -72,24 +70,22 @@ class VOCDetection(Dataset):
         annotations['categories'] = []
         annotations['annotations'] = []
 
-        self.cname2cid = OrderedDict()
-        self.cid2cname = OrderedDict()
+        cname2cid = OrderedDict()
         label_id = 1
         with open(label_list, 'r', encoding=get_encoding(label_list)) as fr:
             for line in fr.readlines():
-                self.cname2cid[line.strip()] = label_id
-                self.cid2cname[label_id] = line.strip()
+                cname2cid[line.strip()] = label_id
                 label_id += 1
                 self.labels.append(line.strip())
         logging.info("Starting to read file list from dataset...")
-        for k, v in self.cname2cid.items():
+        for k, v in cname2cid.items():
             annotations['categories'].append({
                 'supercategory': 'component',
                 'id': v,
                 'name': k
             })
         ct = 0
-        self.ann_ct = 0
+        ann_ct = 0
         with open(file_list, 'r', encoding=get_encoding(file_list)) as fr:
             while True:
                 line = fr.readline()
@@ -150,7 +146,9 @@ class VOCDetection(Dataset):
                     name_tag = pattern.findall(str(ET.tostringlist(obj)))[0][
                         1:-1]
                     cname = obj.find(name_tag).text.strip()
-                    gt_class[i][0] = self.cname2cid[cname]
+                    if cname in ['bu_dao_dian', 'jiao_wei_lou_di']:
+                        cname = 'lou_di'
+                    gt_class[i][0] = cname2cid[cname]
                     pattern = re.compile('<difficult>', re.IGNORECASE)
                     diff_tag = pattern.findall(str(ET.tostringlist(obj)))[0][
                         1:-1]
@@ -191,11 +189,11 @@ class VOCDetection(Dataset):
                         'image_id': int(im_id[0]),
                         'bbox': [x1, y1, x2 - x1 + 1, y2 - y1 + 1],
                         'area': float((x2 - x1 + 1) * (y2 - y1 + 1)),
-                        'category_id': self.cname2cid[cname],
-                        'id': self.ann_ct,
+                        'category_id': cname2cid[cname],
+                        'id': ann_ct,
                         'difficult': _difficult
                     })
-                    self.ann_ct += 1
+                    ann_ct += 1
 
                 im_info = {
                     'im_id': im_id,
@@ -273,162 +271,6 @@ class VOCDetection(Dataset):
             self.file_list.append([im_fname, coco_rec])
         self.num_samples = len(self.file_list)
 
-    def generate_image(self, templates, background, save_dir='dataset_clone'):
-        """将目标物体粘贴在背景图片上生成新的图片,并加入到数据集中
-
-        Args:
-            templates (list|tuple):可以将多张图像上的目标物体同时粘贴在同一个背景图片上,
-                因此templates是一个列表,其中每个元素是一个dict,表示一张图片的目标物体。
-                一张图片的目标物体有`image`和`annos`两个关键字,`image`的键值是图像的路径,
-                或者是解码后的排列格式为(H, W, C)且类型为uint8且为BGR格式的数组。
-                图像上可以有多个目标物体,因此`annos`的键值是一个列表,列表中每个元素是一个dict,
-                表示一个目标物体的信息。该dict包含`polygon`和`category`两个关键字,
-                其中`polygon`表示目标物体的边缘坐标,例如[[0, 0], [0, 1], [1, 1], [1, 0]],
-                `category`表示目标物体的类别,例如'dog'。
-            background (dict): 背景图片可以有真值,因此background是一个dict,包含`image`和`annos`
-                两个关键字,`image`的键值是背景图像的路径,或者是解码后的排列格式为(H, W, C)
-                且类型为uint8且为BGR格式的数组。若背景图片上没有真值,则`annos`的键值是空列表[],
-                若有,则`annos`的键值是由多个dict组成的列表,每个dict表示一个物体的信息,
-                包含`bbox`和`category`两个关键字,`bbox`的键值是物体框左上角和右下角的坐标,即
-                [x1, y1, x2, y2],`category`表示目标物体的类别,例如'dog'。
-            save_dir (str):新图片及其标注文件的存储目录。默认值为`dataset_clone`。
-
-        """
-        if not osp.exists(save_dir):
-            os.makedirs(save_dir)
-        image_dir = osp.join(save_dir, 'JPEGImages_clone')
-        anno_dir = osp.join(save_dir, 'Annotations_clone')
-        json_path = osp.join(save_dir, "annotations.json")
-        logging.info("Gegerated images will be saved in {}".format(image_dir))
-        logging.info(
-            "Annotation of generated images will be saved as xml files in {}".
-            format(anno_dir))
-        logging.info(
-            "Annotation of images (loaded before and generated now) will be saved as a COCO json file {}".
-            format(json_path))
-        if not osp.exists(image_dir):
-            os.makedirs(image_dir)
-        if not osp.exists(anno_dir):
-            os.makedirs(anno_dir)
-
-        num_objs = len(background['annos'])
-        for temp in templates:
-            num_objs += len(temp['annos'])
-
-        gt_bbox = np.zeros((num_objs, 4), dtype=np.float32)
-        gt_class = np.zeros((num_objs, 1), dtype=np.int32)
-        gt_score = np.ones((num_objs, 1), dtype=np.float32)
-        is_crowd = np.zeros((num_objs, 1), dtype=np.int32)
-        difficult = np.zeros((num_objs, 1), dtype=np.int32)
-        i = -1
-        for i, back_anno in enumerate(background['annos']):
-            gt_bbox[i] = back_anno['bbox']
-            gt_class[i][0] = self.cname2cid[back_anno['category']]
-
-        max_img_id = max(self.coco_gt.getImgIds())
-        max_img_id += 1
-
-        back_im = background['image']
-        if isinstance(back_im, np.ndarray):
-            if len(back_im.shape) != 3:
-                raise Exception(
-                    "background image should be 3-dimensions, but now is {}-dimensions".
-                    format(len(back_im.shape)))
-        else:
-            try:
-                back_im = cv2.imread(back_im, cv2.IMREAD_UNCHANGED)
-            except:
-                raise TypeError('Can\'t read The image file {}!'.format(
-                    back_im))
-        back_annos = background['annos']
-        im_h, im_w, im_c = back_im.shape
-        for temp in templates:
-            temp_im = temp['image']
-            if isinstance(temp_im, np.ndarray):
-                if len(temp_im.shape) != 3:
-                    raise Exception(
-                        "template image should be 3-dimensions, but now is {}-dimensions".
-                        format(len(temp_im.shape)))
-            else:
-                try:
-                    temp_im = cv2.imread(temp_im, cv2.IMREAD_UNCHANGED)
-                except:
-                    raise TypeError('Can\'t read The image file {}!'.format(
-                        temp_im))
-            temp_annos = temp['annos']
-            for temp_anno in temp_annos:
-                temp_mask = np.zeros(temp_im.shape, temp_im.dtype)
-                temp_poly = np.array(temp_anno['polygon'], np.int32)
-                temp_category = temp_anno['category']
-                cv2.fillPoly(temp_mask, [temp_poly], (255, 255, 255))
-                x_list = [temp_poly[i][0] for i in range(len(temp_poly))]
-                y_list = [temp_poly[i][1] for i in range(len(temp_poly))]
-                temp_poly_w = max(x_list) - min(x_list)
-                temp_poly_h = max(y_list) - min(y_list)
-                found = False
-                while not found:
-                    center_x = random.randint(1, im_w - 1)
-                    center_y = random.randint(1, im_h - 1)
-                    if center_x < temp_poly_w / 2 or center_x > im_w - temp_poly_w / 2 - 1 or \
-                       center_y < temp_poly_h / 2 or center_y > im_h - temp_poly_h / 2 - 1:
-                        found = False
-                        continue
-                    if len(back_annos) == 0:
-                        found = True
-                    for back_anno in back_annos:
-                        x1, y1, x2, y2 = back_anno['bbox']
-                        category = back_anno['category']
-                        if center_x > x1 and center_x < x2 and center_y > y1 and center_y < y2:
-                            found = False
-                            continue
-                        found = True
-                center = (center_x, center_y)
-                back_im = cv2.seamlessClone(temp_im, back_im, temp_mask,
-                                            center, cv2.MIXED_CLONE)
-                i += 1
-                x1 = center[0] - temp_poly_w / 2
-                x2 = center[0] + temp_poly_w / 2
-                y1 = center[1] - temp_poly_h / 2
-                y2 = center[1] + temp_poly_h / 2
-                gt_bbox[i] = [x1, y1, x2, y2]
-                gt_class[i][0] = self.cname2cid[temp_category]
-                self.ann_ct += 1
-                self.coco_gt.dataset['annotations'].append({
-                    'iscrowd': 0,
-                    'image_id': max_img_id,
-                    'bbox': [x1, y1, x2 - x1 + 1, y2 - y1 + 1],
-                    'area': float((x2 - x1 + 1) * (y2 - y1 + 1)),
-                    'category_id': self.cname2cid[temp_category],
-                    'id': self.ann_ct,
-                    'difficult': 0,
-                })
-        im_info = {
-            'im_id': np.array([max_img_id]).astype('int32'),
-            'image_shape': np.array([im_h, im_w]).astype('int32'),
-        }
-        label_info = {
-            'is_crowd': is_crowd,
-            'gt_class': gt_class,
-            'gt_bbox': gt_bbox,
-            'gt_score': gt_score,
-            'difficult': difficult,
-            'gt_poly': [],
-        }
-        self.coco_gt.dataset['images'].append({
-            'height': im_h,
-            'width': im_w,
-            'id': max_img_id,
-            'file_name': "clone_{:06d}.jpg".format(max_img_id)
-        })
-        coco_rec = (im_info, label_info)
-        im_fname = osp.join(image_dir, "clone_{:06d}.jpg".format(max_img_id))
-        cv2.imwrite(im_fname, back_im.astype('uint8'))
-        self._write_xml(im_fname, im_h, im_w, im_c, label_info, anno_dir)
-
-        self.file_list.append([im_fname, coco_rec])
-        self.num_samples = len(self.file_list)
-        self._write_json(self.coco_gt.dataset, save_dir)
-
     def iterator(self):
         self._epoch += 1
         self._pos = 0
@@ -454,64 +296,3 @@ class VOCDetection(Dataset):
             self._pos += 1
             sample = [f[0], im_info, label_info]
             yield sample
-
-    def _write_xml(self, im_fname, im_h, im_w, im_c, label_info, anno_dir):
-        is_crowd = label_info['is_crowd']
-        gt_class = label_info['gt_class']
-        gt_bbox = label_info['gt_bbox']
-        gt_score = label_info['gt_score']
-        gt_poly = label_info['gt_poly']
-        difficult = label_info['difficult']
-        import xml.dom.minidom as minidom
-        xml_doc = minidom.Document()
-        root = xml_doc.createElement("annotation")
-        xml_doc.appendChild(root)
-        node_filename = xml_doc.createElement("filename")
-        node_filename.appendChild(xml_doc.createTextNode(im_fname))
-        root.appendChild(node_filename)
-        node_size = xml_doc.createElement("size")
-        node_width = xml_doc.createElement("width")
-        node_width.appendChild(xml_doc.createTextNode(str(im_w)))
-        node_size.appendChild(node_width)
-        node_height = xml_doc.createElement("height")
-        node_height.appendChild(xml_doc.createTextNode(str(im_h)))
-        node_size.appendChild(node_height)
-        node_depth = xml_doc.createElement("depth")
-        node_depth.appendChild(xml_doc.createTextNode(str(im_c)))
-        node_size.appendChild(node_depth)
-        root.appendChild(node_size)
-        for i in range(label_info['gt_class'].shape[0]):
-            node_obj = xml_doc.createElement("object")
-            node_name = xml_doc.createElement("name")
-            label = self.cid2cname[gt_class[i][0]]
-            node_name.appendChild(xml_doc.createTextNode(label))
-            node_obj.appendChild(node_name)
-            node_diff = xml_doc.createElement("difficult")
-            node_diff.appendChild(xml_doc.createTextNode(str(difficult[i][0])))
-            node_obj.appendChild(node_diff)
-            node_box = xml_doc.createElement("bndbox")
-            node_xmin = xml_doc.createElement("xmin")
-            node_xmin.appendChild(xml_doc.createTextNode(str(gt_bbox[i][0])))
-            node_box.appendChild(node_xmin)
-            node_ymin = xml_doc.createElement("ymin")
-            node_ymin.appendChild(xml_doc.createTextNode(str(gt_bbox[i][1])))
-            node_box.appendChild(node_ymin)
-            node_xmax = xml_doc.createElement("xmax")
-            node_xmax.appendChild(xml_doc.createTextNode(str(gt_bbox[i][2])))
-            node_box.appendChild(node_xmax)
-            node_ymax = xml_doc.createElement("ymax")
-            node_ymax.appendChild(xml_doc.createTextNode(str(gt_bbox[i][3])))
-            node_box.appendChild(node_ymax)
-            node_obj.appendChild(node_box)
-            root.appendChild(node_obj)
-        img_name_part = osp.split(im_fname)[-1].split('.')[0]
-        with open(osp.join(anno_dir, img_name_part + ".xml"), 'w') as fxml:
-            xml_doc.writexml(
-                fxml, indent='\t', addindent='\t', newl='\n', encoding="utf-8")
-
-    def _write_json(self, coco_gt, save_dir):
-        from paddlex.tools.base import MyEncoder
-        json_path = osp.join(save_dir, "annotations.json")
-        f = open(json_path, "w")
-        json.dump(coco_gt, f, indent=4, cls=MyEncoder)
-        f.close()

+ 3 - 7
paddlex/cv/models/base.py

@@ -135,9 +135,7 @@ class BaseAPI:
                            batch_size=1,
                            batch_num=10,
                            cache_dir="./temp"):
-        input_channel = 3
-        if hasattr(self, 'input_channel'):
-            input_channel = self.input_channel
+        input_channel = getattr(self, 'input_channel', 3)
         arrange_transforms(
             model_type=self.model_type,
             class_name=self.__class__.__name__,
@@ -205,7 +203,7 @@ class BaseAPI:
             if pretrain_weights is not None and not os.path.exists(
                     pretrain_weights):
                 if self.model_type == 'classifier':
-                    if pretrain_weights not in ['IMAGENET']:
+                    if pretrain_weights not in ['IMAGENET', 'BAIDU10W']:
                         logging.warning(
                             "Path of pretrain_weights('{}') is not exists!".
                             format(pretrain_weights))
@@ -423,9 +421,7 @@ class BaseAPI:
             from visualdl import LogWriter
             vdl_logdir = osp.join(save_dir, 'vdl_log')
         # 给transform添加arrange操作
-        input_channel = 3
-        if hasattr(self, 'input_channel'):
-            input_channel = self.input_channel
+        input_channel = getattr(self, 'input_channel', 3)
         arrange_transforms(
             model_type=self.model_type,
             class_name=self.__class__.__name__,

+ 67 - 8
paddlex/cv/models/classifier.py

@@ -279,7 +279,11 @@ class BaseClassifier(BaseAPI):
         return metrics
 
     @staticmethod
-    def _preprocess(images, transforms, model_type, class_name, thread_pool=None):
+    def _preprocess(images,
+                    transforms,
+                    model_type,
+                    class_name,
+                    thread_pool=None):
         arrange_transforms(
             model_type=model_type,
             class_name=class_name,
@@ -343,10 +347,7 @@ class BaseClassifier(BaseAPI):
 
         return preds[0]
 
-    def batch_predict(self,
-                      img_file_list,
-                      transforms=None,
-                      topk=1):
+    def batch_predict(self, img_file_list, transforms=None, topk=1):
         """预测。
         Args:
             img_file_list(list|tuple): 对列表(或元组)中的图像同时进行预测,列表中的元素可以是图像路径
@@ -365,9 +366,9 @@ class BaseClassifier(BaseAPI):
 
         if transforms is None:
             transforms = self.test_transforms
-        im = BaseClassifier._preprocess(img_file_list, transforms,
-                                        self.model_type,
-                                        self.__class__.__name__, self.thread_pool)
+        im = BaseClassifier._preprocess(
+            img_file_list, transforms, self.model_type,
+            self.__class__.__name__, self.thread_pool)
 
         with fluid.scope_guard(self.scope):
             result = self.exe.run(self.test_prog,
@@ -409,6 +410,64 @@ class ResNet50_vd(BaseClassifier):
         super(ResNet50_vd, self).__init__(
             model_name='ResNet50_vd', num_classes=num_classes)
 
+    def train(self,
+              num_epochs,
+              train_dataset,
+              train_batch_size=64,
+              eval_dataset=None,
+              save_interval_epochs=1,
+              log_interval_steps=2,
+              save_dir='output',
+              pretrain_weights='BAIDU10W',
+              optimizer=None,
+              learning_rate=0.025,
+              warmup_steps=0,
+              warmup_start_lr=0.0,
+              lr_decay_epochs=[30, 60, 90],
+              lr_decay_gamma=0.1,
+              use_vdl=False,
+              sensitivities_file=None,
+              eval_metric_loss=0.05,
+              early_stop=False,
+              early_stop_patience=5,
+              resume_checkpoint=None):
+        """训练。
+        Args:
+            num_epochs (int): 训练迭代轮数。
+            train_dataset (paddlex.datasets): 训练数据读取器。
+            train_batch_size (int): 训练数据batch大小。同时作为验证数据batch大小。默认值为64。
+            eval_dataset (paddlex.datasets: 验证数据读取器。
+            save_interval_epochs (int): 模型保存间隔(单位:迭代轮数)。默认为1。
+            log_interval_steps (int): 训练日志输出间隔(单位:迭代步数)。默认为2。
+            save_dir (str): 模型保存路径。
+            pretrain_weights (str): 若指定为路径时,则加载路径下预训练模型;若为字符串'IMAGENET',
+                则自动下载在ImageNet图片数据上预训练的模型权重;若为None,则不使用预训练模型。若为'BAIDU10W',则自动下载百度自研10万类预训练。默认为'BAIDU10W'。
+            optimizer (paddle.fluid.optimizer): 优化器。当该参数为None时,使用默认优化器:
+                fluid.layers.piecewise_decay衰减策略,fluid.optimizer.Momentum优化方法。
+            learning_rate (float): 默认优化器的初始学习率。默认为0.025。
+            warmup_steps(int): 学习率从warmup_start_lr上升至设定的learning_rate,所需的步数,默认为0
+            warmup_start_lr(float): 学习率在warmup阶段时的起始值,默认为0.0
+            lr_decay_epochs (list): 默认优化器的学习率衰减轮数。默认为[30, 60, 90]。
+            lr_decay_gamma (float): 默认优化器的学习率衰减率。默认为0.1。
+            use_vdl (bool): 是否使用VisualDL进行可视化。默认值为False。
+            sensitivities_file (str): 若指定为路径时,则加载路径下敏感度信息进行裁剪;若为字符串'DEFAULT',
+                则自动下载在ImageNet图片数据上获得的敏感度信息进行裁剪;若为None,则不进行裁剪。默认为None。
+            eval_metric_loss (float): 可容忍的精度损失。默认为0.05。
+            early_stop (bool): 是否使用提前终止训练策略。默认值为False。
+            early_stop_patience (int): 当使用提前终止训练策略时,如果验证集精度在`early_stop_patience`个epoch内
+                连续下降或持平,则终止训练。默认值为5。
+            resume_checkpoint (str): 恢复训练时指定上次训练保存的模型路径。若为None,则不会恢复训练。默认值为None。
+        Raises:
+            ValueError: 模型从inference model进行加载。
+        """
+        return super(ResNet50_vd, self).train(
+            num_epochs, train_dataset, train_batch_size, eval_dataset,
+            save_interval_epochs, log_interval_steps, save_dir,
+            pretrain_weights, optimizer, learning_rate, warmup_steps,
+            warmup_start_lr, lr_decay_epochs, lr_decay_gamma, use_vdl,
+            sensitivities_file, eval_metric_loss, early_stop,
+            early_stop_patience, resume_checkpoint)
+
 
 class ResNet101_vd(BaseClassifier):
     def __init__(self, num_classes=1000):

+ 17 - 5
paddlex/cv/models/deeplabv3p.py

@@ -459,12 +459,14 @@ class DeepLabv3p(BaseAPI):
                     transforms,
                     model_type,
                     class_name,
-                    thread_pool=None):
+                    thread_pool=None,
+                    input_channel=3):
         arrange_transforms(
             model_type=model_type,
             class_name=class_name,
             transforms=transforms,
-            mode='test')
+            mode='test',
+            input_channel=input_channel)
         if thread_pool is not None:
             batch_data = thread_pool.map(transforms, images)
         else:
@@ -523,8 +525,13 @@ class DeepLabv3p(BaseAPI):
 
         if transforms is None:
             transforms = self.test_transforms
+        input_channel = getattr(self, 'input_channel', 3)
         im, im_info = DeepLabv3p._preprocess(
-            images, transforms, self.model_type, self.__class__.__name__)
+            images,
+            transforms,
+            self.model_type,
+            self.__class__.__name__,
+            input_channel=input_channel)
 
         with fluid.scope_guard(self.scope):
             result = self.exe.run(self.test_prog,
@@ -553,9 +560,14 @@ class DeepLabv3p(BaseAPI):
             raise Exception("im_file must be list/tuple")
         if transforms is None:
             transforms = self.test_transforms
+        input_channel = getattr(self, 'input_channel', 3)
         im, im_info = DeepLabv3p._preprocess(
-            img_file_list, transforms, self.model_type,
-            self.__class__.__name__, self.thread_pool)
+            img_file_list,
+            transforms,
+            self.model_type,
+            self.__class__.__name__,
+            self.thread_pool,
+            input_channel=input_channel)
 
         with fluid.scope_guard(self.scope):
             result = self.exe.run(self.test_prog,

+ 51 - 15
paddlex/cv/models/faster_rcnn.py

@@ -35,10 +35,43 @@ class FasterRCNN(BaseAPI):
     Args:
         num_classes (int): 包含了背景类的类别数。默认为81。
         backbone (str): FasterRCNN的backbone网络,取值范围为['ResNet18', 'ResNet50',
-            'ResNet50_vd', 'ResNet101', 'ResNet101_vd', 'HRNet_W18']。默认为'ResNet50'。
+            'ResNet50_vd', 'ResNet101', 'ResNet101_vd', 'HRNet_W18', 'ResNet50_vd_ssld']。默认为'ResNet50'。
         with_fpn (bool): 是否使用FPN结构。默认为True。
         aspect_ratios (list): 生成anchor高宽比的可选值。默认为[0.5, 1.0, 2.0]。
         anchor_sizes (list): 生成anchor大小的可选值。默认为[32, 64, 128, 256, 512]。
+        with_dcn (bool): backbone网络中是否使用deformable convolution network v2。默认为False。
+        rpn_cls_loss (str): RPN部分的分类损失函数,取值范围为['SigmoidCrossEntropy', 'SigmoidFocalLoss']。
+            当遇到模型误检了很多背景区域时,可以考虑使用'SigmoidFocalLoss',并调整适合的`rpn_focal_loss_alpha`
+            和`rpn_focal_loss_gamma`。默认为'SigmoidCrossEntropy'。
+        rpn_focal_loss_alpha (float):当RPN的分类损失函数设置为'SigmoidFocalLoss'时,用于调整
+            正样本和负样本的比例因子,默认为0.25。当PN的分类损失函数设置为'SigmoidCrossEntropy'时,
+            `rpn_focal_loss_alpha`的设置不生效。
+        rpn_focal_loss_gamma (float): 当RPN的分类损失函数设置为'SigmoidFocalLoss'时,用于调整
+            易分样本和难分样本的比例因子,默认为2。当RPN的分类损失函数设置为'SigmoidCrossEntropy'时,
+            `rpn_focal_loss_gamma`的设置不生效。
+        rcnn_bbox_loss (str): RCNN部分的位置回归损失函数,取值范围为['SmoothL1Loss', 'CIoULoss']。
+            默认为'SmoothL1Loss'。
+        rcnn_nms (str): RCNN部分的非极大值抑制的计算方法,取值范围为['MultiClassNMS', 'MultiClassSoftNMS',
+            'MultiClassCiouNMS']。默认为'MultiClassNMS'。当选择'MultiClassNMS'时,可以将`keep_top_k`设置成100、
+            `nms_threshold`设置成0.5、`score_threshold`设置成0.05。当选择'MultiClassSoftNMS'时,可以将`keep_top_k`设置为300、
+            `score_threshold`设置为0.01、`softnms_sigma`设置为0.5。当选择'MultiClassCiouNMS'时,可以将`keep_top_k`设置为100、
+            `score_threshold`设置成0.05、`nms_threshold`设置成0.5。
+        keep_top_k (int): RCNN部分在进行非极大值抑制计算后,每张图像保留最多保存`keep_top_k`个检测框。默认为100。
+        nms_threshold (float): RCNN部分在进行非极大值抑制时,用于剔除检测框所需的IoU阈值。
+            当`rcnn_nms`设置为`MultiClassSoftNMS`时,`nms_threshold`的设置不生效。默认为0.5。
+        score_threshold (float): RCNN部分在进行非极大值抑制前,用于过滤掉低置信度边界框所需的置信度阈值。默认为0.05。
+        softnms_sigma (float): 当`rcnn_nms`设置为`MultiClassSoftNMS`时,用于调整被抑制的检测框的置信度,
+            调整公式为`score = score * weights, weights = exp(-(iou * iou) / softnms_sigma)`。默认设为0.5。
+        bbox_assigner (str): 训练阶段,RCNN部分生成正负样本的采样方式。可选范围为['BBoxAssigner', 'LibraBBoxAssigner']。
+            当目标物体的区域只占原始图像的一小部分时,使用`LibraBBoxAssigner`采样方式模型效果更佳。默认为'BBoxAssigner'。
+        fpn_num_channels (int): FPN部分特征层的通道数量。默认为256。
+        input_channel (int): 输入图像的通道数量。默认为3。
+        rpn_batch_size_per_im (int): 训练阶段,RPN部分每张图片的正负样本的数量总和。默认为256。
+        rpn_fg_fraction (float): 训练阶段,RPN部分每张图片的正负样本数量总和中正样本的占比。默认为0.5。
+        test_pre_nms_top_n (int):预测阶段,RPN部分做非极大值抑制计算的候选框的数量。若设置为None,
+            有FPN结构的话,`test_pre_nms_top_n`会被设置成6000, 无FPN结构的话,`test_pre_nms_top_n`会被设置成
+            1000。默认为None。
+        test_post_nms_top_n (int): 预测阶段,RPN部分做完非极大值抑制后保留的候选框的数量。默认为1000。
     """
 
     def __init__(self,
@@ -57,7 +90,6 @@ class FasterRCNN(BaseAPI):
                  nms_threshold=0.5,
                  score_threshold=0.05,
                  softnms_sigma=0.5,
-                 post_threshold=0.05,
                  bbox_assigner='BBoxAssigner',
                  fpn_num_channels=256,
                  input_channel=3,
@@ -69,7 +101,7 @@ class FasterRCNN(BaseAPI):
         super(FasterRCNN, self).__init__('detector')
         backbones = [
             'ResNet18', 'ResNet50', 'ResNet50_vd', 'ResNet101', 'ResNet101_vd',
-            'HRNet_W18'
+            'HRNet_W18', 'ResNet50_vd_ssld'
         ]
         assert backbone in backbones, "backbone should be one of {}".format(
             backbones)
@@ -93,7 +125,6 @@ class FasterRCNN(BaseAPI):
         self.nms_threshold = nms_threshold
         self.score_threshold = score_threshold
         self.softnms_sigma = softnms_sigma
-        self.post_threshold = post_threshold
         self.bbox_assigner = bbox_assigner
         self.fpn_num_channels = fpn_num_channels
         self.input_channel = input_channel
@@ -181,7 +212,6 @@ class FasterRCNN(BaseAPI):
             keep_top_k=self.keep_top_k,
             nms_threshold=self.nms_threshold,
             score_threshold=self.score_threshold,
-            post_threshold=self.post_threshold,
             softnms_sigma=self.softnms_sigma,
             bbox_assigner=self.bbox_assigner,
             fpn_num_channels=self.fpn_num_channels,
@@ -369,9 +399,7 @@ class FasterRCNN(BaseAPI):
                 一个预测结果是一个由图像id,预测框类别id, 预测框坐标,预测框得分组成的列表。而关键字gt的键值是真实标注框的相关信息。
         """
 
-        input_channel = 3
-        if hasattr(self, 'input_channel'):
-            input_channel = self.input_channel
+        input_channel = getattr(self, 'input_channel', 3)
         arrange_transforms(
             model_type=self.model_type,
             class_name=self.__class__.__name__,
@@ -459,10 +487,8 @@ class FasterRCNN(BaseAPI):
                     transforms,
                     model_type,
                     class_name,
-                    thread_pool=None):
-        input_channel = 3
-        if hasattr(self, input_channel):
-            input_channel = self.input_channel
+                    thread_pool=None,
+                    input_channel=3):
         arrange_transforms(
             model_type=model_type,
             class_name=class_name,
@@ -516,8 +542,13 @@ class FasterRCNN(BaseAPI):
 
         if transforms is None:
             transforms = self.test_transforms
+        input_channel = getattr(self, 'input_channel', 3)
         im, im_resize_info, im_shape = FasterRCNN._preprocess(
-            images, transforms, self.model_type, self.__class__.__name__)
+            images,
+            transforms,
+            self.model_type,
+            self.__class__.__name__,
+            input_channel=input_channel)
 
         with fluid.scope_guard(self.scope):
             result = self.exe.run(self.test_prog,
@@ -563,9 +594,14 @@ class FasterRCNN(BaseAPI):
 
         if transforms is None:
             transforms = self.test_transforms
+        input_channel = getattr(self, 'input_channel', 3)
         im, im_resize_info, im_shape = FasterRCNN._preprocess(
-            img_file_list, transforms, self.model_type,
-            self.__class__.__name__, self.thread_pool)
+            img_file_list,
+            transforms,
+            self.model_type,
+            self.__class__.__name__,
+            self.thread_pool,
+            input_channel=input_channel)
 
         with fluid.scope_guard(self.scope):
             result = self.exe.run(self.test_prog,

+ 22 - 6
paddlex/cv/models/mask_rcnn.py

@@ -38,6 +38,7 @@ class MaskRCNN(FasterRCNN):
         with_fpn (bool): 是否使用FPN结构。默认为True。
         aspect_ratios (list): 生成anchor高宽比的可选值。默认为[0.5, 1.0, 2.0]。
         anchor_sizes (list): 生成anchor大小的可选值。默认为[32, 64, 128, 256, 512]。
+        input_channel (int): 输入图像的通道数量。默认为3。
     """
 
     def __init__(self,
@@ -45,7 +46,8 @@ class MaskRCNN(FasterRCNN):
                  backbone='ResNet50',
                  with_fpn=True,
                  aspect_ratios=[0.5, 1.0, 2.0],
-                 anchor_sizes=[32, 64, 128, 256, 512]):
+                 anchor_sizes=[32, 64, 128, 256, 512],
+                 input_channel=3):
         self.init_params = locals()
         backbones = [
             'ResNet18', 'ResNet50', 'ResNet50_vd', 'ResNet101', 'ResNet101_vd',
@@ -64,6 +66,7 @@ class MaskRCNN(FasterRCNN):
         else:
             self.mask_head_resolution = 14
         self.fixed_input_shape = None
+        self.input_channel = input_channel
 
     def build_net(self, mode='train'):
         train_pre_nms_top_n = 2000 if self.with_fpn else 12000
@@ -78,7 +81,8 @@ class MaskRCNN(FasterRCNN):
             test_pre_nms_top_n=test_pre_nms_top_n,
             num_convs=num_convs,
             mask_head_resolution=self.mask_head_resolution,
-            fixed_input_shape=self.fixed_input_shape)
+            fixed_input_shape=self.fixed_input_shape,
+            input_channel=self.input_channel)
         inputs = model.generate_inputs()
         if mode == 'train':
             model_out = model.build_net(inputs)
@@ -257,11 +261,13 @@ class MaskRCNN(FasterRCNN):
                 预测框类别id、表示预测框内各像素点是否属于物体的二值图、预测框得分。
                 而关键字gt的键值是真实标注框的相关信息。
         """
+        input_channel = getattr(self, 'input_channel', 3)
         arrange_transforms(
             model_type=self.model_type,
             class_name=self.__class__.__name__,
             transforms=eval_dataset.transforms,
-            mode='eval')
+            mode='eval',
+            input_channel=input_channel)
         if metric is None:
             if hasattr(self, 'metric') and self.metric is not None:
                 metric = self.metric
@@ -383,8 +389,13 @@ class MaskRCNN(FasterRCNN):
 
         if transforms is None:
             transforms = self.test_transforms
+        input_channel = getattr(self, 'input_channel', 3)
         im, im_resize_info, im_shape = FasterRCNN._preprocess(
-            images, transforms, self.model_type, self.__class__.__name__)
+            images,
+            transforms,
+            self.model_type,
+            self.__class__.__name__,
+            input_channel=input_channel)
 
         with fluid.scope_guard(self.scope):
             result = self.exe.run(self.test_prog,
@@ -431,9 +442,14 @@ class MaskRCNN(FasterRCNN):
 
         if transforms is None:
             transforms = self.test_transforms
+        input_channel = getattr(self, 'input_channel', 3)
         im, im_resize_info, im_shape = FasterRCNN._preprocess(
-            img_file_list, transforms, self.model_type,
-            self.__class__.__name__, self.thread_pool)
+            img_file_list,
+            transforms,
+            self.model_type,
+            self.__class__.__name__,
+            self.thread_pool,
+            input_channel=input_channel)
 
         with fluid.scope_guard(self.scope):
             result = self.exe.run(self.test_prog,

+ 27 - 9
paddlex/cv/models/ppyolo.py

@@ -58,6 +58,7 @@ class PPYOLO(BaseAPI):
         nms_iou_threshold (float): 进行NMS时,用于剔除检测框IOU的阈值。默认为0.45。
         label_smooth (bool): 是否使用label smooth。默认值为False。
         train_random_shapes (list|tuple): 训练时从列表中随机选择图像大小。默认值为[320, 352, 384, 416, 448, 480, 512, 544, 576, 608]。
+        input_channel (int): 输入图像的通道数量。默认为3。
     """
 
     def __init__(
@@ -85,7 +86,8 @@ class PPYOLO(BaseAPI):
             nms_iou_threshold=0.45,
             train_random_shapes=[
                 320, 352, 384, 416, 448, 480, 512, 544, 576, 608
-            ]):
+            ],
+            input_channel=3):
         self.init_params = locals()
         super(PPYOLO, self).__init__('detector')
         backbones = ['ResNet50_vd_ssld']
@@ -123,6 +125,7 @@ class PPYOLO(BaseAPI):
         self.use_matrix_nms = use_matrix_nms
         self.use_ema = False
         self.with_dcn_v2 = with_dcn_v2
+        self.input_channel = input_channel
 
         if paddle.__version__ < '1.8.4' and paddle.__version__ != '0.0.0':
             raise Exception(
@@ -164,7 +167,8 @@ class PPYOLO(BaseAPI):
             use_fine_grained_loss=self.use_fine_grained_loss,
             use_iou_loss=self.use_iou_loss,
             batch_size=self.batch_size_per_gpu
-            if hasattr(self, 'batch_size_per_gpu') else 8)
+            if hasattr(self, 'batch_size_per_gpu') else 8,
+            input_channel=self.input_channel)
         if mode == 'train' and self.use_iou_loss or self.use_iou_aware:
             model.max_height = self.max_height
             model.max_width = self.max_width
@@ -387,11 +391,13 @@ class PPYOLO(BaseAPI):
                 eval_details为dict,包含bbox和gt两个关键字。其中关键字bbox的键值是一个列表,列表中每个元素代表一个预测结果,
                 一个预测结果是一个由图像id,预测框类别id, 预测框坐标,预测框得分组成的列表。而关键字gt的键值是真实标注框的相关信息。
         """
+        input_channel = getattr(self, 'input_channel', 3)
         arrange_transforms(
             model_type=self.model_type,
             class_name=self.__class__.__name__,
             transforms=eval_dataset.transforms,
-            mode='eval')
+            mode='eval',
+            input_channel=input_channel)
         if metric is None:
             if hasattr(self, 'metric') and self.metric is not None:
                 metric = self.metric
@@ -456,12 +462,14 @@ class PPYOLO(BaseAPI):
                     transforms,
                     model_type,
                     class_name,
-                    thread_pool=None):
+                    thread_pool=None,
+                    input_channel=3):
         arrange_transforms(
             model_type=model_type,
             class_name=class_name,
             transforms=transforms,
-            mode='test')
+            mode='test',
+            input_channel=input_channel)
         if thread_pool is not None:
             batch_data = thread_pool.map(transforms, images)
         else:
@@ -510,8 +518,13 @@ class PPYOLO(BaseAPI):
 
         if transforms is None:
             transforms = self.test_transforms
-        im, im_size = PPYOLO._preprocess(images, transforms, self.model_type,
-                                         self.__class__.__name__)
+        input_channel = getattr(self, 'input_channel', 3)
+        im, im_size = PPYOLO._preprocess(
+            images,
+            transforms,
+            self.model_type,
+            self.__class__.__name__,
+            input_channel=input_channel)
 
         with fluid.scope_guard(self.scope):
             result = self.exe.run(self.test_prog,
@@ -551,9 +564,14 @@ class PPYOLO(BaseAPI):
 
         if transforms is None:
             transforms = self.test_transforms
+        input_channel = getattr(self, 'input_channel', 3)
         im, im_size = PPYOLO._preprocess(
-            img_file_list, transforms, self.model_type,
-            self.__class__.__name__, self.thread_pool)
+            img_file_list,
+            transforms,
+            self.model_type,
+            self.__class__.__name__,
+            self.thread_pool,
+            input_channel=input_channel)
 
         with fluid.scope_guard(self.scope):
             result = self.exe.run(self.test_prog,

+ 41 - 0
paddlex/cv/models/utils/pretrain_weights.py

@@ -77,6 +77,11 @@ image_pretrain = {
     'http://paddle-imagenet-models-name.bj.bcebos.com/AlexNet_pretrained.tar'
 }
 
+baidu10w_pretrain = {
+    'ResNet50_vd_BAIDU10W':
+    'https://paddle-imagenet-models-name.bj.bcebos.com/ResNet50_vd_10w_pretrained.tar'
+}
+
 coco_pretrain = {
     'YOLOv3_DarkNet53_COCO':
     'https://paddlemodels.bj.bcebos.com/object_detection/yolov3_darknet.tar',
@@ -183,6 +188,11 @@ def get_pretrain_weights(flag, class_name, backbone, save_dir):
             logging.warning(
                 warning_info.format(class_name, flag, 'CITYSCAPES'))
             flag = 'CITYSCAPES'
+    elif flag == 'BAIDU10W':
+        if class_name not in ['ResNet50_vd']:
+            raise Exception(
+                "Only the classifier ResNet50_vd supports BAIDU10W pretrained weights"
+            )
 
     if flag == 'IMAGENET':
         new_save_dir = save_dir
@@ -245,6 +255,37 @@ def get_pretrain_weights(flag, class_name, backbone, save_dir):
         if getattr(paddlex, 'gui_mode', False):
             paddlex.utils.download_and_decompress(url, path=new_save_dir)
             return osp.join(new_save_dir, fname)
+        try:
+            logging.info(
+                "Connecting PaddleHub server to get pretrain weights...")
+            hub.download(backbone, save_path=new_save_dir)
+        except Exception as e:
+            logging.error(
+                "Couldn't download pretrain weight, you can download it manualy from {} (decompress the file if it is a compressed file), and set pretrain weights by your self".
+                format(url),
+                exit=False)
+            if isinstance(hub.ResourceNotFoundError):
+                raise Exception("Resource for backbone {} not found".format(
+                    backbone))
+            elif isinstance(hub.ServerConnectionError):
+                raise Exception(
+                    "Cannot get reource for backbone {}, please check your internet connection"
+                    .format(backbone))
+            else:
+                raise Exception(
+                    "Unexpected error, please make sure paddlehub >= 1.6.2")
+        return osp.join(new_save_dir, backbone)
+    elif flag == 'BAIDU10W':
+        new_save_dir = save_dir
+        if hasattr(paddlex, 'pretrain_dir'):
+            new_save_dir = paddlex.pretrain_dir
+        backbone = backbone + '_BAIDU10W'
+        url = baidu10w_pretrain[backbone]
+        fname = osp.split(url)[-1].split('.')[0]
+
+        if getattr(paddlex, 'gui_mode', False):
+            paddlex.utils.download_and_decompress(url, path=new_save_dir)
+            return osp.join(new_save_dir, fname)
 
         try:
             logging.info(

+ 4 - 1
paddlex/cv/models/yolo_v3.py

@@ -38,6 +38,7 @@ class YOLOv3(PPYOLO):
         nms_iou_threshold (float): 进行NMS时,用于剔除检测框IoU的阈值。默认为0.45。
         label_smooth (bool): 是否使用label smooth。默认值为False。
         train_random_shapes (list|tuple): 训练时从列表中随机选择图像大小。默认值为[320, 352, 384, 416, 448, 480, 512, 544, 576, 608]。
+        input_channel (int): 输入图像的通道数量。默认为3。
     """
 
     def __init__(self,
@@ -53,7 +54,8 @@ class YOLOv3(PPYOLO):
                  label_smooth=False,
                  train_random_shapes=[
                      320, 352, 384, 416, 448, 480, 512, 544, 576, 608
-                 ]):
+                 ],
+                 input_channel=3):
         self.init_params = locals()
         backbones = [
             'DarkNet53', 'ResNet34', 'MobileNetV1', 'MobileNetV3_large'
@@ -84,6 +86,7 @@ class YOLOv3(PPYOLO):
         self.use_matrix_nms = False
         self.use_ema = False
         self.with_dcn_v2 = False
+        self.input_channel = input_channel
 
     def _get_backbone(self, backbone_name):
         if backbone_name == 'DarkNet53':

+ 2 - 2
paddlex/cv/nets/detection/bbox_head.py

@@ -110,7 +110,7 @@ class BBoxHead(object):
         self.diouloss_weight = diouloss_weight
         self.diouloss_is_cls_agnostic = diouloss_is_cls_agnostic
         self.diouloss_use_complete_iou_loss = diouloss_use_complete_iou_loss
-        if self.rcnn_bbox_loss == 'DIoULoss':
+        if self.rcnn_bbox_loss == 'CIoULoss':
             self.diou_loss = DiouLoss(
                 loss_weight=self.diouloss_weight,
                 is_cls_agnostic=self.diouloss_is_cls_agnostic,
@@ -238,7 +238,7 @@ class BBoxHead(object):
                 inside_weight=bbox_inside_weights,
                 outside_weight=bbox_outside_weights,
                 sigma=self.sigma)
-        elif self.rcnn_bbox_loss == 'DIoULoss':
+        elif self.rcnn_bbox_loss == 'CIoULoss':
             loss_bbox = self.diou_loss(
                 x=bbox_pred,
                 y=bbox_targets,

+ 8 - 3
paddlex/cv/nets/detection/mask_rcnn.py

@@ -87,7 +87,8 @@ class MaskRCNN(object):
             bg_thresh_hi=.5,
             bg_thresh_lo=0.,
             bbox_reg_weights=[0.1, 0.1, 0.2, 0.2],
-            fixed_input_shape=None):
+            fixed_input_shape=None,
+            input_channel=3):
         super(MaskRCNN, self).__init__()
         self.backbone = backbone
         self.mode = mode
@@ -173,6 +174,7 @@ class MaskRCNN(object):
         self.bbox_reg_weights = bbox_reg_weights
         self.rpn_only = rpn_only
         self.fixed_input_shape = fixed_input_shape
+        self.input_channel = input_channel
 
     def build_net(self, inputs):
         im = inputs['image']
@@ -315,13 +317,16 @@ class MaskRCNN(object):
 
         if self.fixed_input_shape is not None:
             input_shape = [
-                None, 3, self.fixed_input_shape[1], self.fixed_input_shape[0]
+                None, self.input_channel, self.fixed_input_shape[1],
+                self.fixed_input_shape[0]
             ]
             inputs['image'] = fluid.data(
                 dtype='float32', shape=input_shape, name='image')
         else:
             inputs['image'] = fluid.data(
-                dtype='float32', shape=[None, 3, None, None], name='image')
+                dtype='float32',
+                shape=[None, self.input_channel, None, None],
+                name='image')
         if self.mode == 'train':
             inputs['im_info'] = fluid.data(
                 dtype='float32', shape=[None, 3], name='im_info')

+ 4 - 2
paddlex/cv/nets/detection/rpn_head.py

@@ -282,9 +282,9 @@ class RPNHead(object):
                         rpn_positive_overlap=self.rpn_positive_overlap,
                         rpn_negative_overlap=self.rpn_negative_overlap,
                         use_random=self.use_random)
-            score_tgt = fluid.layers.cast(x=score_tgt, dtype='float32')
-            score_tgt.stop_gradient = True
             if self.rpn_cls_loss == 'SigmoidCrossEntropy':
+                score_tgt = fluid.layers.cast(x=score_tgt, dtype='float32')
+                score_tgt.stop_gradient = True
                 rpn_cls_loss = fluid.layers.sigmoid_cross_entropy_with_logits(
                     x=score_pred, label=score_tgt)
             elif self.rpn_cls_loss == 'SigmoidFocalLoss':
@@ -294,6 +294,8 @@ class RPNHead(object):
                 fg_label = fluid.layers.cast(fg_label, dtype='int32')
                 fg_num = fluid.layers.reduce_sum(fg_label)
                 fg_num.stop_gradient = True
+                score_tgt = fluid.layers.cast(x=score_tgt, dtype='float32')
+                score_tgt.stop_gradient = True
                 loss = fluid.layers.sigmoid_cross_entropy_with_logits(
                     x=score_pred, label=score_tgt)
 

+ 8 - 3
paddlex/cv/nets/detection/yolo_v3.py

@@ -62,7 +62,8 @@ class YOLOv3:
             nms_topk=1000,
             nms_keep_topk=100,
             nms_iou_threshold=0.45,
-            fixed_input_shape=None):
+            fixed_input_shape=None,
+            input_channel=3):
         if anchors is None:
             anchors = [[10, 13], [16, 30], [33, 23], [30, 61], [62, 45],
                        [59, 119], [116, 90], [156, 198], [373, 326]]
@@ -125,6 +126,7 @@ class YOLOv3:
         self.keep_prob = 0.9
         self.downsample = [32, 16, 8]
         self.clip_bbox = True
+        self.input_channel = input_channel
 
     def _head(self, input, is_train=True):
         outputs = []
@@ -446,13 +448,16 @@ class YOLOv3:
         inputs = OrderedDict()
         if self.fixed_input_shape is not None:
             input_shape = [
-                None, 3, self.fixed_input_shape[1], self.fixed_input_shape[0]
+                None, self.input_channel, self.fixed_input_shape[1],
+                self.fixed_input_shape[0]
             ]
             inputs['image'] = fluid.data(
                 dtype='float32', shape=input_shape, name='image')
         else:
             inputs['image'] = fluid.data(
-                dtype='float32', shape=[None, 3, None, None], name='image')
+                dtype='float32',
+                shape=[None, self.input_channel, None, None],
+                name='image')
         if self.mode == 'train':
             inputs['gt_box'] = fluid.data(
                 dtype='float32', shape=[None, None, 4], name='gt_box')

+ 3 - 3
paddlex/cv/transforms/det_transforms.py

@@ -141,9 +141,7 @@ class Compose(DetTransform):
             else:
                 return (im, im_info, label_info)
 
-        input_channel = 3
-        if hasattr(self, 'input_channel'):
-            input_channel = self.input_channel
+        input_channel = getattr(self, 'input_channel', 3)
         outputs = decode_image(im, im_info, label_info, input_channel)
         im = outputs[0]
         im_info = outputs[1]
@@ -186,6 +184,8 @@ class ResizeByShort(DetTransform):
     1. 获取图像的长边和短边长度。
     2. 根据短边与short_size的比例,计算长边的目标长度,
        此时高、宽的resize比例为short_size/原图短边长度。
+       若short_size为数组,则随机从该数组中挑选一个数值
+       作为short_size。
     3. 如果max_size>0,调整resize比例:
        如果长边的目标长度>max_size,则高、宽的resize比例为max_size/原图长边长度。
     4. 根据调整大小的比例对图像进行resize。

+ 10 - 6
paddlex/cv/transforms/seg_transforms.py

@@ -65,7 +65,7 @@ class Compose(SegTransform):
                     )
 
     @staticmethod
-    def read_img(img_path):
+    def read_img(img_path, input_channel=3):
         img_format = imghdr.what(img_path)
         name, ext = osp.splitext(img_path)
         if img_format == 'tiff' or ext == '.img':
@@ -83,14 +83,17 @@ class Compose(SegTransform):
             im_data = dataset.ReadAsArray()
             return im_data.transpose((1, 2, 0))
         elif img_format in ['jpeg', 'bmp', 'png']:
-            return cv2.imread(img_path)
+            if input_channel == 3:
+                return cv2.imread(img_path)
+            else:
+                im = cv2.imread(im_file, cv2.IMREAD_UNCHANGED)
         elif ext == '.npy':
             return np.load(img_path)
         else:
             raise Exception('Image format {} is not supported!'.format(ext))
 
     @staticmethod
-    def decode_image(im, label):
+    def decode_image(im, label, input_channel=3):
         if isinstance(im, np.ndarray):
             if len(im.shape) != 3:
                 raise Exception(
@@ -98,7 +101,7 @@ class Compose(SegTransform):
                     format(len(im.shape)))
         else:
             try:
-                im = Compose.read_img(im).astype('float32')
+                im = Compose.read_img(im, input_channel).astype('float32')
             except:
                 raise ValueError('Can\'t read The image file {}!'.format(im))
         im = im.astype('float32')
@@ -134,8 +137,9 @@ class Compose(SegTransform):
             tuple: 根据网络所需字段所组成的tuple;字段由transforms中的最后一个数据预处理操作决定。
         """
 
-        im, label = self.decode_image(im, label)
-        if self.to_rgb:
+        input_channel = getattr(self, 'input_channel', 3)
+        im, label = self.decode_image(im, label, input_channel)
+        if self.to_rgb and input_channel == 3:
             im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
         if im_info is None:
             im_info = [('origin_shape', im.shape[0:2])]

+ 11 - 4
paddlex/deploy.py

@@ -85,6 +85,9 @@ class Predictor:
         # 主要用于batch_predict接口
         thread_num = mp.cpu_count() if mp.cpu_count() < 8 else 8
         self.thread_pool = mp.pool.ThreadPool(thread_num)
+        self.input_channel = 3
+        if 'input_channel' in self.info['_init_params']:
+            self.input_channel = self.info['_init_params']['input_channel']
 
     def reset_thread_pool(self, thread_num):
         self.thread_pool.close()
@@ -144,7 +147,8 @@ class Predictor:
                 self.transforms,
                 self.model_type,
                 self.model_name,
-                thread_pool=thread_pool)
+                thread_pool=thread_pool,
+                input_channel=self.input_channel)
             res['image'] = im
         elif self.model_type == "detector":
             if self.model_name in ["PPYOLO", "YOLOv3"]:
@@ -153,7 +157,8 @@ class Predictor:
                     self.transforms,
                     self.model_type,
                     self.model_name,
-                    thread_pool=thread_pool)
+                    thread_pool=thread_pool,
+                    input_channel=self.input_channel)
                 res['image'] = im
                 res['im_size'] = im_size
             if self.model_name.count('RCNN') > 0:
@@ -162,7 +167,8 @@ class Predictor:
                     self.transforms,
                     self.model_type,
                     self.model_name,
-                    thread_pool=thread_pool)
+                    thread_pool=thread_pool,
+                    input_channel=self.input_channel)
                 res['image'] = im
                 res['im_info'] = im_resize_info
                 res['im_shape'] = im_shape
@@ -172,7 +178,8 @@ class Predictor:
                 self.transforms,
                 self.model_type,
                 self.model_name,
-                thread_pool=thread_pool)
+                thread_pool=thread_pool,
+                input_channel=self.input_channel)
             res['image'] = im
             res['im_info'] = im_info
         return res

+ 2 - 0
paddlex/det.py

@@ -14,6 +14,7 @@
 
 from __future__ import absolute_import
 from . import cv
+from . import tools
 
 FasterRCNN = cv.models.FasterRCNN
 YOLOv3 = cv.models.YOLOv3
@@ -23,3 +24,4 @@ transforms = cv.transforms.det_transforms
 visualize = cv.models.utils.visualize.visualize_detection
 draw_pr_curve = cv.models.utils.visualize.draw_pr_curve
 coco_error_analysis = cv.models.utils.detection_eval.coco_error_analysis
+paste_objects = tools.dataset_generate.det.paste_objects

+ 1 - 0
paddlex/tools/__init__.py

@@ -16,3 +16,4 @@
 
 from .convert import *
 from .split import *
+from .dataset_generate import *

+ 15 - 0
paddlex/tools/dataset_generate/__init__.py

@@ -0,0 +1,15 @@
+# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import det

+ 208 - 0
paddlex/tools/dataset_generate/det.py

@@ -0,0 +1,208 @@
+# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+import os
+import os.path as osp
+import random
+import cv2
+import time
+import numpy as np
+import xml.etree.ElementTree as ET
+import paddlex.utils.logging as logging
+
+
+def write_xml(im_info, label_info, anno_dir):
+    im_fname = im_info['file_name']
+    im_h, im_w, im_c = im_info['image_shape']
+    is_crowd = label_info['is_crowd']
+    gt_class = label_info['gt_class']
+    gt_bbox = label_info['gt_bbox']
+    gt_score = label_info['gt_score']
+    gt_poly = label_info['gt_poly']
+    difficult = label_info['difficult']
+    import xml.dom.minidom as minidom
+    xml_doc = minidom.Document()
+    root = xml_doc.createElement("annotation")
+    xml_doc.appendChild(root)
+    node_filename = xml_doc.createElement("filename")
+    node_filename.appendChild(xml_doc.createTextNode(im_fname))
+    root.appendChild(node_filename)
+    node_size = xml_doc.createElement("size")
+    node_width = xml_doc.createElement("width")
+    node_width.appendChild(xml_doc.createTextNode(str(im_w)))
+    node_size.appendChild(node_width)
+    node_height = xml_doc.createElement("height")
+    node_height.appendChild(xml_doc.createTextNode(str(im_h)))
+    node_size.appendChild(node_height)
+    node_depth = xml_doc.createElement("depth")
+    node_depth.appendChild(xml_doc.createTextNode(str(im_c)))
+    node_size.appendChild(node_depth)
+    root.appendChild(node_size)
+    for i in range(len(label_info['gt_class'])):
+        node_obj = xml_doc.createElement("object")
+        node_name = xml_doc.createElement("name")
+        label = gt_class[i]
+        node_name.appendChild(xml_doc.createTextNode(label))
+        node_obj.appendChild(node_name)
+        node_diff = xml_doc.createElement("difficult")
+        node_diff.appendChild(xml_doc.createTextNode(str(difficult[i][0])))
+        node_obj.appendChild(node_diff)
+        node_box = xml_doc.createElement("bndbox")
+        node_xmin = xml_doc.createElement("xmin")
+        node_xmin.appendChild(xml_doc.createTextNode(str(gt_bbox[i][0])))
+        node_box.appendChild(node_xmin)
+        node_ymin = xml_doc.createElement("ymin")
+        node_ymin.appendChild(xml_doc.createTextNode(str(gt_bbox[i][1])))
+        node_box.appendChild(node_ymin)
+        node_xmax = xml_doc.createElement("xmax")
+        node_xmax.appendChild(xml_doc.createTextNode(str(gt_bbox[i][2])))
+        node_box.appendChild(node_xmax)
+        node_ymax = xml_doc.createElement("ymax")
+        node_ymax.appendChild(xml_doc.createTextNode(str(gt_bbox[i][3])))
+        node_box.appendChild(node_ymax)
+        node_obj.appendChild(node_box)
+        root.appendChild(node_obj)
+    img_name_part = im_fname.split('.')[0]
+    with open(osp.join(anno_dir, img_name_part + ".xml"), 'w') as fxml:
+        xml_doc.writexml(
+            fxml, indent='\t', addindent='\t', newl='\n', encoding="utf-8")
+
+
+def paste_objects(templates, background, save_dir='dataset_clone'):
+    """将目标物体粘贴在背景图片上生成新的图片,并加入到数据集中
+
+    Args:
+        templates (list|tuple):可以将多张图像上的目标物体同时粘贴在同一个背景图片上,
+            因此templates是一个列表,其中每个元素是一个dict,表示一张图片的目标物体。
+            一张图片的目标物体有`image`和`annos`两个关键字,`image`的键值是图像的路径,
+            或者是解码后的排列格式为(H, W, C)且类型为uint8且为BGR格式的数组。
+            图像上可以有多个目标物体,因此`annos`的键值是一个列表,列表中每个元素是一个dict,
+            表示一个目标物体的信息。该dict包含`polygon`和`category`两个关键字,
+            其中`polygon`表示目标物体的边缘坐标,例如[[0, 0], [0, 1], [1, 1], [1, 0]],
+            `category`表示目标物体的类别,例如'dog'。
+        background (dict): 背景图片可以有真值,因此background是一个dict,包含`image`和`annos`
+            两个关键字,`image`的键值是背景图像的路径,或者是解码后的排列格式为(H, W, C)
+            且类型为uint8且为BGR格式的数组。若背景图片上没有真值,则`annos`的键值是空列表[],
+            若有,则`annos`的键值是由多个dict组成的列表,每个dict表示一个物体的信息,
+            包含`bbox`和`category`两个关键字,`bbox`的键值是物体框左上角和右下角的坐标,即
+            [x1, y1, x2, y2],`category`表示目标物体的类别,例如'dog'。
+        save_dir (str):新图片及其标注文件的存储目录。默认值为`dataset_clone`。
+
+    """
+    if not osp.exists(save_dir):
+        os.makedirs(save_dir)
+    image_dir = osp.join(save_dir, 'JPEGImages_clone')
+    anno_dir = osp.join(save_dir, 'Annotations_clone')
+    json_path = osp.join(save_dir, "annotations.json")
+    if not osp.exists(image_dir):
+        os.makedirs(image_dir)
+    if not osp.exists(anno_dir):
+        os.makedirs(anno_dir)
+
+    num_objs = len(background['annos'])
+    for temp in templates:
+        num_objs += len(temp['annos'])
+
+    gt_bbox = np.zeros((num_objs, 4), dtype=np.float32)
+    gt_class = list()
+    gt_score = np.ones((num_objs, 1), dtype=np.float32)
+    is_crowd = np.zeros((num_objs, 1), dtype=np.int32)
+    difficult = np.zeros((num_objs, 1), dtype=np.int32)
+    i = -1
+    for i, back_anno in enumerate(background['annos']):
+        gt_bbox[i] = back_anno['bbox']
+        gt_class.append(back_anno['category'])
+
+    back_im = background['image']
+    if isinstance(back_im, np.ndarray):
+        if len(back_im.shape) != 3:
+            raise Exception(
+                "background image should be 3-dimensions, but now is {}-dimensions".
+                format(len(back_im.shape)))
+    else:
+        try:
+            back_im = cv2.imread(back_im, cv2.IMREAD_UNCHANGED)
+        except:
+            raise TypeError('Can\'t read The image file {}!'.format(back_im))
+    back_annos = background['annos']
+    im_h, im_w, im_c = back_im.shape
+    for temp in templates:
+        temp_im = temp['image']
+        if isinstance(temp_im, np.ndarray):
+            if len(temp_im.shape) != 3:
+                raise Exception(
+                    "template image should be 3-dimensions, but now is {}-dimensions".
+                    format(len(temp_im.shape)))
+        else:
+            try:
+                temp_im = cv2.imread(temp_im, cv2.IMREAD_UNCHANGED)
+            except:
+                raise TypeError('Can\'t read The image file {}!'.format(
+                    temp_im))
+        temp_annos = temp['annos']
+        for temp_anno in temp_annos:
+            temp_mask = np.zeros(temp_im.shape, temp_im.dtype)
+            temp_poly = np.array(temp_anno['polygon'], np.int32)
+            temp_category = temp_anno['category']
+            cv2.fillPoly(temp_mask, [temp_poly], (255, 255, 255))
+            x_list = [temp_poly[i][0] for i in range(len(temp_poly))]
+            y_list = [temp_poly[i][1] for i in range(len(temp_poly))]
+            temp_poly_w = max(x_list) - min(x_list)
+            temp_poly_h = max(y_list) - min(y_list)
+            found = False
+            while not found:
+                center_x = random.randint(1, im_w - 1)
+                center_y = random.randint(1, im_h - 1)
+                if center_x < temp_poly_w / 2 or center_x > im_w - temp_poly_w / 2 - 1 or \
+                   center_y < temp_poly_h / 2 or center_y > im_h - temp_poly_h / 2 - 1:
+                    found = False
+                    continue
+                if len(back_annos) == 0:
+                    found = True
+                for back_anno in back_annos:
+                    x1, y1, x2, y2 = back_anno['bbox']
+                    if center_x > x1 and center_x < x2 and center_y > y1 and center_y < y2:
+                        found = False
+                        continue
+                    found = True
+            center = (center_x, center_y)
+            back_im = cv2.seamlessClone(temp_im, back_im, temp_mask, center,
+                                        cv2.MIXED_CLONE)
+            i += 1
+            x1 = center[0] - temp_poly_w / 2
+            x2 = center[0] + temp_poly_w / 2
+            y1 = center[1] - temp_poly_h / 2
+            y2 = center[1] + temp_poly_h / 2
+            gt_bbox[i] = [x1, y1, x2, y2]
+            gt_class.append(temp_category)
+
+    im_fname = str(int(time.time() * 1000)) + '.jpg'
+    im_info = {
+        'file_name': im_fname,
+        'image_shape': [im_h, im_w, im_c],
+    }
+    label_info = {
+        'is_crowd': is_crowd,
+        'gt_class': gt_class,
+        'gt_bbox': gt_bbox,
+        'gt_score': gt_score,
+        'difficult': difficult,
+        'gt_poly': [],
+    }
+    cv2.imwrite(osp.join(image_dir, im_fname), back_im.astype('uint8'))
+    write_xml(im_info, label_info, anno_dir)
+    logging.info("Gegerated image is saved in {}".format(image_dir))
+    logging.info("Generated Annotation is saved as xml files in {}".format(
+        anno_dir))