Skip to content

Commit

Permalink
✨ feat: 添加了数据集制作和模型训练的支持
Browse files Browse the repository at this point in the history
  • Loading branch information
henryzhuhr committed Jun 1, 2024
1 parent 7802b7a commit 78304c6
Show file tree
Hide file tree
Showing 28 changed files with 780 additions and 188 deletions.
101 changes: 101 additions & 0 deletions dataset-process.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from typing import List, Set
import os
import argparse
import shutil
import yaml

from utils.dataset import get_all_label_files


class DatasetProcessArgs:
def __init__(self) -> None:
args = self.get_args()
self.datadir: str = os.path.expandvars(os.path.expanduser(args.datadir))

# check if the directory exists
if not os.path.exists(self.datadir):
raise FileNotFoundError(
f"Directory '{self.datadir}' not found, check '--datadir {args.datadir}'"
)

@staticmethod
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument("--datadir", type=str, default="~/data/bottle")
return parser.parse_args()


def main():
args = DatasetProcessArgs()

# -- get all label files, type: List[ImageLabel]
label_file_list = get_all_label_files(args.datadir)

# -- organize the dataset into a new directory
organized_datadir = f"{args.datadir}-organized"
if not os.path.exists(organized_datadir):
os.makedirs(organized_datadir, exist_ok=False)
else:
raise FileExistsError(
f"Directory '{organized_datadir}' already exists, "
f"delete by 'rm -rf {organized_datadir}'"
)

images_dir = os.path.join(organized_datadir, "images")
os.makedirs(images_dir, exist_ok=True)
labels_dir = os.path.join(organized_datadir, "labels")
os.makedirs(labels_dir, exist_ok=True)

# -- get all classes first to order classes
class_set: Set[str] = set()
for label_file in label_file_list:
for cls in label_file.get_all_class():
class_set.add(cls)
class_map = {cls: i for i, cls in enumerate(sorted(list(class_set)))}

with open(os.path.join(organized_datadir, "classes.txt"), "w") as f:
for icls, cls in enumerate(class_map):
f.write(f"{icls} {cls}\n")

# -- copy image files and create label files
image_file_list: List[str] = []
for label_file in label_file_list:
src_image_file = os.path.join(
(label_file.parent_dir), label_file.image_file
)
dst_image_file = os.path.join(
images_dir, dst_file_name := os.path.basename(label_file.image_file)
)
shutil.copy(src_image_file, dst_image_file)
image_file_list.append(dst_file_name)

obj_list = label_file.to_coco()
label_file_name = os.path.splitext(label_file.label_file)[0] + ".txt"
with open(os.path.join(labels_dir, label_file_name), "w") as f:
for obj in obj_list: # [cls_name, x, y, w, h]
cls_name, *bbox = obj
cls_id = class_map[cls_name]
bbox_str = " ".join([str(x)[:8] for x in bbox])
f.write(f"{cls_id} {bbox_str}\n")

# -- create train.txt and dataset.yaml which are used in training
with open(os.path.join(organized_datadir, "train.relpath.txt"), "w") as f:
for image_file in image_file_list:
f.write(f"images/{image_file}\n")

with open(os.path.join(organized_datadir, "train.txt"), "w") as f:
for image_file in image_file_list:
f.write(f"{organized_datadir}/images/{image_file}\n")

with open(os.path.join(organized_datadir, "dataset.yaml"), "w") as f:
data_dict = {
"path": organized_datadir,
"train": "./train.txt",
"val": "./train.txt",
"names": {i: cls for cls, i in class_map.items()},
}
yaml.dump(data_dict, f, sort_keys=False)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export default defineConfig({
text: '「项目文档」',
items: [
{ text: '安装环境', link: '/install' },
{ text: '模型训练', link: '/train' },
{ text: '数据集制作', link: '/dataset' },
{ text: '模型训练', link: '/train' },
{ text: '模型部署', link: '/deploy' },
// { text: '目标检测YOLOv5项目', link: '/yolo' },
]
Expand Down
203 changes: 203 additions & 0 deletions docs/dataset.legacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
---
lastUpdated: true
editLink: true
footer: true
outline: deep
---

# 目标检测数据集制作

## 图片收集


## 启动标注工具

```bash
pip install -r requirements/requirements.train.txt
```






这是一个目标检测预处理的说明,事实上这个版本已经用了很多年了,比较老了,

## 功能
- [x] VOC -> yolo
- [x] [yolo -> coco](#yolo-to-coco): 转化为 COCO 数据集用于 [ultralytics/yolov5](https://github.com/ultralytics/yolov5) 项目


## 数据集制作

> 需要说明的是,脚本只能按照标注进行分类的数据进行处理,如果一张图片有多个标注,是无法处理的
> 该项目仅仅是脚本库,不包括数据集
### 数据采集与归档

将采集到的数据放置在 `dataset-custom/src` 目录下面,并且按照类别归档至对应文件夹下,参考的文件目如下

数据集目录结构如下
```bash
·
└── objdect-dataset # 数据集目录
├── src # 原始数据,按照类别进行归档
│ ├─ A # 类别 A
│ ├─ B
│ └─ ...
├── labeled # 压缩、重命名后的文件,在这里进行标注
├── VOC # VOC 标准数据集,用于训练
└── coco # coco 标准数据集,用于训练
```
> `labeled` 目录是用于后续步骤[数据标注](#数据标注)的目录,这样我们可以在不破坏原始数据对情况下完成数据处理,如果不再需要原始数据,在完成此步骤后,可以删除 `src` 目录
数据集的图片的大小不能太大,需要预先压缩尺寸,执行 `objdet_dataset_processor/resize.py`
```bash
python objdet_dataset_processor/resize.py [--dataset-path] [--height] [--is_rename]
# example
python objdet_dataset_processor/resize.py -d ~/data/objdect-dataset --is_rename
```


### 数据标注

在前面步骤中生成的 `labeled` 目录是用于数据标注的目录,选择图像注释工具 labelImg 进行标注。

[labelImg](https://github.com/tzutalin/labelImg) 是 Python 编写、基于 Qt 图形界面的软件,标注以 PASCAL VOC 格式(ImageNet 使用的格式)另存为 `.xml` 文件。此外,它还支持 YOLO 格式。

你可以通过从[源码编译](https://github.com/tzutalin/labelImg)的方式安装,也可以通过 pip3 快速安装
```bash
pip install labelImg
```

安装后,可以在命令行启动
```bash
labelImg
```

在 Ubuntu 下启动后的界面如下(Windows 版本可能略有差异)
![start](./dataset/images/labelImg-start.png)

<!-- ![start](./dataset/images/labelImg-start-1.png) -->

- 打开文件 : 标注单张图像(不推荐使用)
- **打开目录** : 打开数据集存放的目录,目录下应该是图像的位置
- **改变存放目录**: 标注文件 `.xml` 存放的目录
- 下一个图片:
- 上一个图像:
- **验证图像**: 验证标记无误,用于全部数据集标记完成后的检查工作
- **保存**: 保存标记结果,快捷键 `Ctrl+s`
- **数据集格式**: `PascalVOC``YOLO` 可选,一般选择 `PascalVOC` 即可,需要 `YOLO` 可以之后进行转换

点击 `创建区块` 创建一个矩形框,画出范围
![rect](./dataset/images/labelImg-rect-1.png)

每个类别都有对应的颜色加以区分
![rect](./dataset/images/labelImg-rect-3.png)

完成一张图片的标注后,点击 `下一个图片`

- labelImg 快捷键

| 快捷键 | 功能 | 快捷键 | 功能 |
| :----: | :----------------------: | :----: | :--------------: |
| Ctrl+u | 从目录加载所有图像 | w | 创建一个矩形框 |
| Ctrl+R | 更改默认注释目标目录 | d | 下一张图片 |
| Ctrl+s | 保存当前标注结果 | a | 上一张图片 |
| Ctrl+d | 复制当前标签和矩形框 | del | 删除选定的矩形框 |
| space | 将当前图像标记为已验证 | Ctrl+ | 放大 |
| ↑→↓← | 键盘箭头移动选定的矩形框 | Ctrl– | 缩小 |





## 数据集预处理

### 转换至可训练的标准数据集
当标注完成后,我们就需要将图像和标注文件转换为我们所需要的数据格式

可用的转换流程如下
```bash
labeled -> VOC # to PASCAL VOC
└─> YOLO -> COCO # to YOLOv5 COCO
```
- [x] [转换成 VOC 格式](#转换成-VOC-格式)
- [x] [转换成用于 YOLOv5 的 COCO 格式](#转换成用于-YOLOv5-的-COCO-格式)



#### 转换成 VOC 格式

<!-- **VOC2012** 数据集描述:
- **Annotations**: 存放了数据`xml`格式存储的标签,里面包含了每张图片的`bounding box`信息,主要用于**目标检测**。
- **ImageSets**: ImageSets中的Segmentation目录下存放了用于分割的train, val, trainval数据集的索引。
- **JPEGImages**: 这里存放的就是JPG格式的原图,包含17125张彩色图片,但只有一部分(2913张)是用于分割的。
- **SegmentationClass**: 语义分割任务中用到的label图片,PNG格式,共2913张,与原图的每一张图片相对应。
- **SegmentationObject**: 实例分割任务用到的label图片,在语义分割中用不到,这里不详解介绍。
--- -->

转换成 VOC 数据集流程如下
```bash
labeled -> VOC # to PASCAL VOC
```

执行 `labeled-voc.py` 将已经标记好的数据集转化成VOC格式
```bash
python scripts/dataset/labeled-voc.py [--dataset-path] [--height] [--is_rename]
# example
python scripts/dataset/labeled-voc.py
```

运行后会在 VOC 目录下类别文件 `classes.names` 和训练集文件 `train.txt` 、验证集文件 `val.txt`
```bash
·
└── dataset-custom # 数据集文件夹
├── src # 原始数据,按照类别进行归档
├── labeled # 压缩、重命名后的文件,在这里进行标注
└── VOC # VOC 标准数据集,用于训练
├── Annotations
├── ImageSets
│   └── Main
│ ├── classes.names
│ ├── train.txt
│ └── val.txt
└── JPEGImages
```



#### 转换成用于 YOLOv5 的 COCO 格式
```bash
VOC -> YOLO -> COCO # to YOLOv5 COCO
```


执行 `voc-yolo.py` 将数据集从 VOC 转换成 YOLO
```bash
python scripts/dataset/voc-yolo.py [--conf]
```

执行 `yolo-coco.py` 将数据集从 YOLO 转换成 COCO (yolov5)
```bash
python scripts/dataset/yolo-coco.py [--conf]
```

运行后得到 YOLOv5 的 COCO 数据集,包含 `images`,`labels` 目录,目录下分别包含子目录 `train`,`val`
```bash
·
└── dataset-custom # 数据集文件夹
├── src # 原始数据,按照类别进行归档
├── labeled # 压缩、重命名后的文件,在这里进行标注
└── coco # coco 数据集,用于训练
├── images
│   ├── train
│   └── val
└── labels
   ├── train
   └── val
```

## 相关仓库

- [labelImg](https://github.com/tzutalin/labelImg): 用于目标检测数据的图像标注软件
Loading

0 comments on commit 78304c6

Please sign in to comment.