feat: initial HSAP platform

Huaxu Sentinel Active Safety Platform with embedded algorithm code,
Docker Compose setup, and vendored dataset scaffolds for clone-and-run.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-25 16:59:59 +08:00
commit 7c43b44c57
1619 changed files with 373355 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,25 @@
work_dirs
data
cache/
__pycache__/
*/*.un~
.*.swp
logs/
*.egg-info/
*.egg
*.eggs
.ipynb_checkpoints/
output.txt
.vscode/*
.DS_Store
tmp.*
*.pt
*.pth
*.un~
*.so
build
*.jpg

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@@ -0,0 +1,211 @@
<div align="center">
[![PWC](https://img.shields.io/endpoint.svg?url=https://paperswithcode.com/badge/clrnet-cross-layer-refinement-network-for/lane-detection-on-culane)](https://paperswithcode.com/sota/lane-detection-on-culane?p=clrnet-cross-layer-refinement-network-for)
[![PWC](https://img.shields.io/endpoint.svg?url=https://paperswithcode.com/badge/clrnet-cross-layer-refinement-network-for/lane-detection-on-llamas)](https://paperswithcode.com/sota/lane-detection-on-llamas?p=clrnet-cross-layer-refinement-network-for)
[![PWC](https://img.shields.io/endpoint.svg?url=https://paperswithcode.com/badge/clrnet-cross-layer-refinement-network-for/lane-detection-on-tusimple)](https://paperswithcode.com/sota/lane-detection-on-tusimple?p=clrnet-cross-layer-refinement-network-for)
</div>
<div align="center">
# CLRNet: Cross Layer Refinement Network for Lane Detection
</div>
Pytorch implementation of the paper "[CLRNet: Cross Layer Refinement Network for Lane Detection](https://arxiv.org/abs/2203.10350)" (CVPR2022 Acceptance).
## Introduction
![Arch](.github/arch.png)
- CLRNet exploits more contextual information to detect lanes while leveraging local detailed lane features to improve localization accuracy.
- CLRNet achieves SOTA result on CULane, Tusimple, and LLAMAS datasets.
## Installation
### Prerequisites
Only test on Ubuntu18.04 and 20.04 with:
- Python >= 3.8 (tested with Python3.8)
- PyTorch >= 1.6 (tested with Pytorch1.6)
- CUDA (tested with cuda10.2)
- Other dependencies described in `requirements.txt`
### Clone this repository
Clone this code to your workspace.
We call this directory as `$CLRNET_ROOT`
```Shell
git clone https://github.com/Turoad/clrnet
```
### Create a conda virtual environment and activate it (conda is optional)
```Shell
conda create -n clrnet python=3.8 -y
conda activate clrnet
```
### Install dependencies
```Shell
# Install pytorch firstly, the cudatoolkit version should be same in your system.
conda install pytorch torchvision cudatoolkit=10.1 -c pytorch
# Or you can install via pip
pip install torch==1.8.0 torchvision==0.9.0
# Install python packages
python setup.py build develop
```
### Data preparation
#### CULane
Download [CULane](https://xingangpan.github.io/projects/CULane.html). Then extract them to `$CULANEROOT`. Create link to `data` directory.
```Shell
cd $CLRNET_ROOT
mkdir -p data
ln -s $CULANEROOT data/CULane
```
For CULane, you should have structure like this:
```
$CULANEROOT/driver_xx_xxframe # data folders x6
$CULANEROOT/laneseg_label_w16 # lane segmentation labels
$CULANEROOT/list # data lists
```
#### Tusimple
Download [Tusimple](https://github.com/TuSimple/tusimple-benchmark/issues/3). Then extract them to `$TUSIMPLEROOT`. Create link to `data` directory.
```Shell
cd $CLRNET_ROOT
mkdir -p data
ln -s $TUSIMPLEROOT data/tusimple
```
For Tusimple, you should have structure like this:
```
$TUSIMPLEROOT/clips # data folders
$TUSIMPLEROOT/lable_data_xxxx.json # label json file x4
$TUSIMPLEROOT/test_tasks_0627.json # test tasks json file
$TUSIMPLEROOT/test_label.json # test label json file
```
For Tusimple, the segmentation annotation is not provided, hence we need to generate segmentation from the json annotation.
```Shell
python tools/generate_seg_tusimple.py --root $TUSIMPLEROOT
# this will generate seg_label directory
```
#### LLAMAS
Dowload [LLAMAS](https://unsupervised-llamas.com/llamas/). Then extract them to `$LLAMASROOT`. Create link to `data` directory.
```Shell
cd $CLRNET_ROOT
mkdir -p data
ln -s $LLAMASROOT data/llamas
```
Unzip both files (`color_images.zip` and `labels.zip`) into the same directory (e.g., `data/llamas/`), which will be the dataset's root. For LLAMAS, you should have structure like this:
```
$LLAMASROOT/color_images/train # data folders
$LLAMASROOT/color_images/test # data folders
$LLAMASROOT/color_images/valid # data folders
$LLAMASROOT/labels/train # labels folders
$LLAMASROOT/labels/valid # labels folders
```
## Getting Started
### Training
For training, run
```Shell
python main.py [configs/path_to_your_config] --gpus [gpu_num]
```
For example, run
```Shell
python main.py configs/clrnet/clr_resnet18_culane.py --gpus 0
```
### Validation
For testing, run
```Shell
python main.py [configs/path_to_your_config] --[test|validate] --load_from [path_to_your_model] --gpus [gpu_num]
```
For example, run
```Shell
python main.py configs/clrnet/clr_dla34_culane.py --validate --load_from culane_dla34.pth --gpus 0
```
Currently, this code can output the visualization result when testing, just add `--view`.
We will get the visualization result in `work_dirs/xxx/xxx/visualization`.
## Results
![F1 vs. Latency for SOTA methods on the lane detection](.github/latency_f1score.png)
[assets]: https://github.com/turoad/CLRNet/releases
### CULane
| Backbone | mF1 | F1@50 | F1@75 |
| :--- | :---: | :---: | :---:|
| [ResNet-18][assets] | 55.23 | 79.58 | 62.21 |
| [ResNet-34][assets] | 55.14 | 79.73 | 62.11 |
| [ResNet-101][assets] | 55.55| 80.13 | 62.96 |
| [DLA-34][assets] | 55.64| 80.47 | 62.78 |
### TuSimple
| Backbone | F1 | Acc | FDR | FNR |
| :--- | ---: | ---: | ---: | ---: |
| [ResNet-18][assets] | 97.89 | 96.84 | 2.28 | 1.92 |
| [ResNet-34][assets] | 97.82 | 96.87 | 2.27 | 2.08 |
| [ResNet-101][assets] | 97.62| 96.83 | 2.37 | 2.38 |
### LLAMAS
| Backbone | <center> valid <br><center> &nbsp; mF1 &nbsp; &nbsp; &nbsp;F1@50 &nbsp; F1@75 | <center> test <br> F1@50 |
| :---: | :---: | :---:|
| [ResNet-18][assets] | <center> 70.83 &nbsp; &nbsp; 96.93 &nbsp; &nbsp; 85.23 | 96.00 |
| [DLA-34][assets] | <center> 71.57 &nbsp; &nbsp; 97.06 &nbsp; &nbsp; 85.43 | 96.12 |
“F1@50” refers to the official metric, i.e., F1 score when IoU threshold is 0.5 between the gt and prediction. "F1@75" is the F1 score when IoU threshold is 0.75.
## Citation
If our paper and code are beneficial to your work, please consider citing:
```
@InProceedings{Zheng_2022_CVPR,
author = {Zheng, Tu and Huang, Yifei and Liu, Yang and Tang, Wenjian and Yang, Zheng and Cai, Deng and He, Xiaofei},
title = {CLRNet: Cross Layer Refinement Network for Lane Detection},
booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
month = {June},
year = {2022},
pages = {898-907}
}
```
## Acknowledgement
<!--ts-->
* [open-mmlab/mmdetection](https://github.com/open-mmlab/mmdetection)
* [pytorch/vision](https://github.com/pytorch/vision)
* [Turoad/lanedet](https://github.com/Turoad/lanedet)
* [ZJULearning/resa](https://github.com/ZJULearning/resa)
* [cfzd/Ultra-Fast-Lane-Detection](https://github.com/cfzd/Ultra-Fast-Lane-Detection)
* [lucastabelini/LaneATT](https://github.com/lucastabelini/LaneATT)
* [aliyun/conditional-lane-detection](https://github.com/aliyun/conditional-lane-detection)
<!--te-->

View File

@@ -0,0 +1,122 @@
CLRNet 使用说明lane0_copy 数据)
代码目录:/home/chengfanglu/DATA/BK2/CLRNet-main
数据目录:/home/chengfanglu/DATA/lane0_copy/(与 UFLD 相同多包布局)
上游论文原版说明见同目录 README.md。
一、环境安装
source ~/miniconda3/etc/profile.d/conda.sh
cd /home/chengfanglu/DATA/BK2/CLRNet-main
bash scripts/setup_clrnet_env.sh
conda activate clrnet_lane
说明:
环境名默认 clrnet_lane
需要 NVIDIA GPUmain.py 使用 .cuda()
安装包含 PyTorch、mmcv-full、python setup.py develop编译 NMS
二、数据目录
lane0_copy/
DATASET/images、annotations/segmentation_masks、list/
DATASET-AddBy-zhangsan-20260615/(增量包)
lists_merged/(合并列表)
datasets_registry.json别名
重要dataset_path 填父目录 lane0_copy不要只填 DATASET。
数据集类clrnet/datasets/mufld.pyMufldLane从 mask 提取车道折线。
目录规范:/home/chengfanglu/DATA/lane0_copy/DATASETS_LAYOUT.md
三、配置文件
configs/clrnet/clr_resnet18_mufld.py — 推荐1280x720多包
configs/clrnet/clr_resnet18_mufld_smoke.py — 冒烟
请改 configs/clrnet/clr_resnet18_mufld.py
dataset_path = '/home/chengfanglu/DATA/lane0_copy'
train_packs = ['DATASET']
多包train_packs = ['DATASET', 'DATASET-A'],别名见 datasets_registry.json
val_packs = ['DATASET']
pack_list_name = 'list/train_gt.txt'
remerge_lists = False
合并列表输出lane0_copy/lists_merged/train__DATASET__....txt
图像与网络参数:
原图 1280x720
cut_height 160
网络输入 800x320
max_lanes 4
四、训练
conda activate clrnet_lane
cd /home/chengfanglu/DATA/BK2/CLRNet-main
首次冒烟前先建短列表:
head -64 /home/chengfanglu/DATA/lane0_copy/DATASET/list/train_gt.txt > /home/chengfanglu/DATA/lane0_copy/DATASET/list/train_gt_smoke.txt
冒烟:
python main.py configs/clrnet/clr_resnet18_mufld_smoke.py --gpus 0
正式:
python main.py configs/clrnet/clr_resnet18_mufld.py --gpus 0
权重目录work_dirs/clr/mufld_r18/(由 config 里 work_dirs 决定)
多卡示例:--gpus 0 1
常用修改:
batch_size = 16
epochs = 15
optimizer = dict(type='AdamW', lr=1.0e-3)
换 train_packs 后请改 total_iter = (144117 // batch_size + 1) * epochs
五、预生成车道线缓存(推荐)
首轮会从 mask 现场提线,较慢。可先执行:
python tools/generate_mufld_lines.py --data-root /home/chengfanglu/DATA/lane0_copy --list DATASET/list/train_gt.txt
缓存目录lane0_copy/cache/mufld_lines/
六、测试与推理
python main.py configs/clrnet/clr_resnet18_mufld.py --gpus 0 --test --load_from work_dirs/clr/mufld_r18/ckpt/best.pth
--load_from 按实际 ckpt 路径填写)
说明:
没有 CULane 官方评测
预测会写成 lines.txt日志 metric 为占位值
七、增量数据
1建包与 UFLD 共用脚本):
python /home/chengfanglu/DATA/lane0_copy/scripts/build_ufld_pack.py --src /path/to/archive --parent /home/chengfanglu/DATA/lane0_copy --engineer zhangsan --date 20260615
2config 增加包名:
train_packs = ['DATASET', 'DATASET-AddBy-zhangsan-20260615']
remerge_lists = True
3重新训练
python main.py configs/clrnet/clr_resnet18_mufld.py --gpus 0
八、路径速查
代码:/home/chengfanglu/DATA/BK2/CLRNet-main
数据父目录:/home/chengfanglu/DATA/lane0_copy
训练配置configs/clrnet/clr_resnet18_mufld.py
数据集实现clrnet/datasets/mufld.py
安装脚本scripts/setup_clrnet_env.sh

View File

@@ -0,0 +1 @@
from .ops import *

View File

@@ -0,0 +1,7 @@
from .registry import build_dataset, build_dataloader
from .tusimple import TuSimple
from .culane import CULane
from .llamas import LLAMAS
from .mufld import MufldLane
from .process import *

View File

@@ -0,0 +1,67 @@
import os.path as osp
import os
import numpy as np
import cv2
import torch
from torch.utils.data import Dataset
import torchvision
import logging
from .registry import DATASETS
from .process import Process
from clrnet.utils.visualization import imshow_lanes
from mmcv.parallel import DataContainer as DC
@DATASETS.register_module
class BaseDataset(Dataset):
def __init__(self, data_root, split, processes=None, cfg=None):
self.cfg = cfg
self.logger = logging.getLogger(__name__)
self.data_root = data_root
self.training = 'train' in split
self.processes = Process(processes, cfg)
def view(self, predictions, img_metas):
img_metas = [item for img_meta in img_metas.data for item in img_meta]
for lanes, img_meta in zip(predictions, img_metas):
img_name = img_meta['img_name']
img = cv2.imread(osp.join(self.data_root, img_name))
out_file = osp.join(self.cfg.work_dir, 'visualization',
img_name.replace('/', '_'))
lanes = [lane.to_array(self.cfg) for lane in lanes]
imshow_lanes(img, lanes, out_file=out_file)
def __len__(self):
return len(self.data_infos)
def __getitem__(self, idx):
data_info = self.data_infos[idx]
img = cv2.imread(data_info['img_path'])
img = img[self.cfg.cut_height:, :, :]
sample = data_info.copy()
sample.update({'img': img})
if self.training:
label = cv2.imread(sample['mask_path'], cv2.IMREAD_UNCHANGED)
if len(label.shape) > 2:
label = label[:, :, 0]
label = label.squeeze()
label = label[self.cfg.cut_height:, :]
sample.update({'mask': label})
if self.cfg.cut_height != 0:
new_lanes = []
for i in sample['lanes']:
lanes = []
for p in i:
lanes.append((p[0], p[1] - self.cfg.cut_height))
new_lanes.append(lanes)
sample.update({'lanes': new_lanes})
sample = self.processes(sample)
meta = {'full_img_path': data_info['img_path'],
'img_name': data_info['img_name']}
meta = DC(meta, cpu_only=True)
sample.update({'meta': meta})
return sample

View File

@@ -0,0 +1,143 @@
import os
import os.path as osp
import numpy as np
from .base_dataset import BaseDataset
from .registry import DATASETS
import clrnet.utils.culane_metric as culane_metric
import cv2
from tqdm import tqdm
import logging
import pickle as pkl
LIST_FILE = {
'train': 'list/train_gt.txt',
'val': 'list/val.txt',
'test': 'list/test.txt',
}
CATEGORYS = {
'normal': 'list/test_split/test0_normal.txt',
'crowd': 'list/test_split/test1_crowd.txt',
'hlight': 'list/test_split/test2_hlight.txt',
'shadow': 'list/test_split/test3_shadow.txt',
'noline': 'list/test_split/test4_noline.txt',
'arrow': 'list/test_split/test5_arrow.txt',
'curve': 'list/test_split/test6_curve.txt',
'cross': 'list/test_split/test7_cross.txt',
'night': 'list/test_split/test8_night.txt',
}
@DATASETS.register_module
class CULane(BaseDataset):
def __init__(self, data_root, split, processes=None, cfg=None):
super().__init__(data_root, split, processes=processes, cfg=cfg)
self.list_path = osp.join(data_root, LIST_FILE[split])
self.split = split
self.load_annotations()
def load_annotations(self):
self.logger.info('Loading CULane annotations...')
# Waiting for the dataset to load is tedious, let's cache it
os.makedirs('cache', exist_ok=True)
cache_path = 'cache/culane_{}.pkl'.format(self.split)
if os.path.exists(cache_path):
with open(cache_path, 'rb') as cache_file:
self.data_infos = pkl.load(cache_file)
self.max_lanes = max(
len(anno['lanes']) for anno in self.data_infos)
return
self.data_infos = []
with open(self.list_path) as list_file:
for line in list_file:
infos = self.load_annotation(line.split())
self.data_infos.append(infos)
# cache data infos to file
with open(cache_path, 'wb') as cache_file:
pkl.dump(self.data_infos, cache_file)
def load_annotation(self, line):
infos = {}
img_line = line[0]
img_line = img_line[1 if img_line[0] == '/' else 0::]
img_path = os.path.join(self.data_root, img_line)
infos['img_name'] = img_line
infos['img_path'] = img_path
if len(line) > 1:
mask_line = line[1]
mask_line = mask_line[1 if mask_line[0] == '/' else 0::]
mask_path = os.path.join(self.data_root, mask_line)
infos['mask_path'] = mask_path
if len(line) > 2:
exist_list = [int(l) for l in line[2:]]
infos['lane_exist'] = np.array(exist_list)
anno_path = img_path[:-3] + 'lines.txt' # remove sufix jpg and add lines.txt
with open(anno_path, 'r') as anno_file:
data = [
list(map(float, line.split()))
for line in anno_file.readlines()
]
lanes = [[(lane[i], lane[i + 1]) for i in range(0, len(lane), 2)
if lane[i] >= 0 and lane[i + 1] >= 0] for lane in data]
lanes = [list(set(lane)) for lane in lanes] # remove duplicated points
lanes = [lane for lane in lanes
if len(lane) > 2] # remove lanes with less than 2 points
lanes = [sorted(lane, key=lambda x: x[1])
for lane in lanes] # sort by y
infos['lanes'] = lanes
return infos
def get_prediction_string(self, pred):
ys = np.arange(270, 590, 8) / self.cfg.ori_img_h
out = []
for lane in pred:
xs = lane(ys)
valid_mask = (xs >= 0) & (xs < 1)
xs = xs * self.cfg.ori_img_w
lane_xs = xs[valid_mask]
lane_ys = ys[valid_mask] * self.cfg.ori_img_h
lane_xs, lane_ys = lane_xs[::-1], lane_ys[::-1]
lane_str = ' '.join([
'{:.5f} {:.5f}'.format(x, y) for x, y in zip(lane_xs, lane_ys)
])
if lane_str != '':
out.append(lane_str)
return '\n'.join(out)
def evaluate(self, predictions, output_basedir):
loss_lines = [[], [], [], []]
print('Generating prediction output...')
for idx, pred in enumerate(predictions):
output_dir = os.path.join(
output_basedir,
os.path.dirname(self.data_infos[idx]['img_name']))
output_filename = os.path.basename(
self.data_infos[idx]['img_name'])[:-3] + 'lines.txt'
os.makedirs(output_dir, exist_ok=True)
output = self.get_prediction_string(pred)
with open(os.path.join(output_dir, output_filename),
'w') as out_file:
out_file.write(output)
for cate, cate_file in CATEGORYS.items():
result = culane_metric.eval_predictions(output_basedir,
self.data_root,
os.path.join(self.data_root, cate_file),
iou_thresholds=[0.5],
official=True)
result = culane_metric.eval_predictions(output_basedir,
self.data_root,
self.list_path,
iou_thresholds=np.linspace(0.5, 0.95, 10),
official=True)
return result[0.5]['F1']

View File

@@ -0,0 +1,180 @@
import os
import pickle as pkl
import cv2
from .registry import DATASETS
import numpy as np
from tqdm import tqdm
from .base_dataset import BaseDataset
TRAIN_LABELS_DIR = 'labels/train'
TEST_LABELS_DIR = 'labels/valid'
TEST_IMGS_DIR = 'color_images/test'
SPLIT_DIRECTORIES = {'train': 'labels/train', 'val': 'labels/valid'}
from clrnet.utils.llamas_utils import get_horizontal_values_for_four_lanes
import clrnet.utils.llamas_metric as llamas_metric
@DATASETS.register_module
class LLAMAS(BaseDataset):
def __init__(self, data_root, split='train', processes=None, cfg=None):
self.split = split
self.data_root = data_root
super().__init__(data_root, split, processes, cfg)
if split != 'test' and split not in SPLIT_DIRECTORIES.keys():
raise Exception('Split `{}` does not exist.'.format(split))
if split != 'test':
self.labels_dir = os.path.join(self.data_root,
SPLIT_DIRECTORIES[split])
self.data_infos = []
self.load_annotations()
def get_img_heigth(self, _):
return self.cfg.ori_img_h
def get_img_width(self, _):
return self.cfg.ori_img_w
def get_metrics(self, lanes, _):
# Placeholders
return [0] * len(lanes), [0] * len(lanes), [1] * len(lanes), [
1
] * len(lanes)
def get_img_path(self, json_path):
# /foo/bar/test/folder/image_label.ext --> test/folder/image_label.ext
base_name = '/'.join(json_path.split('/')[-3:])
image_path = os.path.join(
'color_images', base_name.replace('.json', '_color_rect.png'))
return image_path
def get_img_name(self, json_path):
base_name = (json_path.split('/')[-1]).replace('.json',
'_color_rect.png')
return base_name
def get_json_paths(self):
json_paths = []
for root, _, files in os.walk(self.labels_dir):
for file in files:
if file.endswith(".json"):
json_paths.append(os.path.join(root, file))
return json_paths
def load_annotations(self):
# the labels are not public for the test set yet
if self.split == 'test':
imgs_dir = os.path.join(self.data_root, TEST_IMGS_DIR)
self.data_infos = [{
'img_path':
os.path.join(root, file),
'img_name':
os.path.join(TEST_IMGS_DIR,
root.split('/')[-1], file),
'lanes': [],
'relative_path':
os.path.join(root.split('/')[-1], file)
} for root, _, files in os.walk(imgs_dir) for file in files
if file.endswith('.png')]
self.data_infos = sorted(self.data_infos,
key=lambda x: x['img_path'])
return
# Waiting for the dataset to load is tedious, let's cache it
os.makedirs('cache', exist_ok=True)
cache_path = 'cache/llamas_{}.pkl'.format(self.split)
if os.path.exists(cache_path):
with open(cache_path, 'rb') as cache_file:
self.data_infos = pkl.load(cache_file)
self.max_lanes = max(
len(anno['lanes']) for anno in self.data_infos)
return
self.max_lanes = 0
print("Searching annotation files...")
json_paths = self.get_json_paths()
print('{} annotations found.'.format(len(json_paths)))
for json_path in tqdm(json_paths):
lanes = get_horizontal_values_for_four_lanes(json_path)
lanes = [[(x, y) for x, y in zip(lane, range(self.cfg.ori_img_h))
if x >= 0] for lane in lanes]
lanes = [lane for lane in lanes if len(lane) > 0]
lanes = [list(set(lane))
for lane in lanes] # remove duplicated points
lanes = [lane for lane in lanes
if len(lane) > 2] # remove lanes with less than 2 points
lanes = [sorted(lane, key=lambda x: x[1])
for lane in lanes] # sort by y
lanes.sort(key=lambda lane: lane[0][0])
mask_path = json_path.replace('.json', '.png')
# generate seg labels
seg = np.zeros((717, 1276, 3))
for i, lane in enumerate(lanes):
for j in range(0, len(lane) - 1):
cv2.line(seg, (round(lane[j][0]), lane[j][1]),
(round(lane[j + 1][0]), lane[j + 1][1]),
(i + 1, i + 1, i + 1),
thickness=15)
cv2.imwrite(mask_path, seg)
relative_path = self.get_img_path(json_path)
img_path = os.path.join(self.data_root, relative_path)
self.max_lanes = max(self.max_lanes, len(lanes))
self.data_infos.append({
'img_path': img_path,
'img_name': relative_path,
'mask_path': mask_path,
'lanes': lanes,
'relative_path': relative_path
})
with open(cache_path, 'wb') as cache_file:
pkl.dump(self.data_infos, cache_file)
def assign_class_to_lanes(self, lanes):
return {
label: value
for label, value in zip(['l0', 'l1', 'r0', 'r1'], lanes)
}
def get_prediction_string(self, pred):
ys = np.arange(300, 717, 1) / (self.cfg.ori_img_h - 1)
out = []
for lane in pred:
xs = lane(ys)
valid_mask = (xs >= 0) & (xs < 1)
xs = xs * (self.cfg.ori_img_w - 1)
lane_xs = xs[valid_mask]
lane_ys = ys[valid_mask] * (self.cfg.ori_img_h - 1)
lane_xs, lane_ys = lane_xs[::-1], lane_ys[::-1]
lane_str = ' '.join([
'{:.5f} {:.5f}'.format(x, y) for x, y in zip(lane_xs, lane_ys)
])
if lane_str != '':
out.append(lane_str)
return '\n'.join(out)
def evaluate(self, predictions, output_basedir):
print('Generating prediction output...')
for idx, pred in enumerate(predictions):
relative_path = self.data_infos[idx]['relative_path']
output_filename = '/'.join(relative_path.split('/')[-2:]).replace(
'_color_rect.png', '.lines.txt')
output_filepath = os.path.join(output_basedir, output_filename)
os.makedirs(os.path.dirname(output_filepath), exist_ok=True)
output = self.get_prediction_string(pred)
with open(output_filepath, 'w') as out_file:
out_file.write(output)
if self.split == 'test':
return None
result = llamas_metric.eval_predictions(output_basedir,
self.labels_dir,
iou_thresholds=np.linspace(0.5, 0.95, 10),
unofficial=False)
return result[0.5]['F1']

View File

@@ -0,0 +1,188 @@
import os
import os.path as osp
import pickle as pkl
import cv2
import numpy as np
from .base_dataset import BaseDataset
from .registry import DATASETS
from clrnet.utils.mask_to_lanes import lanes_from_mask, normalize_mask_labels
from clrnet.utils.dataset_packs import resolve_list_file
DEFAULT_LIST = {
"train": "list/train_gt.txt",
"val": "list/val_gt.txt",
"test": "list/test_gt.txt",
}
@DATASETS.register_module
class MufldLane(BaseDataset):
"""
MUFLD / lane0_copy DATASET packs.
list: <img_rel> <mask_rel> per line; lanes from mask or cached .lines.txt
"""
def __init__(
self,
data_root,
split,
processes=None,
cfg=None,
list_file=None,
):
super().__init__(data_root, split, processes=processes, cfg=cfg)
self.split = split
if list_file is None:
list_file = getattr(cfg, f"{split}_list_file", None)
if list_file is None:
list_file = resolve_list_file(cfg, split)
if list_file is None:
rel = DEFAULT_LIST.get(split, DEFAULT_LIST["train"])
packs = getattr(cfg, "train_packs" if split == "train" else "val_packs", None)
if packs:
pack = packs[0] if isinstance(packs, (list, tuple)) else packs
list_file = f"{pack}/{rel}"
else:
list_file = rel
if osp.isabs(list_file):
self.list_path = list_file
else:
self.list_path = osp.join(data_root, list_file)
self.sample_ys = list(getattr(cfg, "sample_y", range(710, 150, -10)))
self.num_lanes = getattr(cfg, "max_lanes", 4)
self.lines_cache = getattr(cfg, "lines_cache_dir", "cache/mufld_lines")
self.load_annotations()
def load_annotations(self):
self.logger.info("Loading MufldLane annotations from %s", self.list_path)
os.makedirs("cache", exist_ok=True)
cache_key = self.list_path.replace("/", "_")
cache_path = osp.join("cache", f"mufld_{self.split}_{cache_key}.pkl")
if osp.exists(cache_path):
with open(cache_path, "rb") as f:
self.data_infos = pkl.load(f)
self.max_lanes = max(len(a["lanes"]) for a in self.data_infos) if self.data_infos else self.num_lanes
return
self.data_infos = []
with open(self.list_path) as f:
for line in f:
parts = line.strip().split()
if len(parts) < 2:
continue
info = self.load_annotation(parts)
if info and len(info.get("lanes", [])) > 0:
self.data_infos.append(info)
with open(cache_path, "wb") as f:
pkl.dump(self.data_infos, f)
self.max_lanes = max(len(a["lanes"]) for a in self.data_infos) if self.data_infos else self.num_lanes
self.logger.info("Loaded %d samples, max_lanes=%d", len(self.data_infos), self.max_lanes)
def _lines_path(self, img_path: str) -> str:
base = img_path[:-4] if img_path.lower().endswith((".jpg", ".png")) else img_path
cache_root = osp.join(self.data_root, self.lines_cache)
rel = osp.relpath(base, self.data_root)
return osp.join(cache_root, rel + ".lines.txt")
def load_annotation(self, line):
img_line = line[0].lstrip("/")
mask_line = line[1].lstrip("/")
img_path = osp.join(self.data_root, img_line)
mask_path = osp.join(self.data_root, mask_line)
infos = {
"img_name": img_line,
"img_path": img_path,
"mask_path": mask_path,
}
if len(line) > 2:
infos["lane_exist"] = np.array([int(x) for x in line[2:]])
lines_path = self._lines_path(img_path)
if osp.isfile(lines_path):
with open(lines_path) as f:
data = [list(map(float, ln.split())) for ln in f.readlines() if ln.strip()]
lanes = [
[(lane[i], lane[i + 1]) for i in range(0, len(lane), 2) if lane[i] >= 0 and lane[i + 1] >= 0]
for lane in data
]
elif osp.isfile(mask_path):
mask = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED)
if mask is None:
return None
if mask.ndim > 2:
mask = mask[:, :, 0]
lanes = lanes_from_mask(mask, self.sample_ys, self.num_lanes)
if getattr(self.cfg, "write_lines_cache", False):
os.makedirs(osp.dirname(lines_path), exist_ok=True)
with open(lines_path, "w") as out:
for lane in lanes:
out.write(" ".join(f"{x:.5f} {y:.5f}" for x, y in lane) + "\n")
else:
return None
lanes = [lane for lane in lanes if len(lane) > 2]
lanes = [sorted(lane, key=lambda x: x[1]) for lane in lanes]
infos["lanes"] = lanes
return infos
def __getitem__(self, idx):
data_info = self.data_infos[idx]
img = cv2.imread(data_info["img_path"])
if img is None:
raise FileNotFoundError(data_info["img_path"])
img = img[self.cfg.cut_height :, :, :]
sample = data_info.copy()
sample.update({"img": img})
if self.training:
label = cv2.imread(sample["mask_path"], cv2.IMREAD_UNCHANGED)
if label is None:
raise FileNotFoundError(sample["mask_path"])
if label.ndim > 2:
label = label[:, :, 0]
label = normalize_mask_labels(label.squeeze(), self.num_lanes)
label = label[self.cfg.cut_height :, :]
sample.update({"mask": label})
if self.cfg.cut_height != 0:
new_lanes = []
for lane in sample["lanes"]:
new_lanes.append([(p[0], p[1] - self.cfg.cut_height) for p in lane])
sample.update({"lanes": new_lanes})
from mmcv.parallel import DataContainer as DC
from clrnet.datasets.process import Process
sample = self.processes(sample)
meta = {"full_img_path": data_info["img_path"], "img_name": data_info["img_name"]}
sample.update({"meta": DC(meta, cpu_only=True)})
return sample
def get_prediction_string(self, pred):
ys = np.array(self.sample_ys) / self.cfg.ori_img_h
out = []
for lane in pred:
xs = lane(ys)
valid = (xs >= 0) & (xs < 1)
xs = xs[valid] * self.cfg.ori_img_w
lane_ys = ys[valid] * self.cfg.ori_img_h
xs, lane_ys = xs[::-1], lane_ys[::-1]
s = " ".join(f"{x:.5f} {y:.5f}" for x, y in zip(xs, lane_ys))
if s:
out.append(s)
return "\n".join(out)
def evaluate(self, predictions, output_basedir):
os.makedirs(output_basedir, exist_ok=True)
for idx, pred in enumerate(predictions):
rel = self.data_infos[idx]["img_name"]
out_dir = osp.join(output_basedir, osp.dirname(rel))
os.makedirs(out_dir, exist_ok=True)
out_file = osp.join(out_dir, osp.basename(rel)[:-4] + ".lines.txt")
with open(out_file, "w") as f:
f.write(self.get_prediction_string(pred))
self.logger.info("Wrote predictions under %s (MUFLD: no CULane official eval)", output_basedir)
return 0.0

View File

@@ -0,0 +1,21 @@
from .transforms import (RandomLROffsetLABEL, RandomUDoffsetLABEL, Resize,
RandomCrop, CenterCrop, RandomRotation, RandomBlur,
RandomHorizontalFlip, Normalize, ToTensor)
from .generate_lane_line import GenerateLaneLine
from .process import Process
__all__ = [
'Process',
'RandomLROffsetLABEL',
'RandomUDoffsetLABEL',
'Resize',
'RandomCrop',
'CenterCrop',
'RandomRotation',
'RandomBlur',
'RandomHorizontalFlip',
'Normalize',
'ToTensor',
'GenerateLaneLine',
]

View File

@@ -0,0 +1,218 @@
import math
import numpy as np
import cv2
import imgaug.augmenters as iaa
from imgaug.augmentables.lines import LineString, LineStringsOnImage
from imgaug.augmentables.segmaps import SegmentationMapsOnImage
from scipy.interpolate import InterpolatedUnivariateSpline
from clrnet.datasets.process.transforms import CLRTransforms
from ..registry import PROCESS
@PROCESS.register_module
class GenerateLaneLine(object):
def __init__(self, transforms=None, cfg=None, training=True):
self.transforms = transforms
self.img_w, self.img_h = cfg.img_w, cfg.img_h
self.num_points = cfg.num_points
self.n_offsets = cfg.num_points
self.n_strips = cfg.num_points - 1
self.strip_size = self.img_h / self.n_strips
self.max_lanes = cfg.max_lanes
self.offsets_ys = np.arange(self.img_h, -1, -self.strip_size)
self.training = training
if transforms is None:
transforms = CLRTransforms(self.img_h, self.img_w)
if transforms is not None:
img_transforms = []
for aug in transforms:
p = aug['p']
if aug['name'] != 'OneOf':
img_transforms.append(
iaa.Sometimes(p=p,
then_list=getattr(
iaa,
aug['name'])(**aug['parameters'])))
else:
img_transforms.append(
iaa.Sometimes(
p=p,
then_list=iaa.OneOf([
getattr(iaa,
aug_['name'])(**aug_['parameters'])
for aug_ in aug['transforms']
])))
else:
img_transforms = []
self.transform = iaa.Sequential(img_transforms)
def lane_to_linestrings(self, lanes):
lines = []
for lane in lanes:
lines.append(LineString(lane))
return lines
def sample_lane(self, points, sample_ys):
# this function expects the points to be sorted
points = np.array(points)
if not np.all(points[1:, 1] < points[:-1, 1]):
raise Exception('Annotaion points have to be sorted')
x, y = points[:, 0], points[:, 1]
# interpolate points inside domain
assert len(points) > 1
interp = InterpolatedUnivariateSpline(y[::-1],
x[::-1],
k=min(3,
len(points) - 1))
domain_min_y = y.min()
domain_max_y = y.max()
sample_ys_inside_domain = sample_ys[(sample_ys >= domain_min_y)
& (sample_ys <= domain_max_y)]
assert len(sample_ys_inside_domain) > 0
interp_xs = interp(sample_ys_inside_domain)
# extrapolate lane to the bottom of the image with a straight line using the 2 points closest to the bottom
two_closest_points = points[:2]
extrap = np.polyfit(two_closest_points[:, 1],
two_closest_points[:, 0],
deg=1)
extrap_ys = sample_ys[sample_ys > domain_max_y]
extrap_xs = np.polyval(extrap, extrap_ys)
all_xs = np.hstack((extrap_xs, interp_xs))
# separate between inside and outside points
inside_mask = (all_xs >= 0) & (all_xs < self.img_w)
xs_inside_image = all_xs[inside_mask]
xs_outside_image = all_xs[~inside_mask]
return xs_outside_image, xs_inside_image
def filter_lane(self, lane):
assert lane[-1][1] <= lane[0][1]
filtered_lane = []
used = set()
for p in lane:
if p[1] not in used:
filtered_lane.append(p)
used.add(p[1])
return filtered_lane
def transform_annotation(self, anno, img_wh=None):
img_w, img_h = self.img_w, self.img_h
old_lanes = anno['lanes']
# removing lanes with less than 2 points
old_lanes = filter(lambda x: len(x) > 1, old_lanes)
# sort lane points by Y (bottom to top of the image)
old_lanes = [sorted(lane, key=lambda x: -x[1]) for lane in old_lanes]
# remove points with same Y (keep first occurrence)
old_lanes = [self.filter_lane(lane) for lane in old_lanes]
# normalize the annotation coordinates
old_lanes = [[[
x * self.img_w / float(img_w), y * self.img_h / float(img_h)
] for x, y in lane] for lane in old_lanes]
# create tranformed annotations
lanes = np.ones(
(self.max_lanes, 2 + 1 + 1 + 2 + self.n_offsets), dtype=np.float32
) * -1e5 # 2 scores, 1 start_y, 1 start_x, 1 theta, 1 length, S+1 coordinates
lanes_endpoints = np.ones((self.max_lanes, 2))
# lanes are invalid by default
lanes[:, 0] = 1
lanes[:, 1] = 0
for lane_idx, lane in enumerate(old_lanes):
if lane_idx >= self.max_lanes:
break
try:
xs_outside_image, xs_inside_image = self.sample_lane(
lane, self.offsets_ys)
except AssertionError:
continue
if len(xs_inside_image) <= 1:
continue
all_xs = np.hstack((xs_outside_image, xs_inside_image))
lanes[lane_idx, 0] = 0
lanes[lane_idx, 1] = 1
lanes[lane_idx, 2] = len(xs_outside_image) / self.n_strips
lanes[lane_idx, 3] = xs_inside_image[0]
thetas = []
for i in range(1, len(xs_inside_image)):
theta = math.atan(
i * self.strip_size /
(xs_inside_image[i] - xs_inside_image[0] + 1e-5)) / math.pi
theta = theta if theta > 0 else 1 - abs(theta)
thetas.append(theta)
theta_far = sum(thetas) / len(thetas)
# lanes[lane_idx,
# 4] = (theta_closest + theta_far) / 2 # averaged angle
lanes[lane_idx, 4] = theta_far
lanes[lane_idx, 5] = len(xs_inside_image)
lanes[lane_idx, 6:6 + len(all_xs)] = all_xs
lanes_endpoints[lane_idx, 0] = (len(all_xs) - 1) / self.n_strips
lanes_endpoints[lane_idx, 1] = xs_inside_image[-1]
new_anno = {
'label': lanes,
'old_anno': anno,
'lane_endpoints': lanes_endpoints
}
return new_anno
def linestrings_to_lanes(self, lines):
lanes = []
for line in lines:
lanes.append(line.coords)
return lanes
def __call__(self, sample):
img_org = sample['img']
line_strings_org = self.lane_to_linestrings(sample['lanes'])
line_strings_org = LineStringsOnImage(line_strings_org,
shape=img_org.shape)
for i in range(30):
if self.training:
mask_org = SegmentationMapsOnImage(sample['mask'],
shape=img_org.shape)
img, line_strings, seg = self.transform(
image=img_org.copy().astype(np.uint8),
line_strings=line_strings_org,
segmentation_maps=mask_org)
else:
img, line_strings = self.transform(
image=img_org.copy().astype(np.uint8),
line_strings=line_strings_org)
line_strings.clip_out_of_image_()
new_anno = {'lanes': self.linestrings_to_lanes(line_strings)}
try:
annos = self.transform_annotation(new_anno,
img_wh=(self.img_w,
self.img_h))
label = annos['label']
lane_endpoints = annos['lane_endpoints']
break
except:
if (i + 1) == 30:
self.logger.critical(
'Transform annotation failed 30 times :(')
exit()
sample['img'] = img.astype(np.float32) / 255.
sample['lane_line'] = label
sample['lanes_endpoints'] = lane_endpoints
sample['gt_points'] = new_anno['lanes']
sample['seg'] = seg.get_arr() if self.training else np.zeros(
img_org.shape)
return sample

View File

@@ -0,0 +1,48 @@
import collections
from clrnet.utils import build_from_cfg
from ..registry import PROCESS
class Process(object):
"""Compose multiple process sequentially.
Args:
process (Sequence[dict | callable]): Sequence of process object or
config dict to be composed.
"""
def __init__(self, processes, cfg):
assert isinstance(processes, collections.abc.Sequence)
self.processes = []
for process in processes:
if isinstance(process, dict):
process = build_from_cfg(process,
PROCESS,
default_args=dict(cfg=cfg))
self.processes.append(process)
elif callable(process):
self.processes.append(process)
else:
raise TypeError('process must be callable or a dict')
def __call__(self, data):
"""Call function to apply processes sequentially.
Args:
data (dict): A result dict contains the data to process.
Returns:
dict: Processed data.
"""
for t in self.processes:
data = t(data)
if data is None:
return None
return data
def __repr__(self):
format_string = self.__class__.__name__ + '('
for t in self.processes:
format_string += '\n'
format_string += f' {t}'
format_string += '\n)'
return format_string

View File

@@ -0,0 +1,311 @@
import random
import cv2
import numpy as np
import torch
import numbers
import collections
from PIL import Image
from ..registry import PROCESS
def to_tensor(data):
"""Convert objects of various python types to :obj:`torch.Tensor`.
Supported types are: :class:`numpy.ndarray`, :class:`torch.Tensor`,
:class:`Sequence`, :class:`int` and :class:`float`.
Args:
data (torch.Tensor | numpy.ndarray | Sequence | int | float): Data to
be converted.
"""
if isinstance(data, torch.Tensor):
return data
elif isinstance(data, np.ndarray):
return torch.from_numpy(data)
elif isinstance(data, int):
return torch.LongTensor([data])
elif isinstance(data, float):
return torch.FloatTensor([data])
else:
raise TypeError(f'type {type(data)} cannot be converted to tensor.')
@PROCESS.register_module
class ToTensor(object):
"""Convert some results to :obj:`torch.Tensor` by given keys.
Args:
keys (Sequence[str]): Keys that need to be converted to Tensor.
"""
def __init__(self, keys=['img', 'mask'], cfg=None):
self.keys = keys
def __call__(self, sample):
data = {}
if len(sample['img'].shape) < 3:
sample['img'] = np.expand_dims(img, -1)
for key in self.keys:
if key == 'img_metas' or key == 'gt_masks' or key == 'lane_line':
data[key] = sample[key]
continue
data[key] = to_tensor(sample[key])
data['img'] = data['img'].permute(2, 0, 1)
return data
def __repr__(self):
return self.__class__.__name__ + f'(keys={self.keys})'
@PROCESS.register_module
class RandomLROffsetLABEL(object):
def __init__(self, max_offset, cfg=None):
self.max_offset = max_offset
def __call__(self, sample):
img = sample['img']
label = sample['mask']
offset = np.random.randint(-self.max_offset, self.max_offset)
h, w = img.shape[:2]
img = np.array(img)
if offset > 0:
img[:, offset:, :] = img[:, 0:w - offset, :]
img[:, :offset, :] = 0
if offset < 0:
real_offset = -offset
img[:, 0:w - real_offset, :] = img[:, real_offset:, :]
img[:, w - real_offset:, :] = 0
label = np.array(label)
if offset > 0:
label[:, offset:] = label[:, 0:w - offset]
label[:, :offset] = 0
if offset < 0:
offset = -offset
label[:, 0:w - offset] = label[:, offset:]
label[:, w - offset:] = 0
sample['img'] = img
sample['mask'] = label
return sample
@PROCESS.register_module
class RandomUDoffsetLABEL(object):
def __init__(self, max_offset, cfg=None):
self.max_offset = max_offset
def __call__(self, sample):
img = sample['img']
label = sample['mask']
offset = np.random.randint(-self.max_offset, self.max_offset)
h, w = img.shape[:2]
img = np.array(img)
if offset > 0:
img[offset:, :, :] = img[0:h - offset, :, :]
img[:offset, :, :] = 0
if offset < 0:
real_offset = -offset
img[0:h - real_offset, :, :] = img[real_offset:, :, :]
img[h - real_offset:, :, :] = 0
label = np.array(label)
if offset > 0:
label[offset:, :] = label[0:h - offset, :]
label[:offset, :] = 0
if offset < 0:
offset = -offset
label[0:h - offset, :] = label[offset:, :]
label[h - offset:, :] = 0
sample['img'] = img
sample['mask'] = label
return sample
@PROCESS.register_module
class Resize(object):
def __init__(self, size, cfg=None):
assert (isinstance(size, collections.Iterable) and len(size) == 2)
self.size = size
def __call__(self, sample):
out = list()
sample['img'] = cv2.resize(sample['img'],
self.size,
interpolation=cv2.INTER_CUBIC)
if 'mask' in sample:
sample['mask'] = cv2.resize(sample['mask'],
self.size,
interpolation=cv2.INTER_NEAREST)
return sample
@PROCESS.register_module
class RandomCrop(object):
def __init__(self, size, cfg=None):
if isinstance(size, numbers.Number):
self.size = (int(size), int(size))
else:
self.size = size
def __call__(self, img_group):
h, w = img_group[0].shape[0:2]
th, tw = self.size
out_images = list()
h1 = random.randint(0, max(0, h - th))
w1 = random.randint(0, max(0, w - tw))
h2 = min(h1 + th, h)
w2 = min(w1 + tw, w)
for img in img_group:
assert (img.shape[0] == h and img.shape[1] == w)
out_images.append(img[h1:h2, w1:w2, ...])
return out_images
@PROCESS.register_module
class CenterCrop(object):
def __init__(self, size, cfg=None):
if isinstance(size, numbers.Number):
self.size = (int(size), int(size))
else:
self.size = size
def __call__(self, img_group):
h, w = img_group[0].shape[0:2]
th, tw = self.size
out_images = list()
h1 = max(0, int((h - th) / 2))
w1 = max(0, int((w - tw) / 2))
h2 = min(h1 + th, h)
w2 = min(w1 + tw, w)
for img in img_group:
assert (img.shape[0] == h and img.shape[1] == w)
out_images.append(img[h1:h2, w1:w2, ...])
return out_images
@PROCESS.register_module
class RandomRotation(object):
def __init__(self,
degree=(-10, 10),
interpolation=(cv2.INTER_LINEAR, cv2.INTER_NEAREST),
padding=None,
cfg=None):
self.degree = degree
self.interpolation = interpolation
self.padding = padding
if self.padding is None:
self.padding = [0, 0]
def _rotate_img(self, sample, map_matrix):
h, w = sample['img'].shape[0:2]
sample['img'] = cv2.warpAffine(sample['img'],
map_matrix, (w, h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=self.padding)
def _rotate_mask(self, sample, map_matrix):
if 'mask' not in sample:
return
h, w = sample['mask'].shape[0:2]
sample['mask'] = cv2.warpAffine(sample['mask'],
map_matrix, (w, h),
flags=cv2.INTER_NEAREST,
borderMode=cv2.BORDER_CONSTANT,
borderValue=self.padding)
def __call__(self, sample):
v = random.random()
if v < 0.5:
degree = random.uniform(self.degree[0], self.degree[1])
h, w = sample['img'].shape[0:2]
center = (w / 2, h / 2)
map_matrix = cv2.getRotationMatrix2D(center, degree, 1.0)
self._rotate_img(sample, map_matrix)
self._rotate_mask(sample, map_matrix)
return sample
@PROCESS.register_module
class RandomBlur(object):
def __init__(self, applied, cfg=None):
self.applied = applied
def __call__(self, img_group):
assert (len(self.applied) == len(img_group))
v = random.random()
if v < 0.5:
out_images = []
for img, a in zip(img_group, self.applied):
if a:
img = cv2.GaussianBlur(img, (5, 5),
random.uniform(1e-6, 0.6))
out_images.append(img)
if len(img.shape) > len(out_images[-1].shape):
out_images[-1] = out_images[-1][
..., np.newaxis] # single channel image
return out_images
else:
return img_group
@PROCESS.register_module
class RandomHorizontalFlip(object):
"""Randomly horizontally flips the given numpy Image with a probability of 0.5
"""
def __init__(self, cfg=None):
pass
def __call__(self, sample):
v = random.random()
if v < 0.5:
sample['img'] = np.fliplr(sample['img'])
if 'mask' in sample: sample['mask'] = np.fliplr(sample['mask'])
return sample
@PROCESS.register_module
class Normalize(object):
def __init__(self, img_norm, cfg=None):
self.mean = np.array(img_norm['mean'], dtype=np.float32)
self.std = np.array(img_norm['std'], dtype=np.float32)
def __call__(self, sample):
m = self.mean
s = self.std
img = sample['img']
if len(m) == 1:
img = img - np.array(m) # single channel image
img = img / np.array(s)
else:
img = img - np.array(m)[np.newaxis, np.newaxis, ...]
img = img / np.array(s)[np.newaxis, np.newaxis, ...]
sample['img'] = img
return sample
def CLRTransforms(img_h, img_w):
return [
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
]

View File

@@ -0,0 +1,54 @@
from clrnet.utils import Registry, build_from_cfg
import torch
from functools import partial
import numpy as np
import random
from mmcv.parallel import collate
DATASETS = Registry('datasets')
PROCESS = Registry('process')
def build(cfg, registry, default_args=None):
if isinstance(cfg, list):
modules = [
build_from_cfg(cfg_, registry, default_args) for cfg_ in cfg
]
return nn.Sequential(*modules)
else:
return build_from_cfg(cfg, registry, default_args)
def build_dataset(split_cfg, cfg):
return build(split_cfg, DATASETS, default_args=dict(cfg=cfg))
def worker_init_fn(worker_id, seed):
worker_seed = worker_id + seed
np.random.seed(worker_seed)
random.seed(worker_seed)
def build_dataloader(split_cfg, cfg, is_train=True):
if is_train:
shuffle = True
else:
shuffle = False
dataset = build_dataset(split_cfg, cfg)
init_fn = partial(worker_init_fn, seed=cfg.seed)
samples_per_gpu = cfg.batch_size // cfg.gpus
data_loader = torch.utils.data.DataLoader(
dataset,
batch_size=cfg.batch_size,
shuffle=shuffle,
num_workers=cfg.workers,
pin_memory=False,
drop_last=False,
collate_fn=partial(collate, samples_per_gpu=samples_per_gpu),
worker_init_fn=init_fn)
return data_loader

View File

@@ -0,0 +1,100 @@
import os.path as osp
import numpy as np
import cv2
import os
import json
import torchvision
from .base_dataset import BaseDataset
from clrnet.utils.tusimple_metric import LaneEval
from .registry import DATASETS
import logging
import random
SPLIT_FILES = {
'trainval':
['label_data_0313.json', 'label_data_0601.json', 'label_data_0531.json'],
'train': ['label_data_0313.json', 'label_data_0601.json'],
'val': ['label_data_0531.json'],
'test': ['test_label.json'],
}
@DATASETS.register_module
class TuSimple(BaseDataset):
def __init__(self, data_root, split, processes=None, cfg=None):
super().__init__(data_root, split, processes, cfg)
self.anno_files = SPLIT_FILES[split]
self.load_annotations()
self.h_samples = list(range(160, 720, 10))
def load_annotations(self):
self.logger.info('Loading TuSimple annotations...')
self.data_infos = []
max_lanes = 0
for anno_file in self.anno_files:
anno_file = osp.join(self.data_root, anno_file)
with open(anno_file, 'r') as anno_obj:
lines = anno_obj.readlines()
for line in lines:
data = json.loads(line)
y_samples = data['h_samples']
gt_lanes = data['lanes']
mask_path = data['raw_file'].replace('clips',
'seg_label')[:-3] + 'png'
lanes = [[(x, y) for (x, y) in zip(lane, y_samples) if x >= 0]
for lane in gt_lanes]
lanes = [lane for lane in lanes if len(lane) > 0]
max_lanes = max(max_lanes, len(lanes))
self.data_infos.append({
'img_path':
osp.join(self.data_root, data['raw_file']),
'img_name':
data['raw_file'],
'mask_path':
osp.join(self.data_root, mask_path),
'lanes':
lanes,
})
if self.training:
random.shuffle(self.data_infos)
self.max_lanes = max_lanes
def pred2lanes(self, pred):
ys = np.array(self.h_samples) / self.cfg.ori_img_h
lanes = []
for lane in pred:
xs = lane(ys)
invalid_mask = xs < 0
lane = (xs * self.cfg.ori_img_w).astype(int)
lane[invalid_mask] = -2
lanes.append(lane.tolist())
return lanes
def pred2tusimpleformat(self, idx, pred, runtime):
runtime *= 1000. # s to ms
img_name = self.data_infos[idx]['img_name']
lanes = self.pred2lanes(pred)
output = {'raw_file': img_name, 'lanes': lanes, 'run_time': runtime}
return json.dumps(output)
def save_tusimple_predictions(self, predictions, filename, runtimes=None):
if runtimes is None:
runtimes = np.ones(len(predictions)) * 1.e-3
lines = []
for idx, (prediction, runtime) in enumerate(zip(predictions,
runtimes)):
line = self.pred2tusimpleformat(idx, prediction, runtime)
lines.append(line)
with open(filename, 'w') as output_file:
output_file.write('\n'.join(lines))
def evaluate(self, predictions, output_basedir, runtimes=None):
pred_filename = os.path.join(output_basedir,
'tusimple_predictions.json')
self.save_tusimple_predictions(predictions, pred_filename, runtimes)
result, acc = LaneEval.bench_one_submit(pred_filename,
self.cfg.test_json_file)
self.logger.info(result)
return acc

View File

@@ -0,0 +1,13 @@
import torch
def build_optimizer(cfg, net):
params = []
cfg_cp = cfg.optimizer.copy()
cfg_type = cfg_cp.pop('type')
if cfg_type not in dir(torch.optim):
raise ValueError("{} is not defined.".format(cfg_type))
_optim = getattr(torch.optim, cfg_type)
return _optim(net.parameters(), **cfg_cp)

View File

@@ -0,0 +1,22 @@
from clrnet.utils import Registry, build_from_cfg
TRAINER = Registry('trainer')
EVALUATOR = Registry('evaluator')
def build(cfg, registry, default_args=None):
if isinstance(cfg, list):
modules = [
build_from_cfg(cfg_, registry, default_args) for cfg_ in cfg
]
return nn.Sequential(*modules)
else:
return build_from_cfg(cfg, registry, default_args)
def build_trainer(cfg):
return build(cfg.trainer, TRAINER, default_args=dict(cfg=cfg))
def build_evaluator(cfg):
return build(cfg.evaluator, EVALUATOR, default_args=dict(cfg=cfg))

View File

@@ -0,0 +1,148 @@
import time
import cv2
import torch
from tqdm import tqdm
import pytorch_warmup as warmup
import numpy as np
import random
import os
from clrnet.models.registry import build_net
from .registry import build_trainer, build_evaluator
from .optimizer import build_optimizer
from .scheduler import build_scheduler
from clrnet.datasets import build_dataloader
from clrnet.utils.recorder import build_recorder
from clrnet.utils.net_utils import save_model, load_network, resume_network
from mmcv.parallel import MMDataParallel
class Runner(object):
def __init__(self, cfg):
torch.manual_seed(cfg.seed)
np.random.seed(cfg.seed)
random.seed(cfg.seed)
self.cfg = cfg
self.recorder = build_recorder(self.cfg)
self.net = build_net(self.cfg)
self.net = MMDataParallel(self.net,
device_ids=range(self.cfg.gpus)).cuda()
self.recorder.logger.info('Network: \n' + str(self.net))
self.resume()
self.optimizer = build_optimizer(self.cfg, self.net)
self.scheduler = build_scheduler(self.cfg, self.optimizer)
self.metric = 0.
self.val_loader = None
self.test_loader = None
def to_cuda(self, batch):
for k in batch:
if not isinstance(batch[k], torch.Tensor):
continue
batch[k] = batch[k].cuda()
return batch
def resume(self):
if not self.cfg.load_from and not self.cfg.finetune_from:
return
load_network(self.net, self.cfg.load_from, finetune_from=self.cfg.finetune_from, logger=self.recorder.logger)
def train_epoch(self, epoch, train_loader):
self.net.train()
end = time.time()
max_iter = len(train_loader)
for i, data in enumerate(train_loader):
if self.recorder.step >= self.cfg.total_iter:
break
date_time = time.time() - end
self.recorder.step += 1
data = self.to_cuda(data)
output = self.net(data)
self.optimizer.zero_grad()
loss = output['loss'].sum()
loss.backward()
self.optimizer.step()
if not self.cfg.lr_update_by_epoch:
self.scheduler.step()
batch_time = time.time() - end
end = time.time()
self.recorder.update_loss_stats(output['loss_stats'])
self.recorder.batch_time.update(batch_time)
self.recorder.data_time.update(date_time)
if i % self.cfg.log_interval == 0 or i == max_iter - 1:
lr = self.optimizer.param_groups[0]['lr']
self.recorder.lr = lr
self.recorder.record('train')
def train(self):
self.recorder.logger.info('Build train loader...')
train_loader = build_dataloader(self.cfg.dataset.train,
self.cfg,
is_train=True)
self.recorder.logger.info('Start training...')
start_epoch = 0
if self.cfg.resume_from:
start_epoch = resume_network(self.cfg.resume_from, self.net,
self.optimizer, self.scheduler,
self.recorder)
for epoch in range(start_epoch, self.cfg.epochs):
self.recorder.epoch = epoch
self.train_epoch(epoch, train_loader)
if (epoch +
1) % self.cfg.save_ep == 0 or epoch == self.cfg.epochs - 1:
self.save_ckpt()
if (epoch +
1) % self.cfg.eval_ep == 0 or epoch == self.cfg.epochs - 1:
self.validate()
if self.recorder.step >= self.cfg.total_iter:
break
if self.cfg.lr_update_by_epoch:
self.scheduler.step()
def test(self):
if not self.test_loader:
self.test_loader = build_dataloader(self.cfg.dataset.test,
self.cfg,
is_train=False)
self.net.eval()
predictions = []
for i, data in enumerate(tqdm(self.test_loader, desc=f'Testing')):
data = self.to_cuda(data)
with torch.no_grad():
output = self.net(data)
output = self.net.module.heads.get_lanes(output)
predictions.extend(output)
if self.cfg.view:
self.test_loader.dataset.view(output, data['meta'])
metric = self.test_loader.dataset.evaluate(predictions,
self.cfg.work_dir)
if metric is not None:
self.recorder.logger.info('metric: ' + str(metric))
def validate(self):
if not self.val_loader:
self.val_loader = build_dataloader(self.cfg.dataset.val,
self.cfg,
is_train=False)
self.net.eval()
predictions = []
for i, data in enumerate(tqdm(self.val_loader, desc=f'Validate')):
data = self.to_cuda(data)
with torch.no_grad():
output = self.net(data)
output = self.net.module.heads.get_lanes(output)
predictions.extend(output)
if self.cfg.view:
self.val_loader.dataset.view(output, data['meta'])
metric = self.val_loader.dataset.evaluate(predictions,
self.cfg.work_dir)
self.recorder.logger.info('metric: ' + str(metric))
def save_ckpt(self, is_best=False):
save_model(self.net, self.optimizer, self.scheduler, self.recorder,
is_best)

View File

@@ -0,0 +1,15 @@
import torch
import math
def build_scheduler(cfg, optimizer):
cfg_cp = cfg.scheduler.copy()
cfg_type = cfg_cp.pop('type')
if cfg_type not in dir(torch.optim.lr_scheduler):
raise ValueError("{} is not defined.".format(cfg_type))
_scheduler = getattr(torch.optim.lr_scheduler, cfg_type)
return _scheduler(optimizer, **cfg_cp)

View File

@@ -0,0 +1,5 @@
from .backbones import *
from .heads import *
from .nets import *
from .necks import *
from .registry import build_backbones

View File

@@ -0,0 +1,2 @@
from .resnet import ResNet
from .dla34 import DLA

View File

@@ -0,0 +1,460 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
import math
import logging
import numpy as np
from os.path import join
import torch
from torch import nn
import torch.nn.functional as F
import torch.utils.model_zoo as model_zoo
from clrnet.models.registry import BACKBONES
BN_MOMENTUM = 0.1
logger = logging.getLogger(__name__)
def get_model_url(data='imagenet', name='dla34', hash='ba72cf86'):
return join('http://dl.yf.io/dla/models', data,
'{}-{}.pth'.format(name, hash))
def conv3x3(in_planes, out_planes, stride=1):
"3x3 convolution with padding"
return nn.Conv2d(in_planes,
out_planes,
kernel_size=3,
stride=stride,
padding=1,
bias=False)
class BasicBlock(nn.Module):
def __init__(self, inplanes, planes, stride=1, dilation=1):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(inplanes,
planes,
kernel_size=3,
stride=stride,
padding=dilation,
bias=False,
dilation=dilation)
self.bn1 = nn.BatchNorm2d(planes, momentum=BN_MOMENTUM)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(planes,
planes,
kernel_size=3,
stride=1,
padding=dilation,
bias=False,
dilation=dilation)
self.bn2 = nn.BatchNorm2d(planes, momentum=BN_MOMENTUM)
self.stride = stride
def forward(self, x, residual=None):
if residual is None:
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += residual
out = self.relu(out)
return out
class Bottleneck(nn.Module):
expansion = 2
def __init__(self, inplanes, planes, stride=1, dilation=1):
super(Bottleneck, self).__init__()
expansion = Bottleneck.expansion
bottle_planes = planes // expansion
self.conv1 = nn.Conv2d(inplanes,
bottle_planes,
kernel_size=1,
bias=False)
self.bn1 = nn.BatchNorm2d(bottle_planes, momentum=BN_MOMENTUM)
self.conv2 = nn.Conv2d(bottle_planes,
bottle_planes,
kernel_size=3,
stride=stride,
padding=dilation,
bias=False,
dilation=dilation)
self.bn2 = nn.BatchNorm2d(bottle_planes, momentum=BN_MOMENTUM)
self.conv3 = nn.Conv2d(bottle_planes,
planes,
kernel_size=1,
bias=False)
self.bn3 = nn.BatchNorm2d(planes, momentum=BN_MOMENTUM)
self.relu = nn.ReLU(inplace=True)
self.stride = stride
def forward(self, x, residual=None):
if residual is None:
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
out += residual
out = self.relu(out)
return out
class BottleneckX(nn.Module):
expansion = 2
cardinality = 32
def __init__(self, inplanes, planes, stride=1, dilation=1):
super(BottleneckX, self).__init__()
cardinality = BottleneckX.cardinality
# dim = int(math.floor(planes * (BottleneckV5.expansion / 64.0)))
# bottle_planes = dim * cardinality
bottle_planes = planes * cardinality // 32
self.conv1 = nn.Conv2d(inplanes,
bottle_planes,
kernel_size=1,
bias=False)
self.bn1 = nn.BatchNorm2d(bottle_planes, momentum=BN_MOMENTUM)
self.conv2 = nn.Conv2d(bottle_planes,
bottle_planes,
kernel_size=3,
stride=stride,
padding=dilation,
bias=False,
dilation=dilation,
groups=cardinality)
self.bn2 = nn.BatchNorm2d(bottle_planes, momentum=BN_MOMENTUM)
self.conv3 = nn.Conv2d(bottle_planes,
planes,
kernel_size=1,
bias=False)
self.bn3 = nn.BatchNorm2d(planes, momentum=BN_MOMENTUM)
self.relu = nn.ReLU(inplace=True)
self.stride = stride
def forward(self, x, residual=None):
if residual is None:
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
out += residual
out = self.relu(out)
return out
class Root(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, residual):
super(Root, self).__init__()
self.conv = nn.Conv2d(in_channels,
out_channels,
1,
stride=1,
bias=False,
padding=(kernel_size - 1) // 2)
self.bn = nn.BatchNorm2d(out_channels, momentum=BN_MOMENTUM)
self.relu = nn.ReLU(inplace=True)
self.residual = residual
def forward(self, *x):
children = x
x = self.conv(torch.cat(x, 1))
x = self.bn(x)
if self.residual:
x += children[0]
x = self.relu(x)
return x
class Tree(nn.Module):
def __init__(self,
levels,
block,
in_channels,
out_channels,
stride=1,
level_root=False,
root_dim=0,
root_kernel_size=1,
dilation=1,
root_residual=False):
super(Tree, self).__init__()
if root_dim == 0:
root_dim = 2 * out_channels
if level_root:
root_dim += in_channels
if levels == 1:
self.tree1 = block(in_channels,
out_channels,
stride,
dilation=dilation)
self.tree2 = block(out_channels,
out_channels,
1,
dilation=dilation)
else:
self.tree1 = Tree(levels - 1,
block,
in_channels,
out_channels,
stride,
root_dim=0,
root_kernel_size=root_kernel_size,
dilation=dilation,
root_residual=root_residual)
self.tree2 = Tree(levels - 1,
block,
out_channels,
out_channels,
root_dim=root_dim + out_channels,
root_kernel_size=root_kernel_size,
dilation=dilation,
root_residual=root_residual)
if levels == 1:
self.root = Root(root_dim, out_channels, root_kernel_size,
root_residual)
self.level_root = level_root
self.root_dim = root_dim
self.downsample = None
self.project = None
self.levels = levels
if stride > 1:
self.downsample = nn.MaxPool2d(stride, stride=stride)
# Match CLRerNet/official DLA: project only on leaf when channels differ.
if levels == 1 and in_channels != out_channels:
self.project = nn.Sequential(
nn.Conv2d(in_channels,
out_channels,
kernel_size=1,
stride=1,
bias=False),
nn.BatchNorm2d(out_channels, momentum=BN_MOMENTUM))
def forward(self, x, residual=None, children=None):
children = [] if children is None else children
bottom = self.downsample(x) if self.downsample else x
residual = self.project(bottom) if self.project else bottom
if self.level_root:
children.append(bottom)
x1 = self.tree1(x, residual)
if self.levels == 1:
x2 = self.tree2(x1)
x = self.root(x2, x1, *children)
else:
children.append(x1)
x = self.tree2(x1, children=children)
return x
class DLA(nn.Module):
def __init__(self,
levels,
channels,
num_classes=1000,
block=BasicBlock,
residual_root=False,
linear_root=False):
super(DLA, self).__init__()
self.channels = channels
self.num_classes = num_classes
self.base_layer = nn.Sequential(
nn.Conv2d(3,
channels[0],
kernel_size=7,
stride=1,
padding=3,
bias=False),
nn.BatchNorm2d(channels[0], momentum=BN_MOMENTUM),
nn.ReLU(inplace=True))
self.level0 = self._make_conv_level(channels[0], channels[0],
levels[0])
self.level1 = self._make_conv_level(channels[0],
channels[1],
levels[1],
stride=2)
self.level2 = Tree(levels[2],
block,
channels[1],
channels[2],
2,
level_root=False,
root_residual=residual_root)
self.level3 = Tree(levels[3],
block,
channels[2],
channels[3],
2,
level_root=True,
root_residual=residual_root)
self.level4 = Tree(levels[4],
block,
channels[3],
channels[4],
2,
level_root=True,
root_residual=residual_root)
self.level5 = Tree(levels[5],
block,
channels[4],
channels[5],
2,
level_root=True,
root_residual=residual_root)
# for m in self.modules():
# if isinstance(m, nn.Conv2d):
# n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
# m.weight.data.normal_(0, math.sqrt(2. / n))
# elif isinstance(m, nn.BatchNorm2d):
# m.weight.data.fill_(1)
# m.bias.data.zero_()
def _make_level(self, block, inplanes, planes, blocks, stride=1):
downsample = None
if stride != 1 or inplanes != planes:
downsample = nn.Sequential(
nn.MaxPool2d(stride, stride=stride),
nn.Conv2d(inplanes,
planes,
kernel_size=1,
stride=1,
bias=False),
nn.BatchNorm2d(planes, momentum=BN_MOMENTUM),
)
layers = []
layers.append(block(inplanes, planes, stride, downsample=downsample))
for i in range(1, blocks):
layers.append(block(inplanes, planes))
return nn.Sequential(*layers)
def _make_conv_level(self, inplanes, planes, convs, stride=1, dilation=1):
modules = []
for i in range(convs):
modules.extend([
nn.Conv2d(inplanes,
planes,
kernel_size=3,
stride=stride if i == 0 else 1,
padding=dilation,
bias=False,
dilation=dilation),
nn.BatchNorm2d(planes, momentum=BN_MOMENTUM),
nn.ReLU(inplace=True)
])
inplanes = planes
return nn.Sequential(*modules)
def forward(self, x):
y = []
x = self.base_layer(x)
for i in range(6):
x = getattr(self, 'level{}'.format(i))(x)
y.append(x)
return y[2:]
def load_pretrained_model(self,
data='imagenet',
name='dla34',
hash='ba72cf86'):
# fc = self.fc
if name.endswith('.pth'):
model_weights = torch.load(data + name)
else:
model_url = get_model_url(data, name, hash)
model_weights = model_zoo.load_url(model_url)
self.load_state_dict(model_weights, strict=False)
# self.fc = fc
def dla34(pretrained=True, levels=None, in_channels=None, **kwargs): # DLA-34
model = DLA(levels=levels,
channels=in_channels,
block=BasicBlock,
**kwargs)
if pretrained:
model.load_pretrained_model(data='imagenet',
name='dla34',
hash='ba72cf86')
return model
@BACKBONES.register_module
class DLAWrapper(nn.Module):
def __init__(self,
dla='dla34',
pretrained=True,
levels=[1, 1, 1, 2, 2, 1],
in_channels=[16, 32, 64, 128, 256, 512],
cfg=None):
super(DLAWrapper, self).__init__()
self.cfg = cfg
self.in_channels = in_channels
self.model = eval(dla)(pretrained=pretrained,
levels=levels,
in_channels=in_channels)
def forward(self, x):
x = self.model(x)
return x
class Identity(nn.Module):
def __init__(self):
super(Identity, self).__init__()
def forward(self, x):
return x
def fill_fc_weights(layers):
for m in layers.modules():
if isinstance(m, nn.Conv2d):
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def fill_up_weights(up):
w = up.weight.data
f = math.ceil(w.size(2) / 2)
c = (2 * f - 1 - f % 2) / (2. * f)
for i in range(w.size(2)):
for j in range(w.size(3)):
w[0, 0, i, j] = \
(1 - math.fabs(i / f - c)) * (1 - math.fabs(j / f - c))
for c in range(1, w.size(0)):
w[c, 0, :, :] = w[0, 0, :, :]

View File

@@ -0,0 +1,431 @@
import torch
from torch import nn
import torch.nn.functional as F
from torch.hub import load_state_dict_from_url
from clrnet.models.registry import BACKBONES
model_urls = {
'resnet18':
'https://download.pytorch.org/models/resnet18-5c106cde.pth',
'resnet34':
'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
'resnet50':
'https://download.pytorch.org/models/resnet50-19c8e357.pth',
'resnet101':
'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
'resnet152':
'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
'resnext50_32x4d':
'https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth',
'resnext101_32x8d':
'https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth',
'wide_resnet50_2':
'https://download.pytorch.org/models/wide_resnet50_2-95faca4d.pth',
'wide_resnet101_2':
'https://download.pytorch.org/models/wide_resnet101_2-32ee1156.pth',
}
def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes,
out_planes,
kernel_size=3,
stride=stride,
padding=dilation,
groups=groups,
bias=False,
dilation=dilation)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes,
out_planes,
kernel_size=1,
stride=stride,
bias=False)
class BasicBlock(nn.Module):
expansion = 1
def __init__(self,
inplanes,
planes,
stride=1,
downsample=None,
groups=1,
base_width=64,
dilation=1,
norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64:
raise ValueError(
'BasicBlock only supports groups=1 and base_width=64')
# if dilation > 1:
# raise NotImplementedError(
# "Dilation > 1 not supported in BasicBlock")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv3x3(inplanes, planes, stride, dilation=dilation)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes, dilation=dilation)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class Bottleneck(nn.Module):
expansion = 4
def __init__(self,
inplanes,
planes,
stride=1,
downsample=None,
groups=1,
base_width=64,
dilation=1,
norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
@BACKBONES.register_module
class ResNetWrapper(nn.Module):
def __init__(self,
resnet='resnet18',
pretrained=True,
replace_stride_with_dilation=[False, False, False],
out_conv=False,
fea_stride=8,
out_channel=128,
in_channels=[64, 128, 256, 512],
cfg=None):
super(ResNetWrapper, self).__init__()
self.cfg = cfg
self.in_channels = in_channels
self.model = eval(resnet)(
pretrained=pretrained,
replace_stride_with_dilation=replace_stride_with_dilation,
in_channels=self.in_channels)
self.out = None
if out_conv:
out_channel = 512
for chan in reversed(self.in_channels):
if chan < 0: continue
out_channel = chan
break
self.out = conv1x1(out_channel * self.model.expansion,
cfg.featuremap_out_channel)
def forward(self, x):
x = self.model(x)
if self.out:
x[-1] = self.out(x[-1])
return x
class ResNet(nn.Module):
def __init__(self,
block,
layers,
zero_init_residual=False,
groups=1,
width_per_group=64,
replace_stride_with_dilation=None,
norm_layer=None,
in_channels=None):
super(ResNet, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer
self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
# each element in the tuple indicates if we should replace
# the 2x2 stride with a dilated convolution instead
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError("replace_stride_with_dilation should be None "
"or a 3-element tuple, got {}".format(
replace_stride_with_dilation))
self.groups = groups
self.base_width = width_per_group
self.conv1 = nn.Conv2d(3,
self.inplanes,
kernel_size=7,
stride=2,
padding=3,
bias=False)
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.in_channels = in_channels
self.layer1 = self._make_layer(block, in_channels[0], layers[0])
self.layer2 = self._make_layer(block,
in_channels[1],
layers[1],
stride=2,
dilate=replace_stride_with_dilation[0])
self.layer3 = self._make_layer(block,
in_channels[2],
layers[2],
stride=2,
dilate=replace_stride_with_dilation[1])
if in_channels[3] > 0:
self.layer4 = self._make_layer(
block,
in_channels[3],
layers[3],
stride=2,
dilate=replace_stride_with_dilation[2])
self.expansion = block.expansion
# self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
# self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight,
mode='fan_out',
nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
layers = []
layers.append(
block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(
block(self.inplanes,
planes,
groups=self.groups,
base_width=self.base_width,
dilation=self.dilation,
norm_layer=norm_layer))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
out_layers = []
for name in ['layer1', 'layer2', 'layer3', 'layer4']:
if not hasattr(self, name):
continue
layer = getattr(self, name)
x = layer(x)
out_layers.append(x)
return out_layers
def _resnet(arch, block, layers, pretrained, progress, **kwargs):
model = ResNet(block, layers, **kwargs)
if pretrained:
print('pretrained model: ', model_urls[arch])
# state_dict = torch.load(model_urls[arch])['net']
state_dict = load_state_dict_from_url(model_urls[arch])
model.load_state_dict(state_dict, strict=False)
return model
def resnet18(pretrained=False, progress=True, **kwargs):
r"""ResNet-18 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress,
**kwargs)
def resnet34(pretrained=False, progress=True, **kwargs):
r"""ResNet-34 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress,
**kwargs)
def resnet50(pretrained=False, progress=True, **kwargs):
r"""ResNet-50 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress,
**kwargs)
def resnet101(pretrained=False, progress=True, **kwargs):
r"""ResNet-101 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet101', Bottleneck, [3, 4, 23, 3], pretrained,
progress, **kwargs)
def resnet152(pretrained=False, progress=True, **kwargs):
r"""ResNet-152 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet152', Bottleneck, [3, 8, 36, 3], pretrained,
progress, **kwargs)
def resnext50_32x4d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-50 32x4d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 4
return _resnet('resnext50_32x4d', Bottleneck, [3, 4, 6, 3], pretrained,
progress, **kwargs)
def resnext101_32x8d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-101 32x8d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 8
return _resnet('resnext101_32x8d', Bottleneck, [3, 4, 23, 3], pretrained,
progress, **kwargs)
def wide_resnet50_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-50-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_
The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet50_2', Bottleneck, [3, 4, 6, 3], pretrained,
progress, **kwargs)
def wide_resnet101_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-101-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_
The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet101_2', Bottleneck, [3, 4, 23, 3], pretrained,
progress, **kwargs)

View File

@@ -0,0 +1 @@
from .clr_head import CLRHead

View File

@@ -0,0 +1,492 @@
import math
import cv2
import torch
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
from mmcv.cnn import ConvModule
from clrnet.utils.lane import Lane
from clrnet.models.losses.focal_loss import FocalLoss
from clrnet.models.losses.accuracy import accuracy
from clrnet.ops import nms
from clrnet.models.utils.roi_gather import ROIGather, LinearModule
from clrnet.models.utils.seg_decoder import SegDecoder
from clrnet.models.utils.dynamic_assign import assign
from clrnet.models.losses.lineiou_loss import liou_loss
from clrnet.utils.bilinear_grid_sample import bilinear_grid_sample
from ..registry import HEADS
# Set True for ONNX/RKNN export (Gather+Pad instead of GridSample).
USE_BILINEAR_GRID_SAMPLE = True
@HEADS.register_module
class CLRHead(nn.Module):
def __init__(self,
num_points=72,
prior_feat_channels=64,
fc_hidden_dim=64,
num_priors=192,
num_fc=2,
refine_layers=3,
sample_points=36,
cfg=None):
super(CLRHead, self).__init__()
self.cfg = cfg
self.img_w = self.cfg.img_w
self.img_h = self.cfg.img_h
self.n_strips = num_points - 1
self.n_offsets = num_points
self.num_priors = num_priors
self.sample_points = sample_points
self.refine_layers = refine_layers
self.fc_hidden_dim = fc_hidden_dim
self.register_buffer(name='sample_x_indexs', tensor=(torch.linspace(
0, 1, steps=self.sample_points, dtype=torch.float32) *
self.n_strips).long())
self.register_buffer(name='prior_feat_ys', tensor=torch.flip(
(1 - self.sample_x_indexs.float() / self.n_strips), dims=[-1]))
self.register_buffer(name='prior_ys', tensor=torch.linspace(1,
0,
steps=self.n_offsets,
dtype=torch.float32))
self.prior_feat_channels = prior_feat_channels
self._init_prior_embeddings()
init_priors, priors_on_featmap = self.generate_priors_from_embeddings() #None, None
self.register_buffer(name='priors', tensor=init_priors)
self.register_buffer(name='priors_on_featmap', tensor=priors_on_featmap)
# generate xys for feature map
self.seg_decoder = SegDecoder(self.img_h, self.img_w,
self.cfg.num_classes,
self.prior_feat_channels,
self.refine_layers)
reg_modules = list()
cls_modules = list()
for _ in range(num_fc):
reg_modules += [*LinearModule(self.fc_hidden_dim)]
cls_modules += [*LinearModule(self.fc_hidden_dim)]
self.reg_modules = nn.ModuleList(reg_modules)
self.cls_modules = nn.ModuleList(cls_modules)
self.roi_gather = ROIGather(self.prior_feat_channels, self.num_priors,
self.sample_points, self.fc_hidden_dim,
self.refine_layers)
self.reg_layers = nn.Linear(
self.fc_hidden_dim, self.n_offsets + 1 + 2 +
1) # n offsets + 1 length + start_x + start_y + theta
self.cls_layers = nn.Linear(self.fc_hidden_dim, 2)
weights = torch.ones(self.cfg.num_classes)
weights[0] = self.cfg.bg_weight
self.criterion = torch.nn.NLLLoss(ignore_index=self.cfg.ignore_label,
weight=weights)
# init the weights here
self.init_weights()
# function to init layer weights
def init_weights(self):
# initialize heads
for m in self.cls_layers.parameters():
nn.init.normal_(m, mean=0., std=1e-3)
for m in self.reg_layers.parameters():
nn.init.normal_(m, mean=0., std=1e-3)
def pool_prior_features(self, batch_features, num_priors, prior_xs):
'''
pool prior feature from feature map.
Args:
batch_features (Tensor): Input feature maps, shape: (B, C, H, W)
'''
batch_size = batch_features.shape[0]
prior_xs = prior_xs.view(batch_size, num_priors, -1, 1)
prior_ys = self.prior_feat_ys.repeat(batch_size * num_priors).view(
batch_size, num_priors, -1, 1)
prior_xs = prior_xs * 2. - 1.
prior_ys = prior_ys * 2. - 1.
grid = torch.cat((prior_xs, prior_ys), dim=-1)
if USE_BILINEAR_GRID_SAMPLE:
feature = bilinear_grid_sample(batch_features, grid,
align_corners=True).permute(0, 2, 1, 3)
else:
feature = F.grid_sample(batch_features, grid,
align_corners=True).permute(0, 2, 1, 3)
feature = feature.reshape(batch_size * num_priors,
self.prior_feat_channels, self.sample_points,
1)
return feature
def generate_priors_from_embeddings(self):
predictions = self.prior_embeddings.weight # (num_prop, 3)
# 2 scores, 1 start_y, 1 start_x, 1 theta, 1 length, 72 coordinates, score[0] = negative prob, score[1] = positive prob
priors = predictions.new_zeros(
(self.num_priors, 2 + 2 + 2 + self.n_offsets), device=predictions.device)
priors[:, 2:5] = predictions.clone()
priors[:, 6:] = (
priors[:, 3].unsqueeze(1).clone().repeat(1, self.n_offsets) *
(self.img_w - 1) +
((1 - self.prior_ys.repeat(self.num_priors, 1) -
priors[:, 2].unsqueeze(1).clone().repeat(1, self.n_offsets)) *
self.img_h / torch.tan(priors[:, 4].unsqueeze(1).clone().repeat(
1, self.n_offsets) * math.pi + 1e-5))) / (self.img_w - 1)
# init priors on feature map
priors_on_featmap = priors.clone()[..., 6 + self.sample_x_indexs]
return priors, priors_on_featmap
def _init_prior_embeddings(self):
# [start_y, start_x, theta] -> all normalize
self.prior_embeddings = nn.Embedding(self.num_priors, 3)
bottom_priors_nums = self.num_priors * 3 // 4
left_priors_nums, _ = self.num_priors // 8, self.num_priors // 8
strip_size = 0.5 / (left_priors_nums // 2 - 1)
bottom_strip_size = 1 / (bottom_priors_nums // 4 + 1)
for i in range(left_priors_nums):
nn.init.constant_(self.prior_embeddings.weight[i, 0],
(i // 2) * strip_size)
nn.init.constant_(self.prior_embeddings.weight[i, 1], 0.)
nn.init.constant_(self.prior_embeddings.weight[i, 2],
0.16 if i % 2 == 0 else 0.32)
for i in range(left_priors_nums,
left_priors_nums + bottom_priors_nums):
nn.init.constant_(self.prior_embeddings.weight[i, 0], 0.)
nn.init.constant_(self.prior_embeddings.weight[i, 1],
((i - left_priors_nums) // 4 + 1) *
bottom_strip_size)
nn.init.constant_(self.prior_embeddings.weight[i, 2],
0.2 * (i % 4 + 1))
for i in range(left_priors_nums + bottom_priors_nums, self.num_priors):
nn.init.constant_(
self.prior_embeddings.weight[i, 0],
((i - left_priors_nums - bottom_priors_nums) // 2) *
strip_size)
nn.init.constant_(self.prior_embeddings.weight[i, 1], 1.)
nn.init.constant_(self.prior_embeddings.weight[i, 2],
0.68 if i % 2 == 0 else 0.84)
# forward function here
def forward(self, x, **kwargs):
'''
Take pyramid features as input to perform Cross Layer Refinement and finally output the prediction lanes.
Each feature is a 4D tensor.
Args:
x: input features (list[Tensor])
Return:
prediction_list: each layer's prediction result
seg: segmentation result for auxiliary loss
'''
batch_features = list(x[len(x) - self.refine_layers:])
batch_features.reverse()
batch_size = batch_features[-1].shape[0]
if self.training:
self.priors, self.priors_on_featmap = self.generate_priors_from_embeddings()
priors, priors_on_featmap = self.priors.repeat(batch_size, 1,
1), self.priors_on_featmap.repeat(
batch_size, 1, 1)
predictions_lists = []
# iterative refine
prior_features_stages = []
for stage in range(self.refine_layers):
num_priors = priors_on_featmap.shape[1]
prior_xs = torch.flip(priors_on_featmap, dims=[2])
batch_prior_features = self.pool_prior_features(
batch_features[stage], num_priors, prior_xs)
prior_features_stages.append(batch_prior_features)
fc_features = self.roi_gather(prior_features_stages,
batch_features[stage], stage)
fc_features = fc_features.view(num_priors, batch_size,
-1).reshape(batch_size * num_priors,
self.fc_hidden_dim)
cls_features = fc_features.clone()
reg_features = fc_features.clone()
for cls_layer in self.cls_modules:
cls_features = cls_layer(cls_features)
for reg_layer in self.reg_modules:
reg_features = reg_layer(reg_features)
cls_logits = self.cls_layers(cls_features)
reg = self.reg_layers(reg_features)
cls_logits = cls_logits.reshape(
batch_size, -1, cls_logits.shape[1]) # (B, num_priors, 2)
reg = reg.reshape(batch_size, -1, reg.shape[1])
predictions = priors.clone()
predictions[:, :, :2] = cls_logits
predictions[:, :,
2:5] += reg[:, :, :3] # also reg theta angle here
predictions[:, :, 5] = reg[:, :, 3] # length
def tran_tensor(t):
return t.unsqueeze(2).clone().repeat(1, 1, self.n_offsets)
predictions[..., 6:] = (
tran_tensor(predictions[..., 3]) * (self.img_w - 1) +
((1 - self.prior_ys.repeat(batch_size, num_priors, 1) -
tran_tensor(predictions[..., 2])) * self.img_h /
torch.tan(tran_tensor(predictions[..., 4]) * math.pi + 1e-5))) / (self.img_w - 1)
prediction_lines = predictions.clone()
predictions[..., 6:] += reg[..., 4:]
predictions_lists.append(predictions)
if stage != self.refine_layers - 1:
priors = prediction_lines.detach().clone()
priors_on_featmap = priors[..., 6 + self.sample_x_indexs]
if self.training:
seg = None
seg_features = torch.cat([
F.interpolate(feature,
size=[
batch_features[-1].shape[2],
batch_features[-1].shape[3]
],
mode='bilinear',
align_corners=False)
for feature in batch_features
],
dim=1)
seg = self.seg_decoder(seg_features)
output = {'predictions_lists': predictions_lists, 'seg': seg}
return self.loss(output, kwargs['batch'])
return predictions_lists[-1]
def predictions_to_pred(self, predictions):
'''
Convert predictions to internal Lane structure for evaluation.
'''
self.prior_ys = self.prior_ys.to(predictions.device)
self.prior_ys = self.prior_ys.double()
lanes = []
for lane in predictions:
lane_xs = lane[6:] # normalized value
start = min(max(0, int(round(lane[2].item() * self.n_strips))),
self.n_strips)
length = int(round(lane[5].item()))
end = start + length - 1
end = min(end, len(self.prior_ys) - 1)
# end = label_end
# if the prediction does not start at the bottom of the image,
# extend its prediction until the x is outside the image
mask = ~((((lane_xs[:start] >= 0.) & (lane_xs[:start] <= 1.)
).cpu().numpy()[::-1].cumprod()[::-1]).astype(np.bool))
lane_xs[end + 1:] = -2
lane_xs[:start][mask] = -2
lane_ys = self.prior_ys[lane_xs >= 0]
lane_xs = lane_xs[lane_xs >= 0]
lane_xs = lane_xs.flip(0).double()
lane_ys = lane_ys.flip(0)
lane_ys = (lane_ys * (self.cfg.ori_img_h - self.cfg.cut_height) +
self.cfg.cut_height) / self.cfg.ori_img_h
if len(lane_xs) <= 1:
continue
points = torch.stack(
(lane_xs.reshape(-1, 1), lane_ys.reshape(-1, 1)),
dim=1).squeeze(2)
lane = Lane(points=points.cpu().numpy(),
metadata={
'start_x': lane[3],
'start_y': lane[2],
'conf': lane[1]
})
lanes.append(lane)
return lanes
def loss(self,
output,
batch,
cls_loss_weight=2.,
xyt_loss_weight=0.5,
iou_loss_weight=2.,
seg_loss_weight=1.):
if self.cfg.haskey('cls_loss_weight'):
cls_loss_weight = self.cfg.cls_loss_weight
if self.cfg.haskey('xyt_loss_weight'):
xyt_loss_weight = self.cfg.xyt_loss_weight
if self.cfg.haskey('iou_loss_weight'):
iou_loss_weight = self.cfg.iou_loss_weight
if self.cfg.haskey('seg_loss_weight'):
seg_loss_weight = self.cfg.seg_loss_weight
predictions_lists = output['predictions_lists']
targets = batch['lane_line'].clone()
cls_criterion = FocalLoss(alpha=0.25, gamma=2.)
cls_loss = 0
reg_xytl_loss = 0
iou_loss = 0
cls_acc = []
cls_acc_stage = []
for stage in range(self.refine_layers):
predictions_list = predictions_lists[stage]
for predictions, target in zip(predictions_list, targets):
target = target[target[:, 1] == 1]
if len(target) == 0:
# If there are no targets, all predictions have to be negatives (i.e., 0 confidence)
cls_target = predictions.new_zeros(predictions.shape[0]).long()
cls_pred = predictions[:, :2]
cls_loss = cls_loss + cls_criterion(
cls_pred, cls_target).sum()
continue
with torch.no_grad():
matched_row_inds, matched_col_inds = assign(
predictions, target, self.img_w, self.img_h)
# classification targets
cls_target = predictions.new_zeros(predictions.shape[0]).long()
cls_target[matched_row_inds] = 1
cls_pred = predictions[:, :2]
# regression targets -> [start_y, start_x, theta] (all transformed to absolute values), only on matched pairs
reg_yxtl = predictions[matched_row_inds, 2:6]
reg_yxtl[:, 0] *= self.n_strips
reg_yxtl[:, 1] *= (self.img_w - 1)
reg_yxtl[:, 2] *= 180
reg_yxtl[:, 3] *= self.n_strips
target_yxtl = target[matched_col_inds, 2:6].clone()
# regression targets -> S coordinates (all transformed to absolute values)
reg_pred = predictions[matched_row_inds, 6:]
reg_pred *= (self.img_w - 1)
reg_targets = target[matched_col_inds, 6:].clone()
with torch.no_grad():
predictions_starts = torch.clamp(
(predictions[matched_row_inds, 2] *
self.n_strips).round().long(), 0,
self.n_strips) # ensure the predictions starts is valid
target_starts = (target[matched_col_inds, 2] *
self.n_strips).round().long()
target_yxtl[:, -1] -= (predictions_starts - target_starts
) # reg length
# Loss calculation
cls_loss = cls_loss + cls_criterion(cls_pred, cls_target).sum(
) / target.shape[0]
target_yxtl[:, 0] *= self.n_strips
target_yxtl[:, 2] *= 180
reg_xytl_loss = reg_xytl_loss + F.smooth_l1_loss(
reg_yxtl, target_yxtl,
reduction='none').mean()
iou_loss = iou_loss + liou_loss(
reg_pred, reg_targets,
self.img_w, length=15)
# calculate acc
cls_accuracy = accuracy(cls_pred, cls_target)
cls_acc_stage.append(cls_accuracy)
cls_acc.append(sum(cls_acc_stage) / len(cls_acc_stage))
# extra segmentation loss
seg_loss = self.criterion(F.log_softmax(output['seg'], dim=1),
batch['seg'].long())
cls_loss /= (len(targets) * self.refine_layers)
reg_xytl_loss /= (len(targets) * self.refine_layers)
iou_loss /= (len(targets) * self.refine_layers)
loss = cls_loss * cls_loss_weight + reg_xytl_loss * xyt_loss_weight \
+ seg_loss * seg_loss_weight + iou_loss * iou_loss_weight
return_value = {
'loss': loss,
'loss_stats': {
'loss': loss,
'cls_loss': cls_loss * cls_loss_weight,
'reg_xytl_loss': reg_xytl_loss * xyt_loss_weight,
'seg_loss': seg_loss * seg_loss_weight,
'iou_loss': iou_loss * iou_loss_weight
}
}
for i in range(self.refine_layers):
return_value['loss_stats']['stage_{}_acc'.format(i)] = cls_acc[i]
return return_value
def get_lanes(self, output, as_lanes=True):
'''
Convert model output to lanes.
'''
softmax = nn.Softmax(dim=1)
decoded = []
for predictions in output:
# filter out the conf lower than conf threshold
threshold = self.cfg.test_parameters.conf_threshold
scores = softmax(predictions[:, :2])[:, 1]
keep_inds = scores >= threshold
predictions = predictions[keep_inds]
scores = scores[keep_inds]
if predictions.shape[0] == 0:
decoded.append([])
continue
nms_predictions = predictions.detach().clone()
nms_predictions = torch.cat(
[nms_predictions[..., :4], nms_predictions[..., 5:]], dim=-1)
nms_predictions[..., 4] = nms_predictions[..., 4] * self.n_strips
nms_predictions[...,
5:] = nms_predictions[..., 5:] * (self.img_w - 1)
keep, num_to_keep, _ = nms(
nms_predictions,
scores,
overlap=self.cfg.test_parameters.nms_thres,
top_k=self.cfg.max_lanes)
keep = keep[:num_to_keep]
predictions = predictions[keep]
if predictions.shape[0] == 0:
decoded.append([])
continue
predictions[:, 5] = torch.round(predictions[:, 5] * self.n_strips)
if as_lanes:
pred = self.predictions_to_pred(predictions)
else:
pred = predictions
decoded.append(pred)
return decoded

View File

@@ -0,0 +1,77 @@
import mmcv
import torch.nn as nn
@mmcv.jit(coderize=True)
def accuracy(pred, target, topk=1, thresh=None):
"""Calculate accuracy according to the prediction and target.
Args:
pred (torch.Tensor): The model prediction, shape (N, num_class)
target (torch.Tensor): The target of each prediction, shape (N, )
topk (int | tuple[int], optional): If the predictions in ``topk``
matches the target, the predictions will be regarded as
correct ones. Defaults to 1.
thresh (float, optional): If not None, predictions with scores under
this threshold are considered incorrect. Default to None.
Returns:
float | tuple[float]: If the input ``topk`` is a single integer,
the function will return a single float as accuracy. If
``topk`` is a tuple containing multiple integers, the
function will return a tuple containing accuracies of
each ``topk`` number.
"""
assert isinstance(topk, (int, tuple))
if isinstance(topk, int):
topk = (topk, )
return_single = True
else:
return_single = False
maxk = max(topk)
if pred.size(0) == 0:
accu = [pred.new_tensor(0.) for i in range(len(topk))]
return accu[0] if return_single else accu
assert pred.ndim == 2 and target.ndim == 1
assert pred.size(0) == target.size(0)
assert maxk <= pred.size(1), \
f'maxk {maxk} exceeds pred dimension {pred.size(1)}'
pred_value, pred_label = pred.topk(maxk, dim=1)
pred_label = pred_label.t() # transpose to shape (maxk, N)
correct = pred_label.eq(target.view(1, -1).expand_as(pred_label))
if thresh is not None:
# Only prediction values larger than thresh are counted as correct
correct = correct & (pred_value > thresh).t()
res = []
for k in topk:
correct_k = correct[:k].reshape(-1).float().sum(0, keepdim=True)
res.append(correct_k.mul_(100.0 / pred.size(0)))
return res[0] if return_single else res
class Accuracy(nn.Module):
def __init__(self, topk=(1, ), thresh=None):
"""Module to calculate the accuracy.
Args:
topk (tuple, optional): The criterion used to calculate the
accuracy. Defaults to (1,).
thresh (float, optional): If not None, predictions with scores
under this threshold are considered incorrect. Default to None.
"""
super().__init__()
self.topk = topk
self.thresh = thresh
def forward(self, pred, target):
"""Forward function to calculate accuracy.
Args:
pred (torch.Tensor): Prediction of models.
target (torch.Tensor): Target for each prediction.
Returns:
tuple[float]: The accuracies under different topk criterions.
"""
return accuracy(pred, target, self.topk, self.thresh)

View File

@@ -0,0 +1,191 @@
# pylint: disable-all
from typing import Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
# Source: https://github.com/kornia/kornia/blob/f4f70fefb63287f72bc80cd96df9c061b1cb60dd/kornia/losses/focal.py
class SoftmaxFocalLoss(nn.Module):
def __init__(self, gamma, ignore_lb=255, *args, **kwargs):
super(SoftmaxFocalLoss, self).__init__()
self.gamma = gamma
self.nll = nn.NLLLoss(ignore_index=ignore_lb)
def forward(self, logits, labels):
scores = F.softmax(logits, dim=1)
factor = torch.pow(1. - scores, self.gamma)
log_score = F.log_softmax(logits, dim=1)
log_score = factor * log_score
loss = self.nll(log_score, labels)
return loss
def one_hot(labels: torch.Tensor,
num_classes: int,
device: Optional[torch.device] = None,
dtype: Optional[torch.dtype] = None,
eps: Optional[float] = 1e-6) -> torch.Tensor:
r"""Converts an integer label x-D tensor to a one-hot (x+1)-D tensor.
Args:
labels (torch.Tensor) : tensor with labels of shape :math:`(N, *)`,
where N is batch size. Each value is an integer
representing correct classification.
num_classes (int): number of classes in labels.
device (Optional[torch.device]): the desired device of returned tensor.
Default: if None, uses the current device for the default tensor type
(see torch.set_default_tensor_type()). device will be the CPU for CPU
tensor types and the current CUDA device for CUDA tensor types.
dtype (Optional[torch.dtype]): the desired data type of returned
tensor. Default: if None, infers data type from values.
Returns:
torch.Tensor: the labels in one hot tensor of shape :math:`(N, C, *)`,
Examples::
>>> labels = torch.LongTensor([[[0, 1], [2, 0]]])
>>> kornia.losses.one_hot(labels, num_classes=3)
tensor([[[[1., 0.],
[0., 1.]],
[[0., 1.],
[0., 0.]],
[[0., 0.],
[1., 0.]]]]
"""
if not torch.is_tensor(labels):
raise TypeError(
"Input labels type is not a torch.Tensor. Got {}".format(
type(labels)))
if not labels.dtype == torch.int64:
raise ValueError(
"labels must be of the same dtype torch.int64. Got: {}".format(
labels.dtype))
if num_classes < 1:
raise ValueError("The number of classes must be bigger than one."
" Got: {}".format(num_classes))
shape = labels.shape
one_hot = torch.zeros(shape[0],
num_classes,
*shape[1:],
device=device,
dtype=dtype)
return one_hot.scatter_(1, labels.unsqueeze(1), 1.0) + eps
def focal_loss(input: torch.Tensor,
target: torch.Tensor,
alpha: float,
gamma: float = 2.0,
reduction: str = 'none',
eps: float = 1e-8) -> torch.Tensor:
r"""Function that computes Focal loss.
See :class:`~kornia.losses.FocalLoss` for details.
"""
if not torch.is_tensor(input):
raise TypeError("Input type is not a torch.Tensor. Got {}".format(
type(input)))
if not len(input.shape) >= 2:
raise ValueError(
"Invalid input shape, we expect BxCx*. Got: {}".format(
input.shape))
if input.size(0) != target.size(0):
raise ValueError(
'Expected input batch_size ({}) to match target batch_size ({}).'.
format(input.size(0), target.size(0)))
n = input.size(0)
out_size = (n, ) + input.size()[2:]
if target.size()[1:] != input.size()[2:]:
raise ValueError('Expected target size {}, got {}'.format(
out_size, target.size()))
if not input.device == target.device:
raise ValueError(
"input and target must be in the same device. Got: {} and {}".
format(input.device, target.device))
# compute softmax over the classes axis
input_soft: torch.Tensor = F.softmax(input, dim=1) + eps
# create the labels one hot tensor
target_one_hot: torch.Tensor = one_hot(target,
num_classes=input.shape[1],
device=input.device,
dtype=input.dtype)
# compute the actual focal loss
weight = torch.pow(-input_soft + 1., gamma)
focal = -alpha * weight * torch.log(input_soft)
loss_tmp = torch.sum(target_one_hot * focal, dim=1)
if reduction == 'none':
loss = loss_tmp
elif reduction == 'mean':
loss = torch.mean(loss_tmp)
elif reduction == 'sum':
loss = torch.sum(loss_tmp)
else:
raise NotImplementedError(
"Invalid reduction mode: {}".format(reduction))
return loss
class FocalLoss(nn.Module):
r"""Criterion that computes Focal loss.
According to [1], the Focal loss is computed as follows:
.. math::
\text{FL}(p_t) = -\alpha_t (1 - p_t)^{\gamma} \, \text{log}(p_t)
where:
- :math:`p_t` is the model's estimated probability for each class.
Arguments:
alpha (float): Weighting factor :math:`\alpha \in [0, 1]`.
gamma (float): Focusing parameter :math:`\gamma >= 0`.
reduction (str, optional): Specifies the reduction to apply to the
output: none | mean | sum. none: no reduction will be applied,
mean: the sum of the output will be divided by the number of elements
in the output, sum: the output will be summed. Default: none.
Shape:
- Input: :math:`(N, C, *)` where C = number of classes.
- Target: :math:`(N, *)` where each value is
:math:`0 ≤ targets[i] ≤ C1`.
Examples:
>>> N = 5 # num_classes
>>> kwargs = {"alpha": 0.5, "gamma": 2.0, "reduction": 'mean'}
>>> loss = kornia.losses.FocalLoss(**kwargs)
>>> input = torch.randn(1, N, 3, 5, requires_grad=True)
>>> target = torch.empty(1, 3, 5, dtype=torch.long).random_(N)
>>> output = loss(input, target)
>>> output.backward()
References:
[1] https://arxiv.org/abs/1708.02002
"""
def __init__(self,
alpha: float,
gamma: float = 2.0,
reduction: str = 'none') -> None:
super(FocalLoss, self).__init__()
self.alpha: float = alpha
self.gamma: float = gamma
self.reduction: str = reduction
self.eps: float = 1e-6
def forward( # type: ignore
self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
return focal_loss(input, target, self.alpha, self.gamma,
self.reduction, self.eps)

View File

@@ -0,0 +1,38 @@
import torch
def line_iou(pred, target, img_w, length=15, aligned=True):
'''
Calculate the line iou value between predictions and targets
Args:
pred: lane predictions, shape: (num_pred, 72)
target: ground truth, shape: (num_target, 72)
img_w: image width
length: extended radius
aligned: True for iou loss calculation, False for pair-wise ious in assign
'''
px1 = pred - length
px2 = pred + length
tx1 = target - length
tx2 = target + length
if aligned:
invalid_mask = target
ovr = torch.min(px2, tx2) - torch.max(px1, tx1)
union = torch.max(px2, tx2) - torch.min(px1, tx1)
else:
num_pred = pred.shape[0]
invalid_mask = target.repeat(num_pred, 1, 1)
ovr = (torch.min(px2[:, None, :], tx2[None, ...]) -
torch.max(px1[:, None, :], tx1[None, ...]))
union = (torch.max(px2[:, None, :], tx2[None, ...]) -
torch.min(px1[:, None, :], tx1[None, ...]))
invalid_masks = (invalid_mask < 0) | (invalid_mask >= img_w)
ovr[invalid_masks] = 0.
union[invalid_masks] = 0.
iou = ovr.sum(dim=-1) / (union.sum(dim=-1) + 1e-9)
return iou
def liou_loss(pred, target, img_w, length=15):
return (1 - line_iou(pred, target, img_w, length)).mean()

View File

@@ -0,0 +1,2 @@
from .fpn import FPN
from .pafpn import PAFPN

View File

@@ -0,0 +1,167 @@
import warnings
import torch
import torch.nn as nn
import torch.nn.functional as F
from mmcv.cnn import ConvModule
from ..registry import NECKS
@NECKS.register_module
class FPN(nn.Module):
def __init__(self,
in_channels,
out_channels,
num_outs,
start_level=0,
end_level=-1,
add_extra_convs=False,
extra_convs_on_inputs=True,
relu_before_extra_convs=False,
no_norm_on_lateral=False,
conv_cfg=None,
norm_cfg=None,
attention=False,
act_cfg=None,
upsample_cfg=dict(mode='nearest'),
init_cfg=dict(type='Xavier',
layer='Conv2d',
distribution='uniform'),
cfg=None):
super(FPN, self).__init__()
assert isinstance(in_channels, list)
self.in_channels = in_channels
self.out_channels = out_channels
self.num_ins = len(in_channels)
self.num_outs = num_outs
self.attention = attention
self.relu_before_extra_convs = relu_before_extra_convs
self.no_norm_on_lateral = no_norm_on_lateral
self.upsample_cfg = upsample_cfg.copy()
if end_level == -1:
self.backbone_end_level = self.num_ins
assert num_outs >= self.num_ins - start_level
else:
# if end_level < inputs, no extra level is allowed
self.backbone_end_level = end_level
assert end_level <= len(in_channels)
assert num_outs == end_level - start_level
self.start_level = start_level
self.end_level = end_level
self.add_extra_convs = add_extra_convs
assert isinstance(add_extra_convs, (str, bool))
if isinstance(add_extra_convs, str):
# Extra_convs_source choices: 'on_input', 'on_lateral', 'on_output'
assert add_extra_convs in ('on_input', 'on_lateral', 'on_output')
elif add_extra_convs: # True
if extra_convs_on_inputs:
# TODO: deprecate `extra_convs_on_inputs`
warnings.simplefilter('once')
warnings.warn(
'"extra_convs_on_inputs" will be deprecated in v2.9.0,'
'Please use "add_extra_convs"', DeprecationWarning)
self.add_extra_convs = 'on_input'
else:
self.add_extra_convs = 'on_output'
self.lateral_convs = nn.ModuleList()
self.fpn_convs = nn.ModuleList()
for i in range(self.start_level, self.backbone_end_level):
l_conv = ConvModule(
in_channels[i],
out_channels,
1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg if not self.no_norm_on_lateral else None,
act_cfg=act_cfg,
inplace=False)
fpn_conv = ConvModule(out_channels,
out_channels,
3,
padding=1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=act_cfg,
inplace=False)
self.lateral_convs.append(l_conv)
self.fpn_convs.append(fpn_conv)
# add extra conv layers (e.g., RetinaNet)
extra_levels = num_outs - self.backbone_end_level + self.start_level
if self.add_extra_convs and extra_levels >= 1:
for i in range(extra_levels):
if i == 0 and self.add_extra_convs == 'on_input':
in_channels = self.in_channels[self.backbone_end_level - 1]
else:
in_channels = out_channels
extra_fpn_conv = ConvModule(in_channels,
out_channels,
3,
stride=2,
padding=1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=act_cfg,
inplace=False)
self.fpn_convs.append(extra_fpn_conv)
def forward(self, inputs):
"""Forward function."""
assert len(inputs) >= len(self.in_channels)
if len(inputs) > len(self.in_channels):
for _ in range(len(inputs) - len(self.in_channels)):
del inputs[0]
# build laterals
laterals = [
lateral_conv(inputs[i + self.start_level])
for i, lateral_conv in enumerate(self.lateral_convs)
]
# build top-down path
used_backbone_levels = len(laterals)
for i in range(used_backbone_levels - 1, 0, -1):
# In some cases, fixing `scale factor` (e.g. 2) is preferred, but
# it cannot co-exist with `size` in `F.interpolate`.
if 'scale_factor' in self.upsample_cfg:
laterals[i - 1] += F.interpolate(laterals[i],
**self.upsample_cfg)
else:
prev_shape = laterals[i - 1].shape[2:]
laterals[i - 1] += F.interpolate(laterals[i],
size=prev_shape,
**self.upsample_cfg)
# build outputs
# part 1: from original levels
outs = [
self.fpn_convs[i](laterals[i]) for i in range(used_backbone_levels)
]
# part 2: add extra levels
if self.num_outs > len(outs):
# use max pool to get more levels on top of outputs
# (e.g., Faster R-CNN, Mask R-CNN)
if not self.add_extra_convs:
for i in range(self.num_outs - used_backbone_levels):
outs.append(F.max_pool2d(outs[-1], 1, stride=2))
# add conv layers on top of original feature maps (RetinaNet)
else:
if self.add_extra_convs == 'on_input':
extra_source = inputs[self.backbone_end_level - 1]
elif self.add_extra_convs == 'on_lateral':
extra_source = laterals[-1]
elif self.add_extra_convs == 'on_output':
extra_source = outs[-1]
else:
raise NotImplementedError
outs.append(self.fpn_convs[used_backbone_levels](extra_source))
for i in range(used_backbone_levels + 1, self.num_outs):
if self.relu_before_extra_convs:
outs.append(self.fpn_convs[i](F.relu(outs[-1])))
else:
outs.append(self.fpn_convs[i](outs[-1]))
return tuple(outs)

View File

@@ -0,0 +1,154 @@
import torch.nn as nn
import torch.nn.functional as F
from mmcv.cnn import ConvModule
from mmcv.runner import auto_fp16
from ..registry import NECKS
from .fpn import FPN
@NECKS.register_module
class PAFPN(FPN):
"""Path Aggregation Network for Instance Segmentation.
This is an implementation of the `PAFPN in Path Aggregation Network
<https://arxiv.org/abs/1803.01534>`_.
Args:
in_channels (List[int]): Number of input channels per scale.
out_channels (int): Number of output channels (used at each scale)
num_outs (int): Number of output scales.
start_level (int): Index of the start input backbone level used to
build the feature pyramid. Default: 0.
end_level (int): Index of the end input backbone level (exclusive) to
build the feature pyramid. Default: -1, which means the last level.
add_extra_convs (bool): Whether to add conv layers on top of the
original feature maps. Default: False.
extra_convs_on_inputs (bool): Whether to apply extra conv on
the original feature from the backbone. Default: False.
relu_before_extra_convs (bool): Whether to apply relu before the extra
conv. Default: False.
no_norm_on_lateral (bool): Whether to apply norm on lateral.
Default: False.
conv_cfg (dict): Config dict for convolution layer. Default: None.
norm_cfg (dict): Config dict for normalization layer. Default: None.
act_cfg (str): Config dict for activation layer in ConvModule.
Default: None.
"""
def __init__(self,
in_channels,
out_channels,
num_outs,
start_level=0,
end_level=-1,
add_extra_convs=False,
extra_convs_on_inputs=True,
relu_before_extra_convs=False,
no_norm_on_lateral=False,
conv_cfg=None,
norm_cfg=None,
act_cfg=None,
cfg=None,
attention=False):
super(PAFPN, self).__init__(in_channels,
out_channels,
num_outs,
start_level,
end_level,
add_extra_convs,
extra_convs_on_inputs,
relu_before_extra_convs,
no_norm_on_lateral,
conv_cfg,
norm_cfg,
attention,
act_cfg,
cfg=cfg)
# add extra bottom up pathway
self.downsample_convs = nn.ModuleList()
self.pafpn_convs = nn.ModuleList()
for i in range(self.start_level + 1, self.backbone_end_level):
d_conv = ConvModule(out_channels,
out_channels,
3,
stride=2,
padding=1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=act_cfg,
inplace=False)
pafpn_conv = ConvModule(out_channels,
out_channels,
3,
padding=1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=act_cfg,
inplace=False)
self.downsample_convs.append(d_conv)
self.pafpn_convs.append(pafpn_conv)
def forward(self, inputs):
"""Forward function."""
assert len(inputs) >= len(self.in_channels)
if len(inputs) > len(self.in_channels):
for _ in range(len(inputs) - len(self.in_channels)):
del inputs[0]
# build laterals
laterals = [
lateral_conv(inputs[i + self.start_level])
for i, lateral_conv in enumerate(self.lateral_convs)
]
# build top-down path
used_backbone_levels = len(laterals)
for i in range(used_backbone_levels - 1, 0, -1):
prev_shape = laterals[i - 1].shape[2:]
laterals[i - 1] += F.interpolate(laterals[i],
size=prev_shape,
mode='nearest')
# build outputs
# part 1: from original levels
inter_outs = [
self.fpn_convs[i](laterals[i]) for i in range(used_backbone_levels)
]
# part 2: add bottom-up path
for i in range(0, used_backbone_levels - 1):
inter_outs[i + 1] += self.downsample_convs[i](inter_outs[i])
outs = []
outs.append(inter_outs[0])
outs.extend([
self.pafpn_convs[i - 1](inter_outs[i])
for i in range(1, used_backbone_levels)
])
# part 3: add extra levels
if self.num_outs > len(outs):
# use max pool to get more levels on top of outputs
# (e.g., Faster R-CNN, Mask R-CNN)
if not self.add_extra_convs:
for i in range(self.num_outs - used_backbone_levels):
outs.append(F.max_pool2d(outs[-1], 1, stride=2))
# add conv layers on top of original feature maps (RetinaNet)
else:
if self.add_extra_convs == 'on_input':
orig = inputs[self.backbone_end_level - 1]
outs.append(self.fpn_convs[used_backbone_levels](orig))
elif self.add_extra_convs == 'on_lateral':
outs.append(self.fpn_convs[used_backbone_levels](
laterals[-1]))
elif self.add_extra_convs == 'on_output':
outs.append(self.fpn_convs[used_backbone_levels](outs[-1]))
else:
raise NotImplementedError
for i in range(used_backbone_levels + 1, self.num_outs):
if self.relu_before_extra_convs:
outs.append(self.fpn_convs[i](F.relu(outs[-1])))
else:
outs.append(self.fpn_convs[i](outs[-1]))
return tuple(outs)

View File

@@ -0,0 +1 @@
from .detector import Detector

View File

@@ -0,0 +1,36 @@
import torch.nn as nn
import torch
from clrnet.models.registry import NETS
from ..registry import build_backbones, build_aggregator, build_heads, build_necks
@NETS.register_module
class Detector(nn.Module):
def __init__(self, cfg):
super(Detector, self).__init__()
self.cfg = cfg
self.backbone = build_backbones(cfg)
self.aggregator = build_aggregator(cfg) if cfg.haskey('aggregator') else None
self.neck = build_necks(cfg) if cfg.haskey('neck') else None
self.heads = build_heads(cfg)
def get_lanes(self):
return self.heads.get_lanes(output)
def forward(self, batch):
output = {}
fea = self.backbone(batch['img'] if isinstance(batch, dict) else batch)
if self.aggregator:
fea[-1] = self.aggregator(fea[-1])
if self.neck:
fea = self.neck(fea)
if self.training:
output = self.heads(fea, batch=batch)
else:
output = self.heads(fea)
return output

View File

@@ -0,0 +1,45 @@
from clrnet.utils import Registry, build_from_cfg
import torch.nn as nn
BACKBONES = Registry('backbones')
AGGREGATORS = Registry('aggregators')
HEADS = Registry('heads')
NECKS = Registry('necks')
NETS = Registry('nets')
def build(cfg, registry, default_args=None):
if isinstance(cfg, list):
modules = [
build_from_cfg(cfg_, registry, default_args) for cfg_ in cfg
]
return nn.Sequential(*modules)
else:
return build_from_cfg(cfg, registry, default_args)
def build_backbones(cfg):
return build(cfg.backbone, BACKBONES, default_args=dict(cfg=cfg))
def build_necks(cfg):
return build(cfg.necks, NECKS, default_args=dict(cfg=cfg))
def build_aggregator(cfg):
return build(cfg.aggregator, AGGREGATORS, default_args=dict(cfg=cfg))
def build_heads(cfg):
return build(cfg.heads, HEADS, default_args=dict(cfg=cfg))
def build_head(split_cfg, cfg):
return build(split_cfg, HEADS, default_args=dict(cfg=cfg))
def build_net(cfg):
return build(cfg.net, NETS, default_args=dict(cfg=cfg))
def build_necks(cfg):
return build(cfg.neck, NECKS, default_args=dict(cfg=cfg))

View File

@@ -0,0 +1,140 @@
import torch
from clrnet.models.losses.lineiou_loss import line_iou
def distance_cost(predictions, targets, img_w):
"""
repeat predictions and targets to generate all combinations
use the abs distance as the new distance cost
"""
num_priors = predictions.shape[0]
num_targets = targets.shape[0]
predictions = torch.repeat_interleave(
predictions, num_targets, dim=0
)[...,
6:] # repeat_interleave'ing [a, b] 2 times gives [a, a, b, b] ((np + nt) * 78)
targets = torch.cat(
num_priors *
[targets])[...,
6:] # applying this 2 times on [c, d] gives [c, d, c, d]
invalid_masks = (targets < 0) | (targets >= img_w)
lengths = (~invalid_masks).sum(dim=1)
distances = torch.abs((targets - predictions))
distances[invalid_masks] = 0.
distances = distances.sum(dim=1) / (lengths.float() + 1e-9)
distances = distances.view(num_priors, num_targets)
return distances
def focal_cost(cls_pred, gt_labels, alpha=0.25, gamma=2, eps=1e-12):
"""
Args:
cls_pred (Tensor): Predicted classification logits, shape
[num_query, num_class].
gt_labels (Tensor): Label of `gt_bboxes`, shape (num_gt,).
Returns:
torch.Tensor: cls_cost value
"""
cls_pred = cls_pred.sigmoid()
neg_cost = -(1 - cls_pred + eps).log() * (1 - alpha) * cls_pred.pow(gamma)
pos_cost = -(cls_pred + eps).log() * alpha * (1 - cls_pred).pow(gamma)
cls_cost = pos_cost[:, gt_labels] - neg_cost[:, gt_labels]
return cls_cost
def dynamic_k_assign(cost, pair_wise_ious):
"""
Assign grouth truths with priors dynamically.
Args:
cost: the assign cost.
pair_wise_ious: iou of grouth truth and priors.
Returns:
prior_idx: the index of assigned prior.
gt_idx: the corresponding ground truth index.
"""
matching_matrix = torch.zeros_like(cost)
ious_matrix = pair_wise_ious
ious_matrix[ious_matrix < 0] = 0.
n_candidate_k = 4
topk_ious, _ = torch.topk(ious_matrix, n_candidate_k, dim=0)
dynamic_ks = torch.clamp(topk_ious.sum(0).int(), min=1)
num_gt = cost.shape[1]
for gt_idx in range(num_gt):
_, pos_idx = torch.topk(cost[:, gt_idx],
k=dynamic_ks[gt_idx].item(),
largest=False)
matching_matrix[pos_idx, gt_idx] = 1.0
del topk_ious, dynamic_ks, pos_idx
matched_gt = matching_matrix.sum(1)
if (matched_gt > 1).sum() > 0:
_, cost_argmin = torch.min(cost[matched_gt > 1, :], dim=1)
matching_matrix[matched_gt > 1, 0] *= 0.0
matching_matrix[matched_gt > 1, cost_argmin] = 1.0
prior_idx = matching_matrix.sum(1).nonzero()
gt_idx = matching_matrix[prior_idx].argmax(-1)
return prior_idx.flatten(), gt_idx.flatten()
def assign(
predictions,
targets,
img_w,
img_h,
distance_cost_weight=3.,
cls_cost_weight=1.,
):
'''
computes dynamicly matching based on the cost, including cls cost and lane similarity cost
Args:
predictions (Tensor): predictions predicted by each stage, shape: (num_priors, 78)
targets (Tensor): lane targets, shape: (num_targets, 78)
return:
matched_row_inds (Tensor): matched predictions, shape: (num_targets)
matched_col_inds (Tensor): matched targets, shape: (num_targets)
'''
predictions = predictions.detach().clone()
predictions[:, 3] *= (img_w - 1)
predictions[:, 6:] *= (img_w - 1)
targets = targets.detach().clone()
# distances cost
distances_score = distance_cost(predictions, targets, img_w)
distances_score = 1 - (distances_score / torch.max(distances_score)
) + 1e-2 # normalize the distance
# classification cost
cls_score = focal_cost(predictions[:, :2], targets[:, 1].long())
num_priors = predictions.shape[0]
num_targets = targets.shape[0]
target_start_xys = targets[:, 2:4] # num_targets, 2
target_start_xys[..., 0] *= (img_h - 1)
prediction_start_xys = predictions[:, 2:4]
prediction_start_xys[..., 0] *= (img_h - 1)
start_xys_score = torch.cdist(prediction_start_xys, target_start_xys,
p=2).reshape(num_priors, num_targets)
start_xys_score = (1 - start_xys_score / torch.max(start_xys_score)) + 1e-2
target_thetas = targets[:, 4].unsqueeze(-1)
theta_score = torch.cdist(predictions[:, 4].unsqueeze(-1),
target_thetas,
p=1).reshape(num_priors, num_targets) * 180
theta_score = (1 - theta_score / torch.max(theta_score)) + 1e-2
cost = -(distances_score * start_xys_score * theta_score
)**2 * distance_cost_weight + cls_score * cls_cost_weight
iou = line_iou(predictions[..., 6:], targets[..., 6:], img_w, aligned=False)
matched_row_inds, matched_col_inds = dynamic_k_assign(cost, iou)
return matched_row_inds, matched_col_inds

View File

@@ -0,0 +1,136 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
from mmcv.cnn import ConvModule
def LinearModule(hidden_dim):
return nn.ModuleList(
[nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(inplace=True)])
class FeatureResize(nn.Module):
def __init__(self, size=(10, 25)):
super(FeatureResize, self).__init__()
self.size = size
def forward(self, x):
x = F.interpolate(x, self.size)
return x.flatten(2)
class ROIGather(nn.Module):
'''
ROIGather module for gather global information
Args:
in_channels: prior feature channels
num_priors: prior numbers we predefined
sample_points: the number of sampled points when we extract feature from line
fc_hidden_dim: the fc output channel
refine_layers: the total number of layers to build refine
'''
def __init__(self,
in_channels,
num_priors,
sample_points,
fc_hidden_dim,
refine_layers,
mid_channels=48):
super(ROIGather, self).__init__()
self.in_channels = in_channels
self.num_priors = num_priors
self.f_key = ConvModule(in_channels=self.in_channels,
out_channels=self.in_channels,
kernel_size=1,
stride=1,
padding=0,
norm_cfg=dict(type='BN'))
self.f_query = nn.Sequential(
nn.Conv1d(in_channels=num_priors,
out_channels=num_priors,
kernel_size=1,
stride=1,
padding=0,
groups=num_priors),
nn.ReLU(),
)
self.f_value = nn.Conv2d(in_channels=self.in_channels,
out_channels=self.in_channels,
kernel_size=1,
stride=1,
padding=0)
self.W = nn.Conv1d(in_channels=num_priors,
out_channels=num_priors,
kernel_size=1,
stride=1,
padding=0,
groups=num_priors)
self.resize = FeatureResize()
nn.init.constant_(self.W.weight, 0)
nn.init.constant_(self.W.bias, 0)
self.convs = nn.ModuleList()
self.catconv = nn.ModuleList()
for i in range(refine_layers):
self.convs.append(
ConvModule(in_channels,
mid_channels, (9, 1),
padding=(4, 0),
bias=False,
norm_cfg=dict(type='BN')))
self.catconv.append(
ConvModule(mid_channels * (i + 1),
in_channels, (9, 1),
padding=(4, 0),
bias=False,
norm_cfg=dict(type='BN')))
self.fc = nn.Linear(sample_points * fc_hidden_dim, fc_hidden_dim)
self.fc_norm = nn.LayerNorm(fc_hidden_dim)
def roi_fea(self, x, layer_index):
feats = []
for i, feature in enumerate(x):
feat_trans = self.convs[i](feature)
feats.append(feat_trans)
cat_feat = torch.cat(feats, dim=1)
cat_feat = self.catconv[layer_index](cat_feat)
return cat_feat
def forward(self, roi_features, x, layer_index):
'''
Args:
roi_features: prior feature, shape: (Batch * num_priors, prior_feat_channel, sample_point, 1)
x: feature map
layer_index: currently on which layer to refine
Return:
roi: prior features with gathered global information, shape: (Batch, num_priors, fc_hidden_dim)
'''
roi = self.roi_fea(roi_features, layer_index)
bs = x.size(0)
roi = roi.contiguous().view(bs * self.num_priors, -1)
roi = F.relu(self.fc_norm(self.fc(roi)))
roi = roi.view(bs, self.num_priors, -1)
query = roi
value = self.resize(self.f_value(x))
query = self.f_query(query)
key = self.f_key(x)
value = value.permute(0, 2, 1)
key = self.resize(key)
sim_map = torch.matmul(query, key)
sim_map = (self.in_channels**-.5) * sim_map
sim_map = F.softmax(sim_map, dim=-1)
context = torch.matmul(sim_map, value)
context = self.W(context)
roi = roi + F.dropout(context, p=0.1, training=self.training)
return roi

View File

@@ -0,0 +1,29 @@
import torch.nn as nn
import torch.nn.functional as F
class SegDecoder(nn.Module):
'''
Optionaly seg decoder
'''
def __init__(self,
image_height,
image_width,
num_class,
prior_feat_channels=64,
refine_layers=3):
super().__init__()
self.dropout = nn.Dropout2d(0.1)
self.conv = nn.Conv2d(prior_feat_channels * refine_layers, num_class,
1)
self.image_height = image_height
self.image_width = image_width
def forward(self, x):
x = self.dropout(x)
x = self.conv(x)
x = F.interpolate(x,
size=[self.image_height, self.image_width],
mode='bilinear',
align_corners=False)
return x

View File

@@ -0,0 +1,3 @@
from .nms import nms
__all__ = ['nms']

View File

@@ -0,0 +1,62 @@
/* Copyright (c) 2018, Grégoire Payen de La Garanderie, Durham University
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <torch/extension.h>
#include <torch/types.h>
#include <iostream>
std::vector<at::Tensor> nms_cuda_forward(
at::Tensor boxes,
at::Tensor idx,
float nms_overlap_thresh,
unsigned long top_k);
#define CHECK_CUDA(x) AT_ASSERTM(x.type().is_cuda(), #x " must be a CUDA tensor")
#define CHECK_CONTIGUOUS(x) AT_ASSERTM(x.is_contiguous(), #x " must be contiguous")
#define CHECK_INPUT(x) CHECK_CUDA(x); CHECK_CONTIGUOUS(x)
std::vector<at::Tensor> nms_forward(
at::Tensor boxes,
at::Tensor scores,
float thresh,
unsigned long top_k) {
auto idx = std::get<1>(scores.sort(0,true));
CHECK_INPUT(boxes);
CHECK_INPUT(idx);
return nms_cuda_forward(boxes, idx, thresh, top_k);
}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("nms_forward", &nms_forward, "NMS");
}

View File

@@ -0,0 +1,193 @@
#include <torch/extension.h>
#include <ATen/ATen.h>
#include <cuda.h>
#include <cuda_runtime.h>
#include <vector>
#include <iostream>
// Hard-coded maximum. Increase if needed.
#define MAX_COL_BLOCKS 1000
#define STRIDE 4
#define N_OFFSETS 72 // if you use more than 73 offsets you will have to adjust this value
#define N_STRIPS (N_OFFSETS - 1)
#define PROP_SIZE (5 + N_OFFSETS)
#define DATASET_OFFSET 0
#define DIVUP(m,n) (((m)+(n)-1) / (n))
int64_t const threadsPerBlock = sizeof(unsigned long long) * 8;
// The functions below originates from Fast R-CNN
// See https://github.com/rbgirshick/py-faster-rcnn
// Copyright (c) 2015 Microsoft
// Licensed under The MIT License
// Written by Shaoqing Ren
template <typename scalar_t>
// __device__ inline scalar_t devIoU(scalar_t const * const a, scalar_t const * const b) {
__device__ inline bool devIoU(scalar_t const * const a, scalar_t const * const b, const float threshold) {
const int start_a = (int) (a[2] * N_STRIPS - DATASET_OFFSET + 0.5); // 0.5 rounding trick
const int start_b = (int) (b[2] * N_STRIPS - DATASET_OFFSET + 0.5);
const int start = max(start_a, start_b);
const int end_a = start_a + a[4] - 1 + 0.5 - ((a[4] - 1) < 0); // - (x<0) trick to adjust for negative numbers (in case length is 0)
const int end_b = start_b + b[4] - 1 + 0.5 - ((b[4] - 1) < 0);
const int end = min(min(end_a, end_b), N_OFFSETS - 1);
// if (end < start) return 1e9;
if (end < start) return false;
scalar_t dist = 0;
for(unsigned char i = 5 + start; i <= 5 + end; ++i) {
if (a[i] < b[i]) {
dist += b[i] - a[i];
} else {
dist += a[i] - b[i];
}
}
// return (dist / (end - start + 1)) < threshold;
return dist < (threshold * (end - start + 1));
// return dist / (end - start + 1);
}
template <typename scalar_t>
__global__ void nms_kernel(const int64_t n_boxes, const scalar_t nms_overlap_thresh,
const scalar_t *dev_boxes, const int64_t *idx, int64_t *dev_mask) {
const int64_t row_start = blockIdx.y;
const int64_t col_start = blockIdx.x;
if (row_start > col_start) return;
const int row_size =
min(n_boxes - row_start * threadsPerBlock, threadsPerBlock);
const int col_size =
min(n_boxes - col_start * threadsPerBlock, threadsPerBlock);
__shared__ scalar_t block_boxes[threadsPerBlock * PROP_SIZE];
if (threadIdx.x < col_size) {
for (int i = 0; i < PROP_SIZE; ++i) {
block_boxes[threadIdx.x * PROP_SIZE + i] = dev_boxes[idx[(threadsPerBlock * col_start + threadIdx.x)] * PROP_SIZE + i];
}
// block_boxes[threadIdx.x * 4 + 0] =
// dev_boxes[idx[(threadsPerBlock * col_start + threadIdx.x)] * 4 + 0];
// block_boxes[threadIdx.x * 4 + 1] =
// dev_boxes[idx[(threadsPerBlock * col_start + threadIdx.x)] * 4 + 1];
// block_boxes[threadIdx.x * 4 + 2] =
// dev_boxes[idx[(threadsPerBlock * col_start + threadIdx.x)] * 4 + 2];
// block_boxes[threadIdx.x * 4 + 3] =
// dev_boxes[idx[(threadsPerBlock * col_start + threadIdx.x)] * 4 + 3];
}
__syncthreads();
if (threadIdx.x < row_size) {
const int cur_box_idx = threadsPerBlock * row_start + threadIdx.x;
const scalar_t *cur_box = dev_boxes + idx[cur_box_idx] * PROP_SIZE;
int i = 0;
unsigned long long t = 0;
int start = 0;
if (row_start == col_start) {
start = threadIdx.x + 1;
}
for (i = start; i < col_size; i++) {
if (devIoU(cur_box, block_boxes + i * PROP_SIZE, nms_overlap_thresh)) {
t |= 1ULL << i;
}
}
const int col_blocks = DIVUP(n_boxes, threadsPerBlock);
dev_mask[cur_box_idx * col_blocks + col_start] = t;
}
}
__global__ void nms_collect(const int64_t boxes_num, const int64_t col_blocks, int64_t top_k, const int64_t *idx, const int64_t *mask, int64_t *keep, int64_t *parent_object_index, int64_t *num_to_keep) {
int64_t remv[MAX_COL_BLOCKS];
int64_t num_to_keep_ = 0;
for (int i = 0; i < col_blocks; i++) {
remv[i] = 0;
}
for (int i = 0; i < boxes_num; ++i) {
parent_object_index[i] = 0;
}
for (int i = 0; i < boxes_num; i++) {
int nblock = i / threadsPerBlock;
int inblock = i % threadsPerBlock;
if (!(remv[nblock] & (1ULL << inblock))) {
int64_t idxi = idx[i];
keep[num_to_keep_] = idxi;
const int64_t *p = &mask[0] + i * col_blocks;
for (int j = nblock; j < col_blocks; j++) {
remv[j] |= p[j];
}
for (int j = i; j < boxes_num; j++) {
int nblockj = j / threadsPerBlock;
int inblockj = j % threadsPerBlock;
if (p[nblockj] & (1ULL << inblockj))
parent_object_index[idx[j]] = num_to_keep_+1;
}
parent_object_index[idx[i]] = num_to_keep_+1;
num_to_keep_++;
if (num_to_keep_==top_k)
break;
}
}
// Initialize the rest of the keep array to avoid uninitialized values.
for (int i = num_to_keep_; i < boxes_num; ++i)
keep[i] = 0;
*num_to_keep = min(top_k,num_to_keep_);
}
#define CHECK_CONTIGUOUS(x) AT_ASSERTM(x.is_contiguous(), #x " must be contiguous")
std::vector<at::Tensor> nms_cuda_forward(
at::Tensor boxes,
at::Tensor idx,
float nms_overlap_thresh,
unsigned long top_k) {
const auto boxes_num = boxes.size(0);
TORCH_CHECK(boxes.size(1) == PROP_SIZE, "Wrong number of offsets. Please adjust `PROP_SIZE`");
const int col_blocks = DIVUP(boxes_num, threadsPerBlock);
AT_ASSERTM (col_blocks < MAX_COL_BLOCKS, "The number of column blocks must be less than MAX_COL_BLOCKS. Increase the MAX_COL_BLOCKS constant if needed.");
auto longOptions = torch::TensorOptions().device(torch::kCUDA).dtype(torch::kLong);
auto mask = at::empty({boxes_num * col_blocks}, longOptions);
dim3 blocks(DIVUP(boxes_num, threadsPerBlock),
DIVUP(boxes_num, threadsPerBlock));
dim3 threads(threadsPerBlock);
CHECK_CONTIGUOUS(boxes);
CHECK_CONTIGUOUS(idx);
CHECK_CONTIGUOUS(mask);
AT_DISPATCH_FLOATING_TYPES(boxes.type(), "nms_cuda_forward", ([&] {
nms_kernel<<<blocks, threads>>>(boxes_num,
(scalar_t)nms_overlap_thresh,
boxes.data<scalar_t>(),
idx.data<int64_t>(),
mask.data<int64_t>());
}));
auto keep = at::empty({boxes_num}, longOptions);
auto parent_object_index = at::empty({boxes_num}, longOptions);
auto num_to_keep = at::empty({}, longOptions);
nms_collect<<<1, 1>>>(boxes_num, col_blocks, top_k,
idx.data<int64_t>(),
mask.data<int64_t>(),
keep.data<int64_t>(),
parent_object_index.data<int64_t>(),
num_to_keep.data<int64_t>());
return {keep,num_to_keep,parent_object_index};
}

View File

@@ -0,0 +1,33 @@
# Copyright (c) 2018, Grégoire Payen de La Garanderie, Durham University
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from . import nms_impl
def nms(boxes, scores, overlap, top_k):
return nms_impl.nms_forward(boxes, scores, overlap, top_k)

View File

@@ -0,0 +1,2 @@
from .config import Config
from .registry import Registry, build_from_cfg

View File

@@ -0,0 +1,67 @@
# Bilinear grid_sample using Pad + Gather (ONNX/RKNN friendly).
# Ref: https://zenn.dev/pinto0309/scraps/7d4032067d0160
# https://github.com/ibaiGorordo/CREStereo-Pytorch/.../nets/utils/utils.py
import torch
import torch.nn.functional as F
def bilinear_grid_sample(im, grid, align_corners=False):
"""Drop-in replacement for F.grid_sample(..., mode='bilinear', padding_zeros).
Args:
im: (N, C, H, W)
grid: (N, Hg, Wg, 2), x/y in [-1, 1] (same as grid_sample)
"""
n, c, h, w = im.shape
gn, gh, gw, _ = grid.shape
assert n == gn
x = grid[..., 0]
y = grid[..., 1]
if align_corners:
x = ((x + 1) / 2) * (w - 1)
y = ((y + 1) / 2) * (h - 1)
else:
x = ((x + 1) * w - 1) / 2
y = ((y + 1) * h - 1) / 2
x = x.reshape(n, -1)
y = y.reshape(n, -1)
x0 = torch.floor(x).long()
y0 = torch.floor(y).long()
x1 = x0 + 1
y1 = y0 + 1
wa = ((x1 - x) * (y1 - y)).unsqueeze(1)
wb = ((x1 - x) * (y - y0)).unsqueeze(1)
wc = ((x - x0) * (y1 - y)).unsqueeze(1)
wd = ((x - x0) * (y - y0)).unsqueeze(1)
im_padded = F.pad(im, pad=[1, 1, 1, 1], mode='constant', value=0)
padded_h = h + 2
padded_w = w + 2
x0, x1, y0, y1 = x0 + 1, x1 + 1, y0 + 1, y1 + 1
# Clip in float so ONNX exports valid Clip (ORT rejects int64 Clip bounds).
x0 = x0.float().clamp(0, padded_w - 1).long()
x1 = x1.float().clamp(0, padded_w - 1).long()
y0 = y0.float().clamp(0, padded_h - 1).long()
y1 = y1.float().clamp(0, padded_h - 1).long()
im_flat = im_padded.reshape(n, c, -1)
x0_y0 = (x0 + y0 * padded_w).unsqueeze(1).expand(-1, c, -1)
x0_y1 = (x0 + y1 * padded_w).unsqueeze(1).expand(-1, c, -1)
x1_y0 = (x1 + y0 * padded_w).unsqueeze(1).expand(-1, c, -1)
x1_y1 = (x1 + y1 * padded_w).unsqueeze(1).expand(-1, c, -1)
ia = torch.gather(im_flat, 2, x0_y0)
ib = torch.gather(im_flat, 2, x0_y1)
ic = torch.gather(im_flat, 2, x1_y0)
id_ = torch.gather(im_flat, 2, x1_y1)
out = ia * wa + ib * wb + ic * wc + id_ * wd
return out.reshape(n, c, gh, gw)

View File

@@ -0,0 +1,418 @@
# Copyright (c) Open-MMLab. All rights reserved.
import ast
import os.path as osp
import shutil
import sys
import tempfile
from argparse import Action, ArgumentParser
from collections import abc
from importlib import import_module
from addict import Dict
from yapf.yapflib.yapf_api import FormatCode
BASE_KEY = '_base_'
DELETE_KEY = '_delete_'
RESERVED_KEYS = ['filename', 'text', 'pretty_text']
def check_file_exist(filename, msg_tmpl='file "{}" does not exist'):
if not osp.isfile(filename):
raise FileNotFoundError(msg_tmpl.format(filename))
class ConfigDict(Dict):
def __missing__(self, name):
raise KeyError(name)
def __getattr__(self, name):
try:
value = super(ConfigDict, self).__getattr__(name)
except KeyError:
ex = AttributeError(f"'{self.__class__.__name__}' object has no "
f"attribute '{name}'")
except Exception as e:
ex = e
else:
return value
raise ex
def add_args(parser, cfg, prefix=''):
for k, v in cfg.items():
if isinstance(v, str):
parser.add_argument('--' + prefix + k)
elif isinstance(v, int):
parser.add_argument('--' + prefix + k, type=int)
elif isinstance(v, float):
parser.add_argument('--' + prefix + k, type=float)
elif isinstance(v, bool):
parser.add_argument('--' + prefix + k, action='store_true')
elif isinstance(v, dict):
add_args(parser, v, prefix + k + '.')
elif isinstance(v, abc.Iterable):
parser.add_argument('--' + prefix + k, type=type(v[0]), nargs='+')
else:
print(f'cannot parse key {prefix + k} of type {type(v)}')
return parser
class Config:
"""A facility for config and config files.
It supports common file formats as configs: python/json/yaml. The interface
is the same as a dict object and also allows access config values as
attributes.
Example:
>>> cfg = Config(dict(a=1, b=dict(b1=[0, 1])))
>>> cfg.a
1
>>> cfg.b
{'b1': [0, 1]}
>>> cfg.b.b1
[0, 1]
>>> cfg = Config.fromfile('tests/data/config/a.py')
>>> cfg.filename
"/home/kchen/projects/mmcv/tests/data/config/a.py"
>>> cfg.item4
'test'
>>> cfg
"Config [path: /home/kchen/projects/mmcv/tests/data/config/a.py]: "
"{'item1': [1, 2], 'item2': {'a': 0}, 'item3': True, 'item4': 'test'}"
"""
@staticmethod
def _validate_py_syntax(filename):
with open(filename) as f:
content = f.read()
try:
ast.parse(content)
except SyntaxError:
raise SyntaxError('There are syntax errors in config '
f'file {filename}')
@staticmethod
def _file2dict(filename):
filename = osp.abspath(osp.expanduser(filename))
check_file_exist(filename)
if filename.endswith('.py'):
with tempfile.TemporaryDirectory() as temp_config_dir:
temp_config_file = tempfile.NamedTemporaryFile(
dir=temp_config_dir, suffix='.py')
temp_config_name = osp.basename(temp_config_file.name)
shutil.copyfile(filename,
osp.join(temp_config_dir, temp_config_name))
temp_module_name = osp.splitext(temp_config_name)[0]
sys.path.insert(0, temp_config_dir)
Config._validate_py_syntax(filename)
mod = import_module(temp_module_name)
sys.path.pop(0)
cfg_dict = {
name: value
for name, value in mod.__dict__.items()
if not name.startswith('__')
}
# delete imported module
del sys.modules[temp_module_name]
# close temp file
temp_config_file.close()
elif filename.endswith(('.yml', '.yaml', '.json')):
import mmcv
cfg_dict = mmcv.load(filename)
else:
raise IOError('Only py/yml/yaml/json type are supported now!')
cfg_text = ''
with open(filename, 'r') as f:
cfg_text += f.read()
if BASE_KEY in cfg_dict:
cfg_dir = osp.dirname(filename)
base_filename = cfg_dict.pop(BASE_KEY)
base_filename = base_filename if isinstance(
base_filename, list) else [base_filename]
cfg_dict_list = list()
cfg_text_list = list()
for f in base_filename:
_cfg_dict, _cfg_text = Config._file2dict(osp.join(cfg_dir, f))
cfg_dict_list.append(_cfg_dict)
cfg_text_list.append(_cfg_text)
base_cfg_dict = dict()
for c in cfg_dict_list:
if len(base_cfg_dict.keys() & c.keys()) > 0:
raise KeyError('Duplicate key is not allowed among bases')
base_cfg_dict.update(c)
base_cfg_dict = Config._merge_a_into_b(cfg_dict, base_cfg_dict)
cfg_dict = base_cfg_dict
# merge cfg_text
cfg_text_list.append(cfg_text)
cfg_text = '\n'.join(cfg_text_list)
return cfg_dict, cfg_text
@staticmethod
def _merge_a_into_b(a, b):
# merge dict `a` into dict `b` (non-inplace). values in `a` will
# overwrite `b`.
# copy first to avoid inplace modification
b = b.copy()
for k, v in a.items():
if isinstance(v, dict) and k in b and not v.pop(DELETE_KEY, False):
if not isinstance(b[k], dict):
raise TypeError(
f'{k}={v} in child config cannot inherit from base '
f'because {k} is a dict in the child config but is of '
f'type {type(b[k])} in base config. You may set '
f'`{DELETE_KEY}=True` to ignore the base config')
b[k] = Config._merge_a_into_b(v, b[k])
else:
b[k] = v
return b
@staticmethod
def fromfile(filename):
cfg_dict, cfg_text = Config._file2dict(filename)
return Config(cfg_dict, cfg_text=cfg_text, filename=filename)
@staticmethod
def auto_argparser(description=None):
"""Generate argparser from config file automatically (experimental)
"""
partial_parser = ArgumentParser(description=description)
partial_parser.add_argument('config', help='config file path')
cfg_file = partial_parser.parse_known_args()[0].config
cfg = Config.fromfile(cfg_file)
parser = ArgumentParser(description=description)
parser.add_argument('config', help='config file path')
add_args(parser, cfg)
return parser, cfg
def __init__(self, cfg_dict=None, cfg_text=None, filename=None):
if cfg_dict is None:
cfg_dict = dict()
elif not isinstance(cfg_dict, dict):
raise TypeError('cfg_dict must be a dict, but '
f'got {type(cfg_dict)}')
for key in cfg_dict:
if key in RESERVED_KEYS:
raise KeyError(f'{key} is reserved for config file')
super(Config, self).__setattr__('_cfg_dict', ConfigDict(cfg_dict))
super(Config, self).__setattr__('_filename', filename)
if cfg_text:
text = cfg_text
elif filename:
with open(filename, 'r') as f:
text = f.read()
else:
text = ''
super(Config, self).__setattr__('_text', text)
@property
def filename(self):
return self._filename
@property
def text(self):
return self._text
@property
def pretty_text(self):
indent = 4
def _indent(s_, num_spaces):
s = s_.split('\n')
if len(s) == 1:
return s_
first = s.pop(0)
s = [(num_spaces * ' ') + line for line in s]
s = '\n'.join(s)
s = first + '\n' + s
return s
def _format_basic_types(k, v, use_mapping=False):
if isinstance(v, str):
v_str = f"'{v}'"
else:
v_str = str(v)
if use_mapping:
k_str = f"'{k}'" if isinstance(k, str) else str(k)
attr_str = f'{k_str}: {v_str}'
else:
attr_str = f'{str(k)}={v_str}'
attr_str = _indent(attr_str, indent)
return attr_str
def _format_list(k, v, use_mapping=False):
# check if all items in the list are dict
if all(isinstance(_, dict) for _ in v):
v_str = '[\n'
v_str += '\n'.join(
f'dict({_indent(_format_dict(v_), indent)}),'
for v_ in v).rstrip(',')
if use_mapping:
k_str = f"'{k}'" if isinstance(k, str) else str(k)
attr_str = f'{k_str}: {v_str}'
else:
attr_str = f'{str(k)}={v_str}'
attr_str = _indent(attr_str, indent) + ']'
else:
attr_str = _format_basic_types(k, v, use_mapping)
return attr_str
def _contain_invalid_identifier(dict_str):
contain_invalid_identifier = False
for key_name in dict_str:
contain_invalid_identifier |= \
(not str(key_name).isidentifier())
return contain_invalid_identifier
def _format_dict(input_dict, outest_level=False):
r = ''
s = []
use_mapping = _contain_invalid_identifier(input_dict)
if use_mapping:
r += '{'
for idx, (k, v) in enumerate(input_dict.items()):
is_last = idx >= len(input_dict) - 1
end = '' if outest_level or is_last else ','
if isinstance(v, dict):
v_str = '\n' + _format_dict(v)
if use_mapping:
k_str = f"'{k}'" if isinstance(k, str) else str(k)
attr_str = f'{k_str}: dict({v_str}'
else:
attr_str = f'{str(k)}=dict({v_str}'
attr_str = _indent(attr_str, indent) + ')' + end
elif isinstance(v, list):
attr_str = _format_list(k, v, use_mapping) + end
else:
attr_str = _format_basic_types(k, v, use_mapping) + end
s.append(attr_str)
r += '\n'.join(s)
if use_mapping:
r += '}'
return r
cfg_dict = self._cfg_dict.to_dict()
text = _format_dict(cfg_dict, outest_level=True)
# copied from setup.cfg
yapf_style = dict(based_on_style='pep8',
blank_line_before_nested_class_or_def=True,
split_before_expression_after_opening_paren=True)
text, _ = FormatCode(text, style_config=yapf_style, verify=True)
return text
def __repr__(self):
return f'Config (path: {self.filename}): {self._cfg_dict.__repr__()}'
def __len__(self):
return len(self._cfg_dict)
def __getattr__(self, name):
return getattr(self._cfg_dict, name)
def __getitem__(self, name):
return self._cfg_dict.__getitem__(name)
def __setattr__(self, name, value):
if isinstance(value, dict):
value = ConfigDict(value)
self._cfg_dict.__setattr__(name, value)
def __setitem__(self, name, value):
if isinstance(value, dict):
value = ConfigDict(value)
self._cfg_dict.__setitem__(name, value)
def __iter__(self):
return iter(self._cfg_dict)
def haskey(self, name):
return hasattr(self._cfg_dict, name)
def dump(self, file=None):
cfg_dict = super(Config, self).__getattribute__('_cfg_dict').to_dict()
if self.filename.endswith('.py'):
if file is None:
return self.pretty_text
else:
with open(file, 'w') as f:
f.write(self.pretty_text)
else:
import mmcv
if file is None:
file_format = self.filename.split('.')[-1]
return mmcv.dump(cfg_dict, file_format=file_format)
else:
mmcv.dump(cfg_dict, file)
def has_attr_in_cfg(self, name):
return hasattr(self._cfg_dict, name)
def merge_from_dict(self, options):
"""Merge list into cfg_dict
Merge the dict parsed by MultipleKVAction into this cfg.
Examples:
>>> options = {'model.backbone.depth': 50,
... 'model.backbone.with_cp':True}
>>> cfg = Config(dict(model=dict(backbone=dict(type='ResNet'))))
>>> cfg.merge_from_dict(options)
>>> cfg_dict = super(Config, self).__getattribute__('_cfg_dict')
>>> assert cfg_dict == dict(
... model=dict(backbone=dict(depth=50, with_cp=True)))
Args:
options (dict): dict of configs to merge from.
"""
option_cfg_dict = {}
for full_key, v in options.items():
d = option_cfg_dict
key_list = full_key.split('.')
for subkey in key_list[:-1]:
d.setdefault(subkey, ConfigDict())
d = d[subkey]
subkey = key_list[-1]
d[subkey] = v
cfg_dict = super(Config, self).__getattribute__('_cfg_dict')
super(Config, self).__setattr__(
'_cfg_dict', Config._merge_a_into_b(option_cfg_dict, cfg_dict))
class DictAction(Action):
"""
argparse action to split an argument into KEY=VALUE form
on the first = and append to a dictionary. List options should
be passed as comma separated values, i.e KEY=V1,V2,V3
"""
@staticmethod
def _parse_int_float_bool(val):
try:
return int(val)
except ValueError:
pass
try:
return float(val)
except ValueError:
pass
if val.lower() in ['true', 'false']:
return True if val.lower() == 'true' else False
return val
def __call__(self, parser, namespace, values, option_string=None):
options = {}
for kv in values:
key, val = kv.split('=', maxsplit=1)
val = [self._parse_int_float_bool(v) for v in val.split(',')]
if len(val) == 1:
val = val[0]
options[key] = val
setattr(namespace, self.dest, options)

View File

@@ -0,0 +1,257 @@
import os
import argparse
from functools import partial
import cv2
import numpy as np
from tqdm import tqdm
from p_tqdm import t_map, p_map
from scipy.interpolate import splprep, splev
from scipy.optimize import linear_sum_assignment
from shapely.geometry import LineString, Polygon
def draw_lane(lane, img=None, img_shape=None, width=30):
if img is None:
img = np.zeros(img_shape, dtype=np.uint8)
lane = lane.astype(np.int32)
for p1, p2 in zip(lane[:-1], lane[1:]):
cv2.line(img,
tuple(p1),
tuple(p2),
color=(255, 255, 255),
thickness=width)
return img
def discrete_cross_iou(xs, ys, width=30, img_shape=(590, 1640, 3)):
xs = [draw_lane(lane, img_shape=img_shape, width=width) > 0 for lane in xs]
ys = [draw_lane(lane, img_shape=img_shape, width=width) > 0 for lane in ys]
ious = np.zeros((len(xs), len(ys)))
for i, x in enumerate(xs):
for j, y in enumerate(ys):
ious[i, j] = (x & y).sum() / (x | y).sum()
return ious
def continuous_cross_iou(xs, ys, width=30, img_shape=(590, 1640, 3)):
h, w, _ = img_shape
image = Polygon([(0, 0), (0, h - 1), (w - 1, h - 1), (w - 1, 0)])
xs = [
LineString(lane).buffer(distance=width / 2., cap_style=1,
join_style=2).intersection(image)
for lane in xs
]
ys = [
LineString(lane).buffer(distance=width / 2., cap_style=1,
join_style=2).intersection(image)
for lane in ys
]
ious = np.zeros((len(xs), len(ys)))
for i, x in enumerate(xs):
for j, y in enumerate(ys):
ious[i, j] = x.intersection(y).area / x.union(y).area
return ious
def interp(points, n=50):
x = [x for x, _ in points]
y = [y for _, y in points]
tck, u = splprep([x, y], s=0, t=n, k=min(3, len(points) - 1))
u = np.linspace(0., 1., num=(len(u) - 1) * n + 1)
return np.array(splev(u, tck)).T
def culane_metric(pred,
anno,
width=30,
iou_thresholds=[0.5],
official=True,
img_shape=(590, 1640, 3)):
_metric = {}
for thr in iou_thresholds:
tp = 0
fp = 0 if len(anno) != 0 else len(pred)
fn = 0 if len(pred) != 0 else len(anno)
_metric[thr] = [tp, fp, fn]
interp_pred = np.array([interp(pred_lane, n=5) for pred_lane in pred],
dtype=object) # (4, 50, 2)
interp_anno = np.array([interp(anno_lane, n=5) for anno_lane in anno],
dtype=object) # (4, 50, 2)
if official:
ious = discrete_cross_iou(interp_pred,
interp_anno,
width=width,
img_shape=img_shape)
else:
ious = continuous_cross_iou(interp_pred,
interp_anno,
width=width,
img_shape=img_shape)
row_ind, col_ind = linear_sum_assignment(1 - ious)
_metric = {}
for thr in iou_thresholds:
tp = int((ious[row_ind, col_ind] > thr).sum())
fp = len(pred) - tp
fn = len(anno) - tp
_metric[thr] = [tp, fp, fn]
return _metric
def load_culane_img_data(path):
with open(path, 'r') as data_file:
img_data = data_file.readlines()
img_data = [line.split() for line in img_data]
img_data = [list(map(float, lane)) for lane in img_data]
img_data = [[(lane[i], lane[i + 1]) for i in range(0, len(lane), 2)]
for lane in img_data]
img_data = [lane for lane in img_data if len(lane) >= 2]
return img_data
def load_culane_data(data_dir, file_list_path):
with open(file_list_path, 'r') as file_list:
filepaths = [
os.path.join(
data_dir, line[1 if line[0] == '/' else 0:].rstrip().replace(
'.jpg', '.lines.txt')) for line in file_list.readlines()
]
data = []
for path in filepaths:
img_data = load_culane_img_data(path)
data.append(img_data)
return data
def eval_predictions(pred_dir,
anno_dir,
list_path,
iou_thresholds=[0.5],
width=30,
official=True,
sequential=False):
import logging
logger = logging.getLogger(__name__)
logger.info('Calculating metric for List: {}'.format(list_path))
predictions = load_culane_data(pred_dir, list_path)
annotations = load_culane_data(anno_dir, list_path)
img_shape = (590, 1640, 3)
if sequential:
results = map(
partial(culane_metric,
width=width,
official=official,
iou_thresholds=iou_thresholds,
img_shape=img_shape), predictions, annotations)
else:
from multiprocessing import Pool, cpu_count
from itertools import repeat
with Pool(cpu_count()) as p:
results = p.starmap(culane_metric, zip(predictions, annotations,
repeat(width),
repeat(iou_thresholds),
repeat(official),
repeat(img_shape)))
mean_f1, mean_prec, mean_recall, total_tp, total_fp, total_fn = 0, 0, 0, 0, 0, 0
ret = {}
for thr in iou_thresholds:
tp = sum(m[thr][0] for m in results)
fp = sum(m[thr][1] for m in results)
fn = sum(m[thr][2] for m in results)
precision = float(tp) / (tp + fp) if tp != 0 else 0
recall = float(tp) / (tp + fn) if tp != 0 else 0
f1 = 2 * precision * recall / (precision + recall) if tp !=0 else 0
logger.info('iou thr: {:.2f}, tp: {}, fp: {}, fn: {},'
'precision: {}, recall: {}, f1: {}'.format(
thr, tp, fp, fn, precision, recall, f1))
mean_f1 += f1 / len(iou_thresholds)
mean_prec += precision / len(iou_thresholds)
mean_recall += recall / len(iou_thresholds)
total_tp += tp
total_fp += fp
total_fn += fn
ret[thr] = {
'TP': tp,
'FP': fp,
'FN': fn,
'Precision': precision,
'Recall': recall,
'F1': f1
}
if len(iou_thresholds) > 2:
logger.info('mean result, total_tp: {}, total_fp: {}, total_fn: {},'
'precision: {}, recall: {}, f1: {}'.format(total_tp, total_fp,
total_fn, mean_prec, mean_recall, mean_f1))
ret['mean'] = {
'TP': total_tp,
'FP': total_fp,
'FN': total_fn,
'Precision': mean_prec,
'Recall': mean_recall,
'F1': mean_f1
}
return ret
def main():
args = parse_args()
for list_path in args.list:
results = eval_predictions(args.pred_dir,
args.anno_dir,
list_path,
width=args.width,
official=args.official,
sequential=args.sequential)
header = '=' * 20 + ' Results ({})'.format(
os.path.basename(list_path)) + '=' * 20
print(header)
for metric, value in results.items():
if isinstance(value, float):
print('{}: {:.4f}'.format(metric, value))
else:
print('{}: {}'.format(metric, value))
print('=' * len(header))
def parse_args():
parser = argparse.ArgumentParser(description="Measure CULane's metric")
parser.add_argument(
"--pred_dir",
help="Path to directory containing the predicted lanes",
required=True)
parser.add_argument(
"--anno_dir",
help="Path to directory containing the annotated lanes",
required=True)
parser.add_argument("--width",
type=int,
default=30,
help="Width of the lane")
parser.add_argument("--list",
nargs='+',
help="Path to txt file containing the list of files",
required=True)
parser.add_argument("--sequential",
action='store_true',
help="Run sequentially instead of in parallel")
parser.add_argument("--official",
action='store_true',
help="Use official way to calculate the metric")
return parser.parse_args()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,90 @@
"""Merge train_packs for CLRNet (same layout as UFLD lane0_copy)."""
from __future__ import annotations
import json
from pathlib import Path
def parse_gt_line(line: str) -> tuple[str, str] | None:
parts = line.strip().split()
if len(parts) < 2:
return None
return parts[0].lstrip("/"), parts[1].lstrip("/")
def load_registry(data_root: Path) -> dict:
p = data_root / "datasets_registry.json"
if p.is_file():
return json.loads(p.read_text(encoding="utf-8"))
return {}
def resolve_pack_dir(pack: str, data_root: Path, registry: dict) -> str:
aliases = registry.get("aliases", {})
pack_dirs = registry.get("pack_dirs", {})
if pack in aliases:
pack = aliases[pack]
if pack in pack_dirs:
pack = pack_dirs[pack]
if not (data_root / pack).is_dir():
raise FileNotFoundError(f"pack not found: {data_root / pack} ({pack!r})")
return pack
def apply_pack_prefix(img: str, msk: str, prefix: str) -> tuple[str, str]:
if not prefix:
return img, msk
if not img.startswith(prefix):
img = prefix + img
if not msk.startswith(prefix):
msk = prefix + msk
return img, msk
def resolve_list_file(cfg, split: str = "train") -> str | None:
"""Return list path relative to cfg.dataset_path, or None to use defaults."""
packs_key = "train_packs" if split == "train" else "val_packs"
packs = getattr(cfg, packs_key, None)
if not packs:
return getattr(cfg, f"{split}_list_file", None)
if isinstance(packs, str):
packs = [p.strip() for p in packs.split(",") if p.strip()]
else:
packs = list(packs)
data_root = Path(cfg.dataset_path).resolve()
registry = load_registry(data_root)
pack_dirs = [resolve_pack_dir(p, data_root, registry) for p in packs]
list_name = getattr(cfg, "pack_list_name", "list/train_gt.txt")
if split == "val":
list_name = getattr(cfg, "pack_val_list_name", "list/val_gt.txt")
merged_dir = Path(getattr(cfg, "merged_list_dir", "lists_merged"))
safe = "__".join(pack_dirs)
out_name = getattr(cfg, f"merged_{split}_list", None) or f"{split}__{safe}.txt"
out_path = data_root / merged_dir / out_name
out_rel = (merged_dir / out_name).as_posix()
if getattr(cfg, "remerge_lists", False) or not out_path.is_file():
merged = []
seen = set()
for pack_dir in pack_dirs:
prefix = f"{pack_dir}/"
list_path = data_root / pack_dir / list_name
if not list_path.is_file():
raise FileNotFoundError(list_path)
for line in list_path.read_text(encoding="utf-8", errors="replace").splitlines():
p = parse_gt_line(line)
if not p:
continue
img, msk = apply_pack_prefix(p[0], p[1], prefix)
if img in seen:
continue
seen.add(img)
merged.append(f"{img} {msk}")
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text("\n".join(merged) + "\n", encoding="utf-8")
return out_rel

View File

@@ -0,0 +1,50 @@
from scipy.interpolate import InterpolatedUnivariateSpline
import numpy as np
class Lane:
def __init__(self, points=None, invalid_value=-2., metadata=None):
super(Lane, self).__init__()
self.curr_iter = 0
self.points = points
self.invalid_value = invalid_value
self.function = InterpolatedUnivariateSpline(points[:, 1],
points[:, 0],
k=min(3,
len(points) - 1))
self.min_y = points[:, 1].min() - 0.01
self.max_y = points[:, 1].max() + 0.01
self.metadata = metadata or {}
def __repr__(self):
return '[Lane]\n' + str(self.points) + '\n[/Lane]'
def __call__(self, lane_ys):
lane_xs = self.function(lane_ys)
lane_xs[(lane_ys < self.min_y) |
(lane_ys > self.max_y)] = self.invalid_value
return lane_xs
def to_array(self, cfg):
sample_y = cfg.sample_y
img_w, img_h = cfg.ori_img_w, cfg.ori_img_h
ys = np.array(sample_y) / float(img_h)
xs = self(ys)
valid_mask = (xs >= 0) & (xs < 1)
lane_xs = xs[valid_mask] * img_w
lane_ys = ys[valid_mask] * img_h
lane = np.concatenate((lane_xs.reshape(-1, 1), lane_ys.reshape(-1, 1)),
axis=1)
return lane
def __iter__(self):
return self
def __next__(self):
if self.curr_iter < len(self.points):
self.curr_iter += 1
return self.points[self.curr_iter - 1]
self.curr_iter = 0
raise StopIteration

View File

@@ -0,0 +1,315 @@
""" Evaluation script for the CULane metric on the LLAMAS dataset.
This script will compute the F1, precision and recall metrics as described in the CULane benchmark.
The predictions format is the same one used in the CULane benchmark.
In summary, for every annotation file:
labels/a/b/c.json
There should be a prediction file:
predictions/a/b/c.lines.txt
Inside each .lines.txt file each line will contain a sequence of points (x, y) separated by spaces.
For more information, please see https://xingangpan.github.io/projects/CULane.html
This script uses two methods to compute the IoU: one using an image to draw the lanes (named `discrete` here) and
another one that uses shapes with the shapely library (named `continuous` here). The results achieved with the first
method are very close to the official CULane implementation. Although the second should be a more exact method and is
faster to compute, it deviates more from the official implementation. By default, the method closer to the official
metric is used.
"""
import os
import argparse
from functools import partial
import cv2
import numpy as np
from p_tqdm import t_map, p_map
from scipy.interpolate import splprep, splev
from scipy.optimize import linear_sum_assignment
from shapely.geometry import LineString, Polygon
import clrnet.utils.llamas_utils as llamas_utils
LLAMAS_IMG_RES = (717, 1276)
def add_ys(xs):
"""For each x in xs, make a tuple with x and its corresponding y."""
xs = np.array(xs[300:])
valid = xs >= 0
xs = xs[valid]
assert len(xs) > 1
ys = np.arange(300, 717)[valid]
return list(zip(xs, ys))
def draw_lane(lane, img=None, img_shape=None, width=30):
"""Draw a lane (a list of points) on an image by drawing a line with width `width` through each
pair of points i and i+i"""
if img is None:
img = np.zeros(img_shape, dtype=np.uint8)
lane = lane.astype(np.int32)
for p1, p2 in zip(lane[:-1], lane[1:]):
cv2.line(img, tuple(p1), tuple(p2), color=(1, ), thickness=width)
return img
def discrete_cross_iou(xs, ys, width=30, img_shape=LLAMAS_IMG_RES):
"""For each lane in xs, compute its Intersection Over Union (IoU) with each lane in ys by drawing the lanes on
an image"""
xs = [draw_lane(lane, img_shape=img_shape, width=width) > 0 for lane in xs]
ys = [draw_lane(lane, img_shape=img_shape, width=width) > 0 for lane in ys]
ious = np.zeros((len(xs), len(ys)))
for i, x in enumerate(xs):
for j, y in enumerate(ys):
# IoU by the definition: sum all intersections (binary and) and divide by the sum of the union (binary or)
ious[i, j] = (x & y).sum() / (x | y).sum()
return ious
def continuous_cross_iou(xs, ys, width=30, img_shape=LLAMAS_IMG_RES):
"""For each lane in xs, compute its Intersection Over Union (IoU) with each lane in ys using the area between each
pair of points"""
h, w = img_shape
image = Polygon([(0, 0), (0, h - 1), (w - 1, h - 1), (w - 1, 0)])
xs = [
LineString(lane).buffer(distance=width / 2., cap_style=1,
join_style=2).intersection(image)
for lane in xs
]
ys = [
LineString(lane).buffer(distance=width / 2., cap_style=1,
join_style=2).intersection(image)
for lane in ys
]
ious = np.zeros((len(xs), len(ys)))
for i, x in enumerate(xs):
for j, y in enumerate(ys):
ious[i, j] = x.intersection(y).area / x.union(y).area
return ious
def interpolate_lane(points, n=50):
"""Spline interpolation of a lane. Used on the predictions"""
x = [x for x, _ in points]
y = [y for _, y in points]
tck, _ = splprep([x, y], s=0, t=n, k=min(3, len(points) - 1))
u = np.linspace(0., 1., n)
return np.array(splev(u, tck)).T
def culane_metric(pred,
anno,
width=30,
iou_thresholds=[0.5],
unofficial=False,
img_shape=LLAMAS_IMG_RES):
_metric = {}
for thr in iou_thresholds:
tp = 0
fp = 0 if len(anno) != 0 else len(pred)
fn = 0 if len(pred) != 0 else len(anno)
_metric[thr] = [tp, fp, fn]
"""Computes CULane's metric for a single image"""
if len(pred) == 0:
return 0, 0, len(anno), _metric
if len(anno) == 0:
return 0, len(pred), 0, _metric
interp_pred = np.array([
interpolate_lane(pred_lane, n=50) for pred_lane in pred
]) # (4, 50, 2)
anno = np.array([np.array(anno_lane) for anno_lane in anno], dtype=object)
if unofficial:
ious = continuous_cross_iou(interp_pred, anno, width=width)
else:
ious = discrete_cross_iou(interp_pred,
anno,
width=width,
img_shape=img_shape)
row_ind, col_ind = linear_sum_assignment(1 - ious)
_metric = {}
for thr in iou_thresholds:
tp = int((ious[row_ind, col_ind] > thr).sum())
fp = len(pred) - tp
fn = len(anno) - tp
_metric[thr] = [tp, fp, fn]
return _metric
def load_prediction(path):
"""Loads an image's predictions
Returns a list of lanes, where each lane is a list of points (x,y)
"""
with open(path, 'r') as data_file:
img_data = data_file.readlines()
img_data = [line.split() for line in img_data]
img_data = [list(map(float, lane)) for lane in img_data]
img_data = [[(lane[i], lane[i + 1]) for i in range(0, len(lane), 2)]
for lane in img_data]
img_data = [lane for lane in img_data if len(lane) >= 2]
return img_data
def load_prediction_list(label_paths, pred_dir):
return [
load_prediction(
os.path.join(pred_dir, path.replace('.json', '.lines.txt')))
for path in label_paths
]
def load_labels(label_dir):
"""Loads the annotations and its paths
Each annotation is converted to a list of points (x, y)
"""
label_paths = llamas_utils.get_files_from_folder(label_dir, '.json')
annos = [
[
add_ys(xs) for xs in
llamas_utils.get_horizontal_values_for_four_lanes(label_path)
if (np.array(xs) >= 0).sum() > 1
] # lanes annotated with a single point are ignored
for label_path in label_paths
]
label_paths = [llamas_utils.get_label_base(p) for p in label_paths]
return np.array(annos, dtype=object), np.array(label_paths, dtype=object)
def eval_predictions(pred_dir,
anno_dir,
width=30,
iou_thresholds=[0.5],
unofficial=True,
sequential=False):
"""Evaluates the predictions in pred_dir and returns CULane's metrics (precision, recall, F1 and its components)"""
print(f'Loading annotation data ({anno_dir})...')
os.makedirs('cache', exist_ok=True)
annotations_path = 'cache/llamas_annotations.pkl'
label_path = 'cache/llamas_label_paths.pkl'
import pickle as pkl
if os.path.exists(annotations_path) and os.path.exists(label_path):
with open(annotations_path, 'rb') as cache_file:
annotations = pkl.load(cache_file)
with open(label_path, 'rb') as cache_file:
label_paths = pkl.load(cache_file)
else:
annotations, label_paths = load_labels(anno_dir)
with open(annotations_path, 'wb') as cache_file:
pkl.dump(annotations, cache_file)
with open(label_path, 'wb') as cache_file:
pkl.dump(label_paths, cache_file)
print(f'Loading prediction data ({pred_dir})...')
predictions = load_prediction_list(label_paths, pred_dir)
print('Calculating metric {}...'.format(
'sequentially' if sequential else 'in parallel'))
if sequential:
results = map(
partial(culane_metric,
width=width,
unofficial=unofficial,
img_shape=LLAMAS_IMG_RES), predictions, annotations)
else:
from multiprocessing import Pool, cpu_count
from itertools import repeat
with Pool(cpu_count()) as p:
results = p.starmap(culane_metric, zip(predictions, annotations,
repeat(width),
repeat(iou_thresholds),
repeat(unofficial),
repeat(LLAMAS_IMG_RES)))
import logging
logger = logging.getLogger(__name__)
mean_f1, mean_prec, mean_recall, total_tp, total_fp, total_fn = 0, 0, 0, 0, 0, 0
ret = {}
for thr in iou_thresholds:
tp = sum(m[thr][0] for m in results)
fp = sum(m[thr][1] for m in results)
fn = sum(m[thr][2] for m in results)
precision = float(tp) / (tp + fp) if tp != 0 else 0
recall = float(tp) / (tp + fn) if tp != 0 else 0
f1 = 2 * precision * recall / (precision + recall) if tp !=0 else 0
logger.info('iou thr: {:.2f}, tp: {}, fp: {}, fn: {}, '
'precision: {}, recall: {}, f1: {}'.format(
thr, tp, fp, fn, precision, recall, f1))
mean_f1 += f1 / len(iou_thresholds)
mean_prec += precision / len(iou_thresholds)
mean_recall += recall / len(iou_thresholds)
total_tp += tp
total_fp += fp
total_fn += fn
ret[thr] = {
'TP': tp,
'FP': fp,
'FN': fn,
'Precision': precision,
'Recall': recall,
'F1': f1
}
if len(iou_thresholds) > 2:
logger.info('mean result, total_tp: {}, total_fp: {}, total_fn: {},'
'precision: {}, recall: {}, f1: {}'.format(total_tp, total_fp,
total_fn, mean_prec, mean_recall, mean_f1))
ret['mean'] = {
'TP': total_tp,
'FP': total_fp,
'FN': total_fn,
'Precision': mean_prec,
'Recall': mean_recall,
'F1': mean_f1
}
return ret
def parse_args():
parser = argparse.ArgumentParser(
description="Measure CULane's metric on the LLAMAS dataset")
parser.add_argument(
"--pred_dir",
help="Path to directory containing the predicted lanes",
required=True)
parser.add_argument(
"--anno_dir",
help="Path to directory containing the annotated lanes",
required=True)
parser.add_argument("--width",
type=int,
default=30,
help="Width of the lane")
parser.add_argument("--sequential",
action='store_true',
help="Run sequentially instead of in parallel")
parser.add_argument("--unofficial",
action='store_true',
help="Use a faster but unofficial algorithm")
return parser.parse_args()
def main():
args = parse_args()
results = eval_predictions(args.pred_dir,
args.anno_dir,
width=args.width,
iou_thresholds=[0.5],
unofficial=args.unofficial,
sequential=args.sequential)
header = '=' * 20 + ' Results' + '=' * 20
print(header)
for metric, value in results.items():
if isinstance(value, float):
print('{}: {:.4f}'.format(metric, value))
else:
print('{}: {}'.format(metric, value))
print('=' * len(header))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,389 @@
# All following lines (which were slightly modified) were taken from: https://github.com/karstenBehrendt/unsupervised_llamas
# Its license is copied here
# ##### Begin License ######
# MIT License
# Copyright (c) 2019 Karsten Behrendt, Robert Bosch LLC
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ##### End License ######
# Start code under the previous license
import os
import json
import numpy as np
def get_files_from_folder(directory, extension=None):
"""Get all files within a folder that fit the extension """
# NOTE Can be replaced by glob for newer python versions
label_files = []
for root, _, files in os.walk(directory):
for some_file in files:
label_files.append(os.path.abspath(os.path.join(root, some_file)))
if extension is not None:
label_files = list(filter(lambda x: x.endswith(extension),
label_files))
return label_files
def get_label_base(label_path):
""" Gets directory independent label path """
return '/'.join(label_path.split('/')[-2:])
def get_labels(dataset_root, split='test'):
""" Gets label files of specified dataset split """
label_paths = get_files_from_folder(os.path.join(dataset_root, split),
'.json')
return label_paths
def _extend_lane(lane, projection_matrix):
"""Extends marker closest to the camera
Adds an extra marker that reaches the end of the image
Parameters
----------
lane : iterable of markers
projection_matrix : 3x3 projection matrix
"""
# Unfortunately, we did not store markers beyond the image plane. That hurts us now
# z is the orthongal distance to the car. It's good enough
# The markers are automatically detected, mapped, and labeled. There exist faulty ones,
# e.g., horizontal markers which need to be filtered
filtered_markers = filter(
lambda x: (x['pixel_start']['y'] != x['pixel_end']['y'] and x[
'pixel_start']['x'] != x['pixel_end']['x']), lane['markers'])
# might be the first marker in the list but not guaranteed
closest_marker = min(filtered_markers, key=lambda x: x['world_start']['z'])
if closest_marker['world_start'][
'z'] < 0: # This one likely equals "if False"
return lane
# World marker extension approximation
x_gradient = (closest_marker['world_end']['x'] - closest_marker['world_start']['x']) /\
(closest_marker['world_end']['z'] - closest_marker['world_start']['z'])
y_gradient = (closest_marker['world_end']['y'] - closest_marker['world_start']['y']) /\
(closest_marker['world_end']['z'] - closest_marker['world_start']['z'])
zero_x = closest_marker['world_start']['x'] - (
closest_marker['world_start']['z'] - 1) * x_gradient
zero_y = closest_marker['world_start']['y'] - (
closest_marker['world_start']['z'] - 1) * y_gradient
# Pixel marker extension approximation
pixel_x_gradient = (closest_marker['pixel_end']['x'] - closest_marker['pixel_start']['x']) /\
(closest_marker['pixel_end']['y'] - closest_marker['pixel_start']['y'])
pixel_y_gradient = (closest_marker['pixel_end']['y'] - closest_marker['pixel_start']['y']) /\
(closest_marker['pixel_end']['x'] - closest_marker['pixel_start']['x'])
pixel_zero_x = closest_marker['pixel_start']['x'] + (
716 - closest_marker['pixel_start']['y']) * pixel_x_gradient
if pixel_zero_x < 0:
left_y = closest_marker['pixel_start'][
'y'] - closest_marker['pixel_start']['x'] * pixel_y_gradient
new_pixel_point = (0, left_y)
elif pixel_zero_x > 1276:
right_y = closest_marker['pixel_start']['y'] + (
1276 - closest_marker['pixel_start']['x']) * pixel_y_gradient
new_pixel_point = (1276, right_y)
else:
new_pixel_point = (pixel_zero_x, 716)
new_marker = {
'lane_marker_id': 'FAKE',
'world_end': {
'x': closest_marker['world_start']['x'],
'y': closest_marker['world_start']['y'],
'z': closest_marker['world_start']['z']
},
'world_start': {
'x': zero_x,
'y': zero_y,
'z': 1
},
'pixel_end': {
'x': closest_marker['pixel_start']['x'],
'y': closest_marker['pixel_start']['y']
},
'pixel_start': {
'x': ir(new_pixel_point[0]),
'y': ir(new_pixel_point[1])
}
}
lane['markers'].insert(0, new_marker)
return lane
class SplineCreator():
"""
For each lane divder
- all lines are projected
- linearly interpolated to limit oscillations
- interpolated by a spline
- subsampled to receive individual pixel values
The spline creation can be optimized!
- Better spline parameters
- Extend lowest marker to reach bottom of image would also help
- Extending last marker may in some cases be interesting too
Any help is welcome.
Call create_all_points and get the points in self.sampled_points
It has an x coordinate for each value for each lane
"""
def __init__(self, json_path):
self.json_path = json_path
self.json_content = read_json(json_path)
self.lanes = self.json_content['lanes']
self.lane_marker_points = {}
self.sampled_points = {} # <--- the interesting part
self.debug_image = np.zeros((717, 1276, 3), dtype=np.uint8)
def _sample_points(self, lane, ypp=5, between_markers=True):
""" Markers are given by start and endpoint. This one adds extra points
which need to be considered for the interpolation. Otherwise the spline
could arbitrarily oscillate between start and end of the individual markers
Parameters
----------
lane: polyline, in theory but there are artifacts which lead to inconsistencies
in ordering. There may be parallel lines. The lines may be dashed. It's messy.
ypp: y-pixels per point, e.g. 10 leads to a point every ten pixels
between_markers : bool, interpolates inbetween dashes
Notes
-----
Especially, adding points in the lower parts of the image (high y-values) because
the start and end points are too sparse.
Removing upper lane markers that have starting and end points mapped into the same pixel.
"""
# Collect all x values from all markers along a given line. There may be multiple
# intersecting markers, i.e., multiple entries for some y values
x_values = [[] for i in range(717)]
for marker in lane['markers']:
x_values[marker['pixel_start']['y']].append(
marker['pixel_start']['x'])
height = marker['pixel_start']['y'] - marker['pixel_end']['y']
if height > 2:
slope = (marker['pixel_end']['x'] -
marker['pixel_start']['x']) / height
step_size = (marker['pixel_start']['y'] -
marker['pixel_end']['y']) / float(height)
for i in range(height + 1):
x = marker['pixel_start']['x'] + slope * step_size * i
y = marker['pixel_start']['y'] - step_size * i
x_values[ir(y)].append(ir(x))
# Calculate average x values for each y value
for y, xs in enumerate(x_values):
if not xs:
x_values[y] = -1
else:
x_values[y] = sum(xs) / float(len(xs))
# In the following, we will only interpolate between markers if needed
if not between_markers:
return x_values # TODO ypp
# # interpolate between markers
current_y = 0
while x_values[current_y] == -1: # skip missing first entries
current_y += 1
# Also possible using numpy.interp when accounting for beginning and end
next_set_y = 0
try:
while current_y < 717:
if x_values[current_y] != -1: # set. Nothing to be done
current_y += 1
continue
# Finds target x value for interpolation
while next_set_y <= current_y or x_values[next_set_y] == -1:
next_set_y += 1
if next_set_y >= 717:
raise StopIteration
x_values[current_y] = x_values[current_y - 1] + (x_values[next_set_y] - x_values[current_y - 1]) /\
(next_set_y - current_y + 1)
current_y += 1
except StopIteration:
pass # Done with lane
return x_values
def _lane_points_fit(self, lane):
# TODO name and docstring
""" Fits spline in image space for the markers of a single lane (side)
Parameters
----------
lane: dict as specified in label
Returns
-------
Pixel level values for curve along the y-axis
Notes
-----
This one can be drastically improved. Probably fairly easy as well.
"""
# NOTE all variable names represent image coordinates, interpolation coordinates are swapped!
lane = _extend_lane(lane, self.json_content['projection_matrix'])
sampled_points = self._sample_points(lane, ypp=1)
self.sampled_points[lane['lane_id']] = sampled_points
return sampled_points
def create_all_points(self, ):
""" Creates splines for given label """
for lane in self.lanes:
self._lane_points_fit(lane)
def get_horizontal_values_for_four_lanes(json_path):
""" Gets an x value for every y coordinate for l1, l0, r0, r1
This allows to easily train a direct curve approximation. For each value along
the y-axis, the respective x-values can be compared, e.g. squared distance.
Missing values are filled with -1. Missing values are values missing from the spline.
There is no extrapolation to the image start/end (yet).
But values are interpolated between markers. Space between dashed markers is not missing.
Parameters
----------
json_path: str
path to label-file
Returns
-------
List of [l1, l0, r0, r1], each of which represents a list of ints the length of
the number of vertical pixels of the image
Notes
-----
The points are currently based on the splines. The splines are interpolated based on the
segmentation values. The spline interpolation has lots of room for improvement, e.g.
the lines could be interpolated in 3D, a better approach to spline interpolation could
be used, there is barely any error checking, sometimes the splines oscillate too much.
This was used for a quick poly-line regression training only.
"""
sc = SplineCreator(json_path)
sc.create_all_points()
l1 = sc.sampled_points.get('l1', [-1] * 717)
l0 = sc.sampled_points.get('l0', [-1] * 717)
r0 = sc.sampled_points.get('r0', [-1] * 717)
r1 = sc.sampled_points.get('r1', [-1] * 717)
lanes = [l1, l0, r0, r1]
return lanes
def _filter_lanes_by_size(label, min_height=40):
""" May need some tuning """
filtered_lanes = []
for lane in label['lanes']:
lane_start = min(
[int(marker['pixel_start']['y']) for marker in lane['markers']])
lane_end = max(
[int(marker['pixel_start']['y']) for marker in lane['markers']])
if (lane_end - lane_start) < min_height:
continue
filtered_lanes.append(lane)
label['lanes'] = filtered_lanes
def _filter_few_markers(label, min_markers=2):
"""Filter lines that consist of only few markers"""
filtered_lanes = []
for lane in label['lanes']:
if len(lane['markers']) >= min_markers:
filtered_lanes.append(lane)
label['lanes'] = filtered_lanes
def _fix_lane_names(label):
""" Given keys ['l3', 'l2', 'l0', 'r0', 'r2'] returns ['l2', 'l1', 'l0', 'r0', 'r1']"""
# Create mapping
l_counter = 0
r_counter = 0
mapping = {}
lane_ids = [lane['lane_id'] for lane in label['lanes']]
for key in sorted(lane_ids):
if key[0] == 'l':
mapping[key] = 'l' + str(l_counter)
l_counter += 1
if key[0] == 'r':
mapping[key] = 'r' + str(r_counter)
r_counter += 1
for lane in label['lanes']:
lane['lane_id'] = mapping[lane['lane_id']]
def read_json(json_path, min_lane_height=20):
""" Reads and cleans label file information by path"""
with open(json_path, 'r') as jf:
label_content = json.load(jf)
_filter_lanes_by_size(label_content, min_height=min_lane_height)
_filter_few_markers(label_content, min_markers=2)
_fix_lane_names(label_content)
content = {
'projection_matrix': label_content['projection_matrix'],
'lanes': label_content['lanes']
}
for lane in content['lanes']:
for marker in lane['markers']:
for pixel_key in marker['pixel_start'].keys():
marker['pixel_start'][pixel_key] = int(
marker['pixel_start'][pixel_key])
for pixel_key in marker['pixel_end'].keys():
marker['pixel_end'][pixel_key] = int(
marker['pixel_end'][pixel_key])
for pixel_key in marker['world_start'].keys():
marker['world_start'][pixel_key] = float(
marker['world_start'][pixel_key])
for pixel_key in marker['world_end'].keys():
marker['world_end'][pixel_key] = float(
marker['world_end'][pixel_key])
return content
def ir(some_value):
""" Rounds and casts to int
Useful for pixel values that cannot be floats
Parameters
----------
some_value : float
numeric value
Returns
--------
Rounded integer
Raises
------
ValueError for non scalar types
"""
return int(round(some_value))
# End code under the previous license

View File

@@ -0,0 +1,18 @@
import logging
def init_logger(log_file=None, log_level=logging.INFO):
stream_handler = logging.StreamHandler()
handlers = [stream_handler]
if log_file is not None:
file_handler = logging.FileHandler(log_file, 'w')
handlers.append(file_handler)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
for handler in handlers:
handler.setFormatter(formatter)
handler.setLevel(log_level)
logging.basicConfig(level=log_level, handlers=handlers)

View File

@@ -0,0 +1,60 @@
"""Extract lane polylines from MUFLD/CULane-style segmentation masks."""
from __future__ import annotations
import numpy as np
def normalize_mask_labels(arr: np.ndarray, num_lanes: int = 4) -> np.ndarray:
"""Map MUFLD ids 0,2,3,4,5 -> 0,1,2,3,4 for seg training."""
if arr.max() <= num_lanes:
return arr.astype(np.uint8)
out = np.zeros_like(arr, dtype=np.uint8)
for lane_idx in range(1, num_lanes + 1):
out[arr == (lane_idx + 1)] = lane_idx
return out
def lane_pixel_id(lane_idx: int, mask_max: int) -> int:
if mask_max == 2:
return lane_idx
return lane_idx + 1
def lanes_from_mask(
mask: np.ndarray,
sample_ys: list | range,
num_lanes: int = 4,
) -> list[list[tuple[float, float]]]:
"""Return list of lanes; each lane is [(x,y), ...] sorted by y descending."""
if mask.ndim > 2:
mask = mask[:, :, 0]
mask = normalize_mask_labels(mask.astype(np.uint8), num_lanes)
mx = int(mask.max())
lanes = []
for lane_idx in range(1, num_lanes + 1):
vid = lane_pixel_id(lane_idx, mx)
pts = []
for y in sample_ys:
yi = int(round(y))
if yi < 0 or yi >= mask.shape[0]:
continue
xs = np.where(mask[yi] == vid)[0]
if len(xs) == 0:
continue
pts.append((float(np.mean(xs)), float(yi)))
if len(pts) >= 2:
pts = sorted(pts, key=lambda p: -p[1])
lanes.append(pts)
return lanes
def lanes_to_lines_txt(lanes: list[list[tuple[float, float]]]) -> str:
lines = []
for lane in lanes:
parts = []
for x, y in lane:
parts.append(f"{x:.5f} {y:.5f}")
if parts:
lines.append(" ".join(parts))
return "\n".join(lines) + ("\n" if lines else "")

View File

@@ -0,0 +1,58 @@
import torch
import os
from torch import nn
import numpy as np
import torch.nn.functional
def save_model(net, optim, scheduler, recorder, is_best=False):
model_dir = os.path.join(recorder.work_dir, 'ckpt')
os.system('mkdir -p {}'.format(model_dir))
epoch = recorder.epoch
ckpt_name = 'best' if is_best else epoch
torch.save(
{
'net': net.state_dict(),
'optim': optim.state_dict(),
'scheduler': scheduler.state_dict(),
'recorder': recorder.state_dict(),
'epoch': epoch
}, os.path.join(model_dir, '{}.pth'.format(ckpt_name)))
def load_network_specified(net, model_dir, logger=None):
pretrained_net = torch.load(model_dir)['net']
net_state = net.state_dict()
state = {}
for k, v in pretrained_net.items():
if k not in net_state.keys() or v.size() != net_state[k].size():
if logger:
logger.info('skip weights: ' + k)
continue
state[k] = v
net.load_state_dict(state, strict=False)
def load_network(net, model_dir, finetune_from=None, logger=None):
if finetune_from:
if logger:
logger.info('Finetune model from: ' + finetune_from)
load_network_specified(net, finetune_from, logger)
return
pretrained_model = torch.load(model_dir)
net.load_state_dict(pretrained_model['net'], strict=False)
def resume_network(model_dir, net, optim, scheduler, recorder):
if not os.path.exists(model_dir):
print('WARNING: NO MODEL LOADED !!!', 'red')
return 0
print('resume model: {}'.format(model_dir))
pretrained_model = torch.load(model_dir)
net.load_state_dict(pretrained_model['net'])
optim.load_state_dict(pretrained_model['optim'])
scheduler.load_state_dict(pretrained_model['scheduler'])
recorder.load_state_dict(pretrained_model['recorder'])
return pretrained_model['epoch'] + 1

View File

@@ -0,0 +1,135 @@
from collections import deque, defaultdict
import torch
import os
import datetime
from .logger import init_logger
import logging
import pathspec
class SmoothedValue(object):
"""Track a series of values and provide access to smoothed values over a
window or the global series average.
"""
def __init__(self, window_size=20):
self.deque = deque(maxlen=window_size)
self.total = 0.0
self.count = 0
def update(self, value):
self.deque.append(value)
self.count += 1
self.total += value
@property
def median(self):
d = torch.tensor(list(self.deque))
return d.median().item()
@property
def avg(self):
d = torch.tensor(list(self.deque))
return d.mean().item()
@property
def global_avg(self):
return self.total / self.count
class Recorder(object):
def __init__(self, cfg):
self.cfg = cfg
self.work_dir = self.get_work_dir()
cfg.work_dir = self.work_dir
self.log_path = os.path.join(self.work_dir, 'log.txt')
init_logger(self.log_path)
self.logger = logging.getLogger(__name__)
self.logger.info('Config: \n' + cfg.text)
self.save_cfg(cfg)
self.cp_projects(self.work_dir)
# scalars
self.epoch = 0
self.step = 0
self.loss_stats = defaultdict(SmoothedValue)
self.batch_time = SmoothedValue()
self.data_time = SmoothedValue()
self.max_iter = self.cfg.total_iter
self.lr = 0.
def save_cfg(self, cfg):
cfg_path = os.path.join(self.work_dir, 'config.py')
with open(cfg_path, 'w') as cfg_file:
cfg_file.write(cfg.text)
def cp_projects(self, to_path):
with open('./.gitignore', 'r') as fp:
ign = fp.read()
ign += '\n.git'
spec = pathspec.PathSpec.from_lines(
pathspec.patterns.GitWildMatchPattern, ign.splitlines())
all_files = {
os.path.join(root, name)
for root, dirs, files in os.walk('./') for name in files
}
matches = spec.match_files(all_files)
matches = set(matches)
to_cp_files = all_files - matches
for f in to_cp_files:
dirs = os.path.join(to_path, 'code', os.path.split(f[2:])[0])
if not os.path.exists(dirs):
os.makedirs(dirs)
os.system('cp %s %s' % (f, os.path.join(to_path, 'code', f[2:])))
def get_work_dir(self):
now = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
hyper_param_str = '_lr_%1.0e_b_%d' % (self.cfg.optimizer.lr,
self.cfg.batch_size)
work_dir = os.path.join(self.cfg.work_dirs, now + hyper_param_str)
if not os.path.exists(work_dir):
os.makedirs(work_dir)
return work_dir
def update_loss_stats(self, loss_dict):
for k, v in loss_dict.items():
if not isinstance(v, torch.Tensor): continue
self.loss_stats[k].update(v.detach().mean().cpu())
def record(self, prefix, step=-1, loss_stats=None, image_stats=None):
self.logger.info(self)
# self.write(str(self))
def write(self, content):
with open(self.log_path, 'a+') as f:
f.write(content)
f.write('\n')
def state_dict(self):
scalar_dict = {}
scalar_dict['step'] = self.step
return scalar_dict
def load_state_dict(self, scalar_dict):
self.step = scalar_dict['step']
def __str__(self):
loss_state = []
for k, v in self.loss_stats.items():
loss_state.append('{}: {:.4f}'.format(k, v.avg))
loss_state = ' '.join(loss_state)
recording_state = ' '.join([
'epoch: {}', 'step: {}', 'lr: {:.6f}', '{}', 'data: {:.4f}',
'batch: {:.4f}', 'eta: {}'
])
eta_seconds = self.batch_time.global_avg * (self.max_iter - self.step)
eta_string = str(datetime.timedelta(seconds=int(eta_seconds)))
return recording_state.format(self.epoch, self.step, self.lr,
loss_state, self.data_time.avg,
self.batch_time.avg, eta_string)
def build_recorder(cfg):
return Recorder(cfg)

View File

@@ -0,0 +1,82 @@
import inspect
import six
# borrow from mmdetection
def is_str(x):
"""Whether the input is an string instance."""
return isinstance(x, six.string_types)
class Registry(object):
def __init__(self, name):
self._name = name
self._module_dict = dict()
def __repr__(self):
format_str = self.__class__.__name__ + '(name={}, items={})'.format(
self._name, list(self._module_dict.keys()))
return format_str
@property
def name(self):
return self._name
@property
def module_dict(self):
return self._module_dict
def get(self, key):
return self._module_dict.get(key, None)
def _register_module(self, module_class):
"""Register a module.
Args:
module (:obj:`nn.Module`): Module to be registered.
"""
if not inspect.isclass(module_class):
raise TypeError('module must be a class, but got {}'.format(
type(module_class)))
module_name = module_class.__name__
if module_name in self._module_dict:
raise KeyError('{} is already registered in {}'.format(
module_name, self.name))
self._module_dict[module_name] = module_class
def register_module(self, cls):
self._register_module(cls)
return cls
def build_from_cfg(cfg, registry, default_args=None):
"""Build a module from config dict.
Args:
cfg (dict): Config dict. It should at least contain the key "type".
registry (:obj:`Registry`): The registry to search the type from.
default_args (dict, optional): Default initialization arguments.
Returns:
obj: The constructed object.
"""
assert isinstance(cfg, dict) and 'type' in cfg
assert isinstance(default_args, dict) or default_args is None
args = cfg.copy()
obj_type = args.pop('type')
if is_str(obj_type):
obj_cls = registry.get(obj_type)
if obj_cls is None:
raise KeyError('{} is not in the {} registry'.format(
obj_type, registry.name))
elif inspect.isclass(obj_type):
obj_cls = obj_type
else:
raise TypeError('type must be a str or valid type, but got {}'.format(
type(obj_type)))
if default_args is not None:
for name, value in default_args.items():
args.setdefault(name, value)
return obj_cls(**args)

View File

@@ -0,0 +1,136 @@
import numpy as np
from sklearn.linear_model import LinearRegression
import json as json
class LaneEval(object):
lr = LinearRegression()
pixel_thresh = 20
pt_thresh = 0.85
@staticmethod
def get_angle(xs, y_samples):
xs, ys = xs[xs >= 0], y_samples[xs >= 0]
if len(xs) > 1:
LaneEval.lr.fit(ys[:, None], xs)
k = LaneEval.lr.coef_[0]
theta = np.arctan(k)
else:
theta = 0
return theta
@staticmethod
def line_accuracy(pred, gt, thresh):
pred = np.array([p if p >= 0 else -100 for p in pred])
gt = np.array([g if g >= 0 else -100 for g in gt])
return np.sum(np.where(np.abs(pred - gt) < thresh, 1., 0.)) / len(gt)
@staticmethod
def bench(pred, gt, y_samples, running_time):
if any(len(p) != len(y_samples) for p in pred):
raise Exception('Format of lanes error.')
if running_time > 200 or len(gt) + 2 < len(pred):
return 0., 0., 1.
angles = [
LaneEval.get_angle(np.array(x_gts), np.array(y_samples))
for x_gts in gt
]
threshs = [LaneEval.pixel_thresh / np.cos(angle) for angle in angles]
line_accs = []
fp, fn = 0., 0.
matched = 0.
for x_gts, thresh in zip(gt, threshs):
accs = [
LaneEval.line_accuracy(np.array(x_preds), np.array(x_gts),
thresh) for x_preds in pred
]
max_acc = np.max(accs) if len(accs) > 0 else 0.
if max_acc < LaneEval.pt_thresh:
fn += 1
else:
matched += 1
line_accs.append(max_acc)
fp = len(pred) - matched
if len(gt) > 4 and fn > 0:
fn -= 1
s = sum(line_accs)
if len(gt) > 4:
s -= min(line_accs)
return s / max(min(4.0, len(gt)),
1.), fp / len(pred) if len(pred) > 0 else 0., fn / max(
min(len(gt), 4.), 1.)
@staticmethod
def bench_one_submit(pred_file, gt_file):
try:
json_pred = [
json.loads(line) for line in open(pred_file).readlines()
]
except BaseException as e:
raise Exception('Fail to load json file of the prediction.')
json_gt = [json.loads(line) for line in open(gt_file).readlines()]
if len(json_gt) != len(json_pred):
raise Exception(
'We do not get the predictions of all the test tasks')
gts = {l['raw_file']: l for l in json_gt}
accuracy, fp, fn = 0., 0., 0.
for pred in json_pred:
if 'raw_file' not in pred or 'lanes' not in pred or 'run_time' not in pred:
raise Exception(
'raw_file or lanes or run_time not in some predictions.')
raw_file = pred['raw_file']
pred_lanes = pred['lanes']
run_time = pred['run_time']
if raw_file not in gts:
raise Exception(
'Some raw_file from your predictions do not exist in the test tasks.'
)
gt = gts[raw_file]
gt_lanes = gt['lanes']
y_samples = gt['h_samples']
try:
a, p, n = LaneEval.bench(pred_lanes, gt_lanes, y_samples,
run_time)
except BaseException as e:
raise Exception('Format of lanes error.')
accuracy += a
fp += p
fn += n
num = len(gts)
# the first return parameter is the default ranking parameter
fp = fp / num
fn = fn / num
tp = 1 - fp
precision = tp / (tp + fp)
recall = tp / (tp + fn)
f1 = 2 * precision * recall / (precision + recall)
return json.dumps([{
'name': 'Accuracy',
'value': accuracy / num,
'order': 'desc'
}, {
'name': 'F1_score',
'value': f1,
'order': 'desc'
}, {
'name': 'FP',
'value': fp,
'order': 'asc'
}, {
'name': 'FN',
'value': fn,
'order': 'asc'
}]), accuracy / num
if __name__ == '__main__':
import sys
try:
if len(sys.argv) != 3:
raise Exception('Invalid input arguments')
print(LaneEval.bench_one_submit(sys.argv[1], sys.argv[2]))
except Exception as e:
print(e.message)
sys.exit(e.message)

View File

@@ -0,0 +1,64 @@
import cv2
import os
import os.path as osp
COLORS = [
(255, 0, 0),
(0, 255, 0),
(0, 0, 255),
(255, 255, 0),
(255, 0, 255),
(0, 255, 255),
(128, 255, 0),
(255, 128, 0),
(128, 0, 255),
(255, 0, 128),
(0, 128, 255),
(0, 255, 128),
(128, 255, 255),
(255, 128, 255),
(255, 255, 128),
(60, 180, 0),
(180, 60, 0),
(0, 60, 180),
(0, 180, 60),
(60, 0, 180),
(180, 0, 60),
(255, 0, 0),
(0, 255, 0),
(0, 0, 255),
(255, 255, 0),
(255, 0, 255),
(0, 255, 255),
(128, 255, 0),
(255, 128, 0),
(128, 0, 255),
]
def imshow_lanes(img, lanes, show=False, out_file=None, width=4):
lanes_xys = []
for _, lane in enumerate(lanes):
xys = []
for x, y in lane:
if x <= 0 or y <= 0:
continue
x, y = int(x), int(y)
xys.append((x, y))
lanes_xys.append(xys)
lanes_xys.sort(key=lambda xys : xys[0][0])
for idx, xys in enumerate(lanes_xys):
for i in range(1, len(xys)):
cv2.line(img, xys[i - 1], xys[i], COLORS[idx], thickness=width)
if show:
cv2.imshow('view', img)
cv2.waitKey(0)
if out_file:
if not osp.exists(osp.dirname(out_file)):
os.makedirs(osp.dirname(out_file))
cv2.imwrite(out_file, img)

View File

@@ -0,0 +1,124 @@
net = dict(type='Detector', )
backbone = dict(
type='DLAWrapper',
dla='dla34',
pretrained=True,
)
num_points = 72
max_lanes = 4
sample_y = range(589, 230, -20)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 2.
xyt_loss_weight = 0.2
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/dla34_culane"
neck = dict(type='FPN',
in_channels=[128, 256, 512],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.4, nms_thres=50, nms_topk=max_lanes)
epochs = 15
batch_size = 24
optimizer = dict(type='AdamW', lr=0.6e-3) # 3e-4 for batchsize 8
total_iter = (88880 // batch_size) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 3
save_ep = 10
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1640
ori_img_h = 590
img_w = 800
img_h = 320
cut_height = 270
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='ChannelShuffle', parameters=dict(p=1.0), p=0.1),
dict(name='MultiplyAndAddToBrightness',
parameters=dict(mul=(0.85, 1.15), add=(-10, 10)),
p=0.6),
dict(name='AddToHueAndSaturation',
parameters=dict(value=(-10, 10)),
p=0.7),
dict(name='OneOf',
transforms=[
dict(name='MotionBlur', parameters=dict(k=(3, 5))),
dict(name='MedianBlur', parameters=dict(k=(3, 5)))
],
p=0.2),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = './data/CULane'
dataset_type = 'CULane'
dataset = dict(train=dict(
type=dataset_type,
data_root=dataset_path,
split='train',
processes=train_process,
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
))
workers = 10
log_interval = 500
# seed = 0
num_classes = 4 + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,124 @@
net = dict(type='Detector', )
backbone = dict(
type='DLAWrapper',
dla='dla34',
pretrained=True,
)
num_points = 72
max_lanes = 4
sample_y = range(717, 300, -20)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 4.
xyt_loss_weight = 0.5
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/dla34_llamas"
neck = dict(type='FPN',
in_channels=[128, 256, 512],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.45, nms_thres=50, nms_topk=max_lanes)
epochs = 20
batch_size = 24
optimizer = dict(type='AdamW', lr=0.6e-3) # 3e-4 for batchsize 8
total_iter = (58272 // batch_size) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 3
save_ep = 5
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1276
ori_img_h = 717
img_w = 800
img_h = 320
cut_height = 300
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='ChannelShuffle', parameters=dict(p=1.0), p=0.1),
# dict(name='MultiplyAndAddToBrightness',
# parameters=dict(mul=(0.85, 1.15), add=(-10, 10)),
# p=0.6),
# dict(name='AddToHueAndSaturation',
# parameters=dict(value=(-10, 10)),
# p=0.7),
# dict(name='OneOf',
# transforms=[
# dict(name='MotionBlur', parameters=dict(k=(3, 5))),
# dict(name='MedianBlur', parameters=dict(k=(3, 5)))
# ],
# p=0.2),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = './data/llamas'
dataset_type = 'LLAMAS'
dataset = dict(train=dict(
type=dataset_type,
data_root=dataset_path,
split='train',
processes=train_process,
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='val',
processes=val_process,
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
))
workers = 10
log_interval = 500
# seed = 0
num_classes = 4 + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,126 @@
net = dict(type='Detector', )
backbone = dict(
type='ResNetWrapper',
resnet='resnet101',
pretrained=True,
replace_stride_with_dilation=[False, False, False],
out_conv=False,
)
num_points = 72
max_lanes = 4
sample_y = range(589, 230, -20)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 2.
xyt_loss_weight = 0.2
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/r101_culane"
neck = dict(type='FPN',
in_channels=[512, 1024, 2048],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.4, nms_thres=50, nms_topk=max_lanes)
epochs = 20
batch_size = 12
optimizer = dict(type='AdamW', lr=0.3e-3) # 3e-4 for batchsize 8
total_iter = (88880 // batch_size) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 3
save_ep = 10
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1640
ori_img_h = 590
img_w = 800
img_h = 320
cut_height = 270
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='ChannelShuffle', parameters=dict(p=1.0), p=0.1),
dict(name='MultiplyAndAddToBrightness',
parameters=dict(mul=(0.85, 1.15), add=(-10, 10)),
p=0.6),
dict(name='AddToHueAndSaturation',
parameters=dict(value=(-10, 10)),
p=0.7),
dict(name='OneOf',
transforms=[
dict(name='MotionBlur', parameters=dict(k=(3, 5))),
dict(name='MedianBlur', parameters=dict(k=(3, 5)))
],
p=0.2),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = './data/CULane'
dataset_type = 'CULane'
dataset = dict(train=dict(
type=dataset_type,
data_root=dataset_path,
split='train',
processes=train_process,
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
))
workers = 10
log_interval = 500
# seed = 0
num_classes = 4 + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,127 @@
net = dict(type='Detector', )
backbone = dict(
type='ResNetWrapper',
resnet='resnet101',
pretrained=True,
replace_stride_with_dilation=[False, False, False],
out_conv=False,
)
num_points = 72
max_lanes = 5
sample_y = range(710, 150, -10)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 6.
xyt_loss_weight = 0.5
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/r101_tusimple"
neck = dict(type='FPN',
in_channels=[512, 1024, 2048],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.40, nms_thres=50, nms_topk=max_lanes)
epochs = 70
batch_size = 10
optimizer = dict(type='AdamW', lr=0.3e-3) # 3e-4 for batchsize 8
total_iter = (3616 // batch_size + 1) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 1
save_ep = epochs
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1280
ori_img_h = 720
img_h = 320
img_w = 800
cut_height = 160
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='ChannelShuffle', parameters=dict(p=1.0), p=0.1),
dict(name='MultiplyAndAddToBrightness',
parameters=dict(mul=(0.85, 1.15), add=(-10, 10)),
p=0.6),
dict(name='AddToHueAndSaturation',
parameters=dict(value=(-10, 10)),
p=0.7),
dict(name='OneOf',
transforms=[
dict(name='MotionBlur', parameters=dict(k=(3, 5))),
dict(name='MedianBlur', parameters=dict(k=(3, 5)))
],
p=0.2),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = './data/tusimple'
dataset_type = 'TuSimple'
test_json_file = 'data/tusimple/test_label.json'
dataset = dict(train=dict(
type=dataset_type,
data_root=dataset_path,
split='trainval',
processes=train_process,
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
))
workers = 10
log_interval = 100
# seed = 0
num_classes = 6 + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,126 @@
net = dict(type='Detector', )
backbone = dict(
type='ResNetWrapper',
resnet='resnet18',
pretrained=True,
replace_stride_with_dilation=[False, False, False],
out_conv=False,
)
num_points = 72
max_lanes = 4
sample_y = range(589, 230, -20)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 2.
xyt_loss_weight = 0.2
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/r18_culane"
neck = dict(type='FPN',
in_channels=[128, 256, 512],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.4, nms_thres=50, nms_topk=max_lanes)
epochs = 15
batch_size = 24
optimizer = dict(type='AdamW', lr=0.6e-3) # 3e-4 for batchsize 8
total_iter = (88880 // batch_size) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 1
save_ep = 10
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1640
ori_img_h = 590
img_w = 800
img_h = 320
cut_height = 270
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='ChannelShuffle', parameters=dict(p=1.0), p=0.1),
dict(name='MultiplyAndAddToBrightness',
parameters=dict(mul=(0.85, 1.15), add=(-10, 10)),
p=0.6),
dict(name='AddToHueAndSaturation',
parameters=dict(value=(-10, 10)),
p=0.7),
dict(name='OneOf',
transforms=[
dict(name='MotionBlur', parameters=dict(k=(3, 5))),
dict(name='MedianBlur', parameters=dict(k=(3, 5)))
],
p=0.2),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = './data/CULane'
dataset_type = 'CULane'
dataset = dict(train=dict(
type=dataset_type,
data_root=dataset_path,
split='train',
processes=train_process,
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
))
workers = 10
log_interval = 1000
# seed = 0
num_classes = 4 + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,126 @@
net = dict(type='Detector', )
backbone = dict(
type='ResNetWrapper',
resnet='resnet18',
pretrained=True,
replace_stride_with_dilation=[False, False, False],
out_conv=False,
)
num_points = 72
max_lanes = 4
sample_y = range(717, 300, -20)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 8.
cls_loss_weight = 2.
xyt_loss_weight = 0.5
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/resnet18_llamas"
neck = dict(type='FPN',
in_channels=[128, 256, 512],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.45, nms_thres=60, nms_topk=max_lanes)
epochs = 20
batch_size = 24
optimizer = dict(type='AdamW', lr=0.6e-3) # 3e-4 for batchsize 8
total_iter = (58272 // batch_size) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 3
save_ep = 5
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1276
ori_img_h = 717
img_w = 800
img_h = 320
cut_height = 300
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='ChannelShuffle', parameters=dict(p=1.0), p=0.1),
dict(name='MultiplyAndAddToBrightness',
parameters=dict(mul=(0.85, 1.15), add=(-10, 10)),
p=0.6),
dict(name='AddToHueAndSaturation',
parameters=dict(value=(-10, 10)),
p=0.7),
dict(name='OneOf',
transforms=[
dict(name='MotionBlur', parameters=dict(k=(3, 5))),
dict(name='MedianBlur', parameters=dict(k=(3, 5)))
],
p=0.2),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = './data/llamas'
dataset_type = 'LLAMAS'
dataset = dict(train=dict(
type=dataset_type,
data_root=dataset_path,
split='train',
processes=train_process,
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='val',
processes=val_process,
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
))
workers = 10
log_interval = 100
# seed = 0
num_classes = 4 + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,127 @@
# CLRNet on lane0_copy MUFLD packs (1280x720)
# data_root = parent of DATASET / DATASET-AddBy-*
net = dict(type='Detector', )
backbone = dict(
type='ResNetWrapper',
resnet='resnet18',
pretrained=True,
replace_stride_with_dilation=[False, False, False],
out_conv=False,
)
num_points = 72
max_lanes = 4
sample_y = range(710, 150, -10)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 6.
xyt_loss_weight = 0.5
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/mufld_r18"
neck = dict(type='FPN',
in_channels=[128, 256, 512],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.40, nms_thres=50, nms_topk=max_lanes)
epochs = 15
batch_size = 16
optimizer = dict(type='AdamW', lr=1.0e-3)
# ~144k / 16 * 15 — adjust after changing train_packs
total_iter = (144117 // batch_size + 1) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 3
save_ep = 5
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1280
ori_img_h = 720
img_w = 800
img_h = 320
cut_height = 160
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
# --- MUFLD multi-pack (same as UFLD) ---
dataset_path = '/home/chengfanglu/DATA/lane0_copy'
train_packs = ['DATASET']
# train_packs = ['DATASET', 'DATASET-A'] # alias in datasets_registry.json
val_packs = ['DATASET']
pack_list_name = 'list/train_gt.txt'
pack_val_list_name = 'list/val_gt.txt'
merged_list_dir = 'lists_merged'
remerge_lists = False
write_lines_cache = True
lines_cache_dir = 'cache/mufld_lines'
dataset_type = 'MufldLane'
dataset = dict(
train=dict(
type=dataset_type,
data_root=dataset_path,
split='train',
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='val',
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
list_file='DATASET/list/test_gt.txt',
),
)
workers = 8
log_interval = 500
num_classes = max_lanes + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,60 @@
# Smoke: copy fields from clr_resnet18_mufld.py and override below
# (CLRNet Config does not use _base_ inheritance)
net = dict(type='Detector', )
backbone = dict(type='ResNetWrapper', resnet='resnet18', pretrained=True,
replace_stride_with_dilation=[False, False, False], out_conv=False)
num_points = 72
max_lanes = 4
sample_y = range(710, 150, -10)
heads = dict(type='CLRHead', num_priors=192, refine_layers=3, fc_hidden_dim=64, sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 6.
xyt_loss_weight = 0.5
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/mufld_r18_smoke"
neck = dict(type='FPN', in_channels=[128, 256, 512], out_channels=64, num_outs=3, attention=False)
test_parameters = dict(conf_threshold=0.40, nms_thres=50, nms_topk=max_lanes)
epochs = 1
batch_size = 4
optimizer = dict(type='AdamW', lr=1.0e-3)
total_iter = 32
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 1
save_ep = 1
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1280
ori_img_h = 720
img_w = 800
img_h = 320
cut_height = 160
train_process = [
dict(type='GenerateLaneLine',
transforms=[dict(name='Resize', parameters=dict(size=dict(height=img_h, width=img_w)), p=1.0)],
training=True),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[dict(name='Resize', parameters=dict(size=dict(height=img_h, width=img_w)), p=1.0)],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = '/home/chengfanglu/DATA/lane0_copy'
train_list_file = 'DATASET/list/train_gt_smoke.txt'
val_list_file = 'DATASET/list/train_gt_smoke.txt'
write_lines_cache = True
lines_cache_dir = 'cache/mufld_lines'
dataset_type = 'MufldLane'
dataset = dict(
train=dict(type=dataset_type, data_root=dataset_path, split='train'),
val=dict(type=dataset_type, data_root=dataset_path, split='val'),
test=dict(type=dataset_type, data_root=dataset_path, split='test',
list_file='DATASET/list/train_gt_smoke.txt'),
)
workers = 2
log_interval = 8
num_classes = max_lanes + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,127 @@
net = dict(type='Detector', )
backbone = dict(
type='ResNetWrapper',
resnet='resnet18',
pretrained=True,
replace_stride_with_dilation=[False, False, False],
out_conv=False,
)
num_points = 72
max_lanes = 5
sample_y = range(710, 150, -10)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 6.
xyt_loss_weight = 0.5
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/r18_tusimple"
neck = dict(type='FPN',
in_channels=[128, 256, 512],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.40, nms_thres=50, nms_topk=max_lanes)
epochs = 70
batch_size = 40
optimizer = dict(type='AdamW', lr=1.0e-3) # 3e-4 for batchsize 8
total_iter = (3616 // batch_size + 1) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 3
save_ep = epochs
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1280
ori_img_h = 720
img_h = 320
img_w = 800
cut_height = 160
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='ChannelShuffle', parameters=dict(p=1.0), p=0.1),
dict(name='MultiplyAndAddToBrightness',
parameters=dict(mul=(0.85, 1.15), add=(-10, 10)),
p=0.6),
dict(name='AddToHueAndSaturation',
parameters=dict(value=(-10, 10)),
p=0.7),
dict(name='OneOf',
transforms=[
dict(name='MotionBlur', parameters=dict(k=(3, 5))),
dict(name='MedianBlur', parameters=dict(k=(3, 5)))
],
p=0.2),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = './data/tusimple'
dataset_type = 'TuSimple'
test_json_file = 'data/tusimple/test_label.json'
dataset = dict(train=dict(
type=dataset_type,
data_root=dataset_path,
split='trainval',
processes=train_process,
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
))
workers = 10
log_interval = 100
# seed = 0
num_classes = 6 + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,126 @@
net = dict(type='Detector', )
backbone = dict(
type='ResNetWrapper',
resnet='resnet34',
pretrained=True,
replace_stride_with_dilation=[False, False, False],
out_conv=False,
)
num_points = 72
max_lanes = 4
sample_y = range(589, 230, -20)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 2.
xyt_loss_weight = 0.2
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/r34_culane"
neck = dict(type='FPN',
in_channels=[128, 256, 512],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.4, nms_thres=50, nms_topk=max_lanes)
epochs = 15
batch_size = 24
optimizer = dict(type='AdamW', lr=0.6e-3) # 3e-4 for batchsize 8
total_iter = (88880 // batch_size) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 3
save_ep = 10
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1640
ori_img_h = 590
img_w = 800
img_h = 320
cut_height = 270
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='ChannelShuffle', parameters=dict(p=1.0), p=0.1),
dict(name='MultiplyAndAddToBrightness',
parameters=dict(mul=(0.85, 1.15), add=(-10, 10)),
p=0.6),
dict(name='AddToHueAndSaturation',
parameters=dict(value=(-10, 10)),
p=0.7),
dict(name='OneOf',
transforms=[
dict(name='MotionBlur', parameters=dict(k=(3, 5))),
dict(name='MedianBlur', parameters=dict(k=(3, 5)))
],
p=0.2),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = './data/CULane'
dataset_type = 'CULane'
dataset = dict(train=dict(
type=dataset_type,
data_root=dataset_path,
split='train',
processes=train_process,
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
))
workers = 10
log_interval = 500
# seed = 0
num_classes = 4 + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,127 @@
net = dict(type='Detector', )
backbone = dict(
type='ResNetWrapper',
resnet='resnet34',
pretrained=True,
replace_stride_with_dilation=[False, False, False],
out_conv=False,
)
num_points = 72
max_lanes = 5
sample_y = range(710, 150, -10)
heads = dict(type='CLRHead',
num_priors=192,
refine_layers=3,
fc_hidden_dim=64,
sample_points=36)
iou_loss_weight = 2.
cls_loss_weight = 6.
xyt_loss_weight = 0.5
seg_loss_weight = 1.0
work_dirs = "work_dirs/clr/r34_tusimple"
neck = dict(type='FPN',
in_channels=[128, 256, 512],
out_channels=64,
num_outs=3,
attention=False)
test_parameters = dict(conf_threshold=0.40, nms_thres=50, nms_topk=max_lanes)
epochs = 70
batch_size = 32
optimizer = dict(type='AdamW', lr=0.8e-3) # 3e-4 for batchsize 8
total_iter = (3616 // batch_size + 1) * epochs
scheduler = dict(type='CosineAnnealingLR', T_max=total_iter)
eval_ep = 3
save_ep = epochs
img_norm = dict(mean=[103.939, 116.779, 123.68], std=[1., 1., 1.])
ori_img_w = 1280
ori_img_h = 720
img_h = 320
img_w = 800
cut_height = 160
train_process = [
dict(
type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
dict(name='HorizontalFlip', parameters=dict(p=1.0), p=0.5),
dict(name='ChannelShuffle', parameters=dict(p=1.0), p=0.1),
dict(name='MultiplyAndAddToBrightness',
parameters=dict(mul=(0.85, 1.15), add=(-10, 10)),
p=0.6),
dict(name='AddToHueAndSaturation',
parameters=dict(value=(-10, 10)),
p=0.7),
dict(name='OneOf',
transforms=[
dict(name='MotionBlur', parameters=dict(k=(3, 5))),
dict(name='MedianBlur', parameters=dict(k=(3, 5)))
],
p=0.2),
dict(name='Affine',
parameters=dict(translate_percent=dict(x=(-0.1, 0.1),
y=(-0.1, 0.1)),
rotate=(-10, 10),
scale=(0.8, 1.2)),
p=0.7),
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
),
dict(type='ToTensor', keys=['img', 'lane_line', 'seg']),
]
val_process = [
dict(type='GenerateLaneLine',
transforms=[
dict(name='Resize',
parameters=dict(size=dict(height=img_h, width=img_w)),
p=1.0),
],
training=False),
dict(type='ToTensor', keys=['img']),
]
dataset_path = './data/tusimple'
dataset_type = 'TuSimple'
test_json_file = 'data/tusimple/test_label.json'
dataset = dict(train=dict(
type=dataset_type,
data_root=dataset_path,
split='trainval',
processes=train_process,
),
val=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
),
test=dict(
type=dataset_type,
data_root=dataset_path,
split='test',
processes=val_process,
))
workers = 10
log_interval = 100
# seed = 0
num_classes = 6 + 1
ignore_label = 255
bg_weight = 0.4
lr_update_by_epoch = False

View File

@@ -0,0 +1,75 @@
import os
import cv2
import torch
import torch.nn.parallel
import torch.backends.cudnn as cudnn
import argparse
import numpy as np
import random
from clrnet.utils.config import Config
from clrnet.engine.runner import Runner
from clrnet.datasets import build_dataloader
def main():
args = parse_args()
os.environ["CUDA_VISIBLE_DEVICES"] = ','.join(
str(gpu) for gpu in args.gpus)
cfg = Config.fromfile(args.config)
cfg.gpus = len(args.gpus)
cfg.load_from = args.load_from
cfg.resume_from = args.resume_from
cfg.finetune_from = args.finetune_from
cfg.view = args.view
cfg.seed = args.seed
cfg.work_dirs = args.work_dirs if args.work_dirs else cfg.work_dirs
cudnn.benchmark = True
runner = Runner(cfg)
if args.validate:
runner.validate()
elif args.test:
runner.test()
else:
runner.train()
def parse_args():
parser = argparse.ArgumentParser(description='Train a detector')
parser.add_argument('config', help='train config file path')
parser.add_argument('--work_dirs',
type=str,
default=None,
help='work dirs')
parser.add_argument('--load_from',
default=None,
help='the checkpoint file to load from')
parser.add_argument('--resume_from',
default=None,
help='the checkpoint file to resume from')
parser.add_argument('--finetune_from',
default=None,
help='the checkpoint file to resume from')
parser.add_argument('--view', action='store_true', help='whether to view')
parser.add_argument(
'--validate',
action='store_true',
help='whether to evaluate the checkpoint during training')
parser.add_argument(
'--test',
action='store_true',
help='whether to test the checkpoint on testing set')
parser.add_argument('--gpus', nargs='+', type=int, default='0')
parser.add_argument('--seed', type=int, default=0, help='random seed')
args = parser.parse_args()
return args
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,2 @@
/home/chengfanglu/DATA/BK2/archive/yolo26_rknn_ultralytics-main/ultralytics/assets/bus.jpg
/home/chengfanglu/DATA/BK2/archive/yolo26_rknn_ultralytics-main/ultralytics/assets/zidane.jpg

View File

@@ -0,0 +1,20 @@
torch==1.8.0
torchvision==0.9.0
pandas
addict
sklearn
opencv-python
pytorch_warmup
scikit-image
tqdm
p_tqdm
imgaug>=0.4.0
Shapely==1.7.0
ujson==1.35
yapf
pathspec
timm
mmcv==1.2.5
albumentations==0.4.6
pathspec
ptflops

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Create conda env for CLRNet + MUFLD dataset
set -euo pipefail
ENV_NAME="${CLRNET_ENV_NAME:-clrnet_lane}"
CLRNET_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
source "$(conda info --base)/etc/profile.d/conda.sh"
if conda env list | awk '{print $1}' | grep -qx "$ENV_NAME"; then
echo "Env $ENV_NAME exists, activate: conda activate $ENV_NAME"
else
conda create -n "$ENV_NAME" python=3.8 -y
fi
conda activate "$ENV_NAME"
# PyTorch (adjust CUDA version to match your driver)
pip install torch==1.10.2+cu113 torchvision==0.11.3+cu113 \
-f https://download.pytorch.org/whl/cu113/torch_stable.html
pip install mmcv-full==1.4.0 -f https://download.openmmlab.com/mmcv/dist/cu113/torch1.10/index.html
pip install -r "$CLRNET_ROOT/requirements.txt"
cd "$CLRNET_ROOT"
python setup.py develop
echo ""
echo "Done. Usage:"
echo " conda activate $ENV_NAME"
echo " cd $CLRNET_ROOT"
echo " python main.py configs/clrnet/clr_resnet18_mufld_smoke.py --gpus 0"

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# CPU-friendly env for CLRNet ONNX export (no CUDA required for export).
set -euo pipefail
ENV_NAME="${CLRNET_EXPORT_ENV:-clrnet_export}"
CLRNET_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
source "$(conda info --base)/etc/profile.d/conda.sh"
if ! conda env list | awk '{print $1}' | grep -qx "$ENV_NAME"; then
conda create -n "$ENV_NAME" python=3.8 -y
fi
conda activate "$ENV_NAME"
pip install --upgrade pip
pip install torch==1.10.2+cpu torchvision==0.11.3+cpu \
-f https://download.pytorch.org/whl/cpu/torch_stable.html
pip install mmcv-full==1.4.0 -f https://download.openmmlab.com/mmcv/dist/cpu/torch1.10/index.html
pip install onnx onnxruntime onnxsim
pip install -r "$CLRNET_ROOT/requirements.txt"
cd "$CLRNET_ROOT"
pip install -e .
echo ""
echo "Export ONNX:"
echo " conda activate $ENV_NAME"
echo " cd $CLRNET_ROOT"
echo " python tools/export_onnx.py --check"

View File

@@ -0,0 +1,116 @@
import glob
import os
import re
from setuptools import find_packages, setup
from torch.utils.cpp_extension import CUDAExtension, BuildExtension
def parse_requirements(fname='requirements.txt', with_version=True):
"""Parse the package dependencies listed in a requirements file but strips
specific versioning information.
Args:
fname (str): path to requirements file
with_version (bool, default=False): if True include version specs
Returns:
List[str]: list of requirements items
CommandLine:
python -c "import setup; print(setup.parse_requirements())"
"""
import sys
from os.path import exists
require_fpath = fname
def parse_line(line):
"""Parse information from a line in a requirements text file."""
if line.startswith('-r '):
# Allow specifying requirements in other files
target = line.split(' ')[1]
for info in parse_require_file(target):
yield info
else:
info = {'line': line}
if line.startswith('-e '):
info['package'] = line.split('#egg=')[1]
else:
# Remove versioning from the package
pat = '(' + '|'.join(['>=', '==', '>']) + ')'
parts = re.split(pat, line, maxsplit=1)
parts = [p.strip() for p in parts]
info['package'] = parts[0]
if len(parts) > 1:
op, rest = parts[1:]
if ';' in rest:
# Handle platform specific dependencies
# http://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies
version, platform_deps = map(str.strip,
rest.split(';'))
info['platform_deps'] = platform_deps
else:
version = rest # NOQA
info['version'] = (op, version)
yield info
def parse_require_file(fpath):
with open(fpath, 'r') as f:
for line in f.readlines():
line = line.strip()
if line and not line.startswith('#'):
for info in parse_line(line):
yield info
def gen_packages_items():
if exists(require_fpath):
for info in parse_require_file(require_fpath):
parts = [info['package']]
if with_version and 'version' in info:
parts.extend(info['version'])
if not sys.version.startswith('3.4'):
# apparently package_deps are broken in 3.4
platform_deps = info.get('platform_deps')
if platform_deps is not None:
parts.append(';' + platform_deps)
item = ''.join(parts)
yield item
packages = list(gen_packages_items())
return packages
install_requires = parse_requirements()
def get_extensions():
extensions = []
op_files = glob.glob('./clrnet/ops/csrc/*.c*')
extension = CUDAExtension
ext_name = 'clrnet.ops.nms_impl'
ext_ops = extension(
name=ext_name,
sources=op_files,
)
extensions.append(ext_ops)
return extensions
setup(name='clrnet',
version="1.0",
keywords='computer vision & lane detection',
classifiers=[
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
'Intended Audience :: Developers',
'Operating System :: OS Independent'
],
packages=find_packages(),
include_package_data=True,
setup_requires=['pytest-runner'],
tests_require=['pytest'],
install_requires=install_requires,
ext_modules=get_extensions(),
cmdclass={'build_ext': BuildExtension},
zip_safe=False)

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""Export CLRNet to ONNX (bilinear_grid_sample, no GridSample)."""
import argparse
import os
import sys
import types
import numpy as np
import torch
import torch.nn as nn
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, ROOT)
def _stub_nms_cuda_ext():
"""Export does not need CUDA NMS; stub so clrnet.ops imports on CPU-only hosts."""
if 'clrnet.ops.nms_impl' in sys.modules:
return
stub = types.ModuleType('clrnet.ops.nms_impl')
def _nms_forward(boxes, scores, overlap, top_k):
raise NotImplementedError('NMS runs on CPU after RKNN inference, not in exported graph')
stub.nms_forward = _nms_forward
sys.modules['clrnet.ops.nms_impl'] = stub
_stub_nms_cuda_ext()
from clrnet.utils.config import Config # noqa: E402
from clrnet.models.registry import build_net # noqa: E402
class CLRNetOnnxWrapper(nn.Module):
def __init__(self, net):
super().__init__()
self.net = net
def forward(self, img):
# Detector returns (B, num_priors, 77) in eval mode
return self.net({'img': img})
def _remap_clrernet_keys(key):
"""Map CLRerNet (mmdet) checkpoint keys to Turoad CLRNet-main module names."""
key = key.replace('bbox_head.', 'heads.')
key = key.replace('heads.sample_x_indices', 'heads.sample_x_indexs')
key = key.replace('heads.anchor_generator.prior_embeddings',
'heads.prior_embeddings')
key = key.replace('heads.attention.attention.', 'heads.roi_gather.')
key = key.replace('heads.attention.', 'heads.roi_gather.')
return key
def load_weights(net, path):
ckpt = torch.load(path, map_location='cpu')
if isinstance(ckpt, dict):
if 'net' in ckpt:
state = ckpt['net']
elif 'state_dict' in ckpt:
state = ckpt['state_dict']
else:
state = ckpt
else:
state = ckpt
remapped = {_remap_clrernet_keys(k): v for k, v in state.items()}
missing, unexpected = net.load_state_dict(remapped, strict=False)
print('load_state_dict: missing', len(missing), 'unexpected', len(unexpected))
if missing:
print(' missing sample:', missing[:8])
if unexpected:
print(' unexpected sample:', unexpected[:8])
def compare_outputs(wrapper, dummy, onnx_path):
try:
import onnxruntime as ort
except ImportError:
print('onnxruntime not installed, skip numeric check')
return
wrapper.eval()
with torch.no_grad():
pt_out = wrapper(dummy).numpy()
sess = ort.InferenceSession(onnx_path, providers=['CPUExecutionProvider'])
ort_out = sess.run(None, {'img': dummy.numpy()})[0]
diff = np.abs(pt_out - ort_out).max()
print(f'PyTorch vs ONNX max diff: {diff:.6f}')
print(f'output shape: {pt_out.shape}')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--config', default='configs/clrnet/clr_dla34_culane.py')
parser.add_argument('--checkpoint', default='models/clrernet_culane_dla34.pth')
parser.add_argument('--output', default='models/clrernet_culane_dla34.onnx')
parser.add_argument('--opset', type=int, default=11)
parser.add_argument('--height', type=int, default=None)
parser.add_argument('--width', type=int, default=None)
parser.add_argument('--check', action='store_true', help='run onnxruntime compare')
args = parser.parse_args()
cfg = Config.fromfile(os.path.join(ROOT, args.config))
h = args.height or cfg.img_h
w = args.width or cfg.img_w
# Avoid downloading ImageNet weights; we load the CLRNet checkpoint below.
if cfg.get('backbone', None) is not None:
cfg.backbone.pretrained = False
net = build_net(cfg)
load_weights(net, os.path.join(ROOT, args.checkpoint))
net.eval()
wrapper = CLRNetOnnxWrapper(net)
dummy = torch.randn(1, 3, h, w)
out_path = os.path.join(ROOT, args.output)
os.makedirs(os.path.dirname(out_path) or '.', exist_ok=True)
print(f'export ONNX: {out_path}')
print(f'input: (1, 3, {h}, {w})')
with torch.no_grad():
test_out = wrapper(dummy)
print(f'output: {tuple(test_out.shape)}')
torch.onnx.export(
wrapper,
dummy,
out_path,
opset_version=args.opset,
input_names=['img'],
output_names=['predictions'],
dynamic_axes={'img': {0: 'batch'}, 'predictions': {0: 'batch'}},
do_constant_folding=True,
)
print('saved:', out_path)
try:
import onnx
model = onnx.load(out_path)
ops = {n.op_type for n in model.graph.node}
print('GridSample in graph:', 'GridSample' in ops)
print('node op types (sample):', sorted(ops)[:20], '...')
except ImportError:
pass
if args.check:
compare_outputs(wrapper, dummy, out_path)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Pre-generate .lines.txt from masks for faster CLRNet training."""
import argparse
import os
import os.path as osp
import sys
import cv2
from tqdm import tqdm
ROOT = osp.dirname(osp.dirname(osp.abspath(__file__)))
sys.path.insert(0, ROOT)
from clrnet.utils.mask_to_lanes import lanes_from_mask, lanes_to_lines_txt
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--data-root", required=True)
ap.add_argument("--list", required=True, help="train_gt.txt path")
ap.add_argument("--sample-y", type=int, nargs="+", default=list(range(710, 150, -10)))
ap.add_argument("--num-lanes", type=int, default=4)
ap.add_argument("--cache-dir", default="cache/mufld_lines")
args = ap.parse_args()
list_path = args.list if osp.isabs(args.list) else osp.join(args.data_root, args.list)
n_ok = 0
with open(list_path) as f:
lines = [ln.strip() for ln in f if ln.strip()]
for line in tqdm(lines):
parts = line.split()
if len(parts) < 2:
continue
img_rel, _ = parts[0].lstrip("/"), parts[1].lstrip("/")
img_path = osp.join(args.data_root, img_rel)
base = img_path[:-4]
out = osp.join(args.data_root, args.cache_dir, osp.relpath(base, args.data_root) + ".lines.txt")
mask_path = osp.join(args.data_root, parts[1].lstrip("/"))
mask = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED)
if mask is None:
continue
if mask.ndim > 2:
mask = mask[:, :, 0]
lanes = lanes_from_mask(mask, args.sample_y, args.num_lanes)
os.makedirs(osp.dirname(out), exist_ok=True)
with open(out, "w") as fp:
fp.write(lanes_to_lines_txt(lanes))
n_ok += 1
print("wrote", n_ok, "lines files under", args.cache_dir)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,126 @@
import json
import numpy as np
import cv2
import os
import argparse
TRAIN_SET = ['label_data_0313.json', 'label_data_0601.json']
VAL_SET = ['label_data_0531.json']
TRAIN_VAL_SET = TRAIN_SET + VAL_SET
TEST_SET = ['test_label.json']
def gen_label_for_json(args, image_set):
H, W = 720, 1280
SEG_WIDTH = 30
save_dir = args.savedir
os.makedirs(os.path.join(args.root, args.savedir, "list"), exist_ok=True)
list_f = open(
os.path.join(args.root, args.savedir, "list",
"{}_gt.txt".format(image_set)), "w")
json_path = os.path.join(args.root, args.savedir,
"{}.json".format(image_set))
with open(json_path) as f:
for line in f:
label = json.loads(line)
# ---------- clean and sort lanes -------------
lanes = []
_lanes = []
slope = [
] # identify 0th, 1st, 2nd, 3rd, 4th, 5th lane through slope
for i in range(len(label['lanes'])):
l = [(x, y)
for x, y in zip(label['lanes'][i], label['h_samples'])
if x >= 0]
if (len(l) > 1):
_lanes.append(l)
slope.append(
np.arctan2(l[-1][1] - l[0][1], l[0][0] - l[-1][0]) /
np.pi * 180)
_lanes = [_lanes[i] for i in np.argsort(slope)]
slope = [slope[i] for i in np.argsort(slope)]
idx = [None for i in range(6)]
for i in range(len(slope)):
if slope[i] <= 90:
idx[2] = i
idx[1] = i - 1 if i > 0 else None
idx[0] = i - 2 if i > 1 else None
else:
idx[3] = i
idx[4] = i + 1 if i + 1 < len(slope) else None
idx[5] = i + 2 if i + 2 < len(slope) else None
break
for i in range(6):
lanes.append([] if idx[i] is None else _lanes[idx[i]])
# ---------------------------------------------
img_path = label['raw_file']
seg_img = np.zeros((H, W, 3))
list_str = [] # str to be written to list.txt
for i in range(len(lanes)):
coords = lanes[i]
if len(coords) < 4:
list_str.append('0')
continue
for j in range(len(coords) - 1):
cv2.line(seg_img, coords[j], coords[j + 1],
(i + 1, i + 1, i + 1), SEG_WIDTH // 2)
list_str.append('1')
seg_path = img_path.split("/")
seg_path, img_name = os.path.join(args.root, args.savedir,
seg_path[1],
seg_path[2]), seg_path[3]
os.makedirs(seg_path, exist_ok=True)
seg_path = os.path.join(seg_path, img_name[:-3] + "png")
cv2.imwrite(seg_path, seg_img)
seg_path = "/".join([
args.savedir, *img_path.split("/")[1:3], img_name[:-3] + "png"
])
if seg_path[0] != '/':
seg_path = '/' + seg_path
if img_path[0] != '/':
img_path = '/' + img_path
list_str.insert(0, seg_path)
list_str.insert(0, img_path)
list_str = " ".join(list_str) + "\n"
list_f.write(list_str)
def generate_json_file(save_dir, json_file, image_set):
with open(os.path.join(save_dir, json_file), "w") as outfile:
for json_name in (image_set):
with open(os.path.join(args.root, json_name)) as infile:
for line in infile:
outfile.write(line)
def generate_label(args):
save_dir = os.path.join(args.root, args.savedir)
os.makedirs(save_dir, exist_ok=True)
generate_json_file(save_dir, "train_val.json", TRAIN_VAL_SET)
generate_json_file(save_dir, "test.json", TEST_SET)
print("generating train_val set...")
gen_label_for_json(args, 'train_val')
print("generating test set...")
gen_label_for_json(args, 'test')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--root',
required=True,
help='The root of the Tusimple dataset')
parser.add_argument('--savedir',
type=str,
default='seg_label',
help='The root of the Tusimple dataset')
args = parser.parse_args()
generate_label(args)

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Write a calibration image list for RKNN (one absolute path per line)."""
import argparse
import glob
import os
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--glob', default='**/*.jpg', help='glob under --root')
parser.add_argument('--root', default='.', help='search root')
parser.add_argument('--out', default='models/calib_images.txt')
parser.add_argument('--max', type=int, default=20)
args = parser.parse_args()
root = os.path.abspath(args.root)
paths = []
for p in sorted(glob.glob(os.path.join(root, args.glob), recursive=True)):
if os.path.isfile(p):
paths.append(os.path.abspath(p))
if len(paths) >= args.max:
break
if not paths:
raise SystemExit(f'no images under {root} with {args.glob}')
out = os.path.abspath(args.out)
os.makedirs(os.path.dirname(out) or '.', exist_ok=True)
with open(out, 'w') as f:
f.write('\n'.join(paths) + '\n')
print(f'wrote {len(paths)} paths -> {out}')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""ONNX -> RKNN for CLRNet (platform rk3576). Requires rknn-toolkit2 on x86 Linux."""
import argparse
import os
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--onnx', default='models/clrernet_culane_dla34.onnx')
parser.add_argument('--rknn', default='models/clrernet_culane_dla34_rk3576.rknn')
parser.add_argument('--platform', default='rk3576')
parser.add_argument('--dtype', default='i8', choices=['i8', 'fp'])
parser.add_argument('--dataset', default=None,
help='txt list of calibration images (one path per line)')
parser.add_argument('--height', type=int, default=320)
parser.add_argument('--width', type=int, default=800)
args = parser.parse_args()
try:
from rknn.api import RKNN
except ImportError as e:
raise SystemExit(
'Install rknn-toolkit2 in a separate env: pip install rknn-toolkit2\n' + str(e)
) from e
if not args.dataset:
raise SystemExit(
'Provide --dataset with a txt of RGB image paths for INT8 calibration.'
)
rknn = RKNN(verbose=True)
print('config:', args.platform, args.dtype)
rknn.config(mean_values=[[103.939, 116.779, 123.68]],
std_values=[[1, 1, 1]],
target_platform=args.platform,
quantized_dtype='asymmetric_quantized-8' if args.dtype == 'i8' else 'float16')
ret = rknn.load_onnx(model=args.onnx)
if ret != 0:
raise SystemExit('load_onnx failed')
ret = rknn.build(do_quantization=(args.dtype == 'i8'), dataset=args.dataset)
if ret != 0:
raise SystemExit('build failed')
os.makedirs(os.path.dirname(args.rknn) or '.', exist_ok=True)
ret = rknn.export_rknn(args.rknn)
if ret != 0:
raise SystemExit('export_rknn failed')
print('saved:', os.path.abspath(args.rknn))
rknn.release()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,11 @@
# LaneDection 代码
**ML 入口**`ML/Lane/code`(软链到本目录)
| 子目录 | 用途 |
|--------|------|
| `UFLD/` | 车道线分割训练(`ml.py train lane` 默认) |
| `CLRNet-main/` | CLRNet |
| `pytorch-auto-drive-master/` | 其他车道线模型 |
数据在 `ML/Lane/dataset`= `/DATA/lane`)。

View File

@@ -0,0 +1,138 @@
UFLD 使用说明lane0_copy 数据)
代码目录:/home/chengfanglu/DATA/BK2/UFLD
数据目录:/home/chengfanglu/DATA/lane0_copy/
一、环境
source ~/miniconda3/etc/profile.d/conda.sh
conda activate lane_light
cd /home/chengfanglu/DATA/BK2/UFLD
二、数据目录
lane0_copy/
DATASET/(基线包,勿改 list/train_gt.txt
images/
annotations/segmentation_masks/
list/train_gt.txt训练图 + mask 两列)
list/val_gt.txt
list/test_gt.txt
list/test.txt仅图片
DATASET-AddBy-zhangsan-20260615/(增量包,结构同上)
lists_merged/(多包合并列表,训练时自动生成)
datasets_registry.json短名别名
增量包命名DATASET-AddBy-姓名-YYYYMMDD日期 8 位,如 20260615
目录规范详见:/home/chengfanglu/DATA/lane0_copy/DATASETS_LAYOUT.md
新建增量包命令:
python /home/chengfanglu/DATA/lane0_copy/scripts/build_ufld_pack.py --src /path/to/archive --parent /home/chengfanglu/DATA/lane0_copy --engineer zhangsan --date 20260615
别名config 里可写 DATASET-A编辑 lane0_copy/datasets_registry.json例如
{"aliases": {"DATASET-A": "DATASET-AddBy-zhangsan-20260615"}}
三、配置文件
configs/mufld_lane_multi_pack.py — 推荐,多包训练,用 train_packs 控制合并
configs/mufld_lane_culane.py — 单包data_root 指向 DATASET 目录本身
configs/mufld_lane_smoke.py — 冒烟(少量样本)
configs/tusimple_res18_4lane_v1.py — 对接旧权重 best.pthgriding_num=100
多包训练请改 configs/mufld_lane_multi_pack.py
data_root = '/home/chengfanglu/DATA/lane0_copy'
train_packs = ['DATASET']
多包示例train_packs = ['DATASET', 'DATASET-A']
pack_list_name = 'list/train_gt.txt'
remerge_train_list = False增删包后改为 True强制重建 lists_merged
训练时自动合并列表到lane0_copy/lists_merged/train__DATASET__....txt
四、训练
conda activate lane_light
cd /home/chengfanglu/DATA/BK2/UFLD
冒烟:
UFLD_NUM_WORKERS=0 python train.py configs/mufld_lane_smoke.py
正式(多包):
python train.py configs/mufld_lane_multi_pack.py
断点续训:
python train.py configs/mufld_lane_multi_pack.py --resume log/你的实验目录/best.pth
日志与权重在log/时间_lr_.../best.pth
无 GPU 时可设 UFLD_NUM_WORKERS=0
常用 config 项:
batch_size = 16
learning_rate = 0.1
use_aux = TrueFalse 与旧 best.pth 一致,更省显存)
griding_num = 200旧权重用 100
num_lanes = 4
五、推理与测试
【5.1 可视化 demo】
先准备 test3.txt示例取 3 张):
awk '{print $1}' /home/chengfanglu/DATA/lane0_copy/DATASET/list/test_gt.txt | head -3 > /home/chengfanglu/DATA/lane0_copy/DATASET/test3.txt
python demo.py configs/tusimple_res18_4lane_v1.py --test_model log/20250702_165153_lr_1e-05_b_32_ufld_2lanes_res18/best.pth --data_root /home/chengfanglu/DATA/lane0_copy/DATASET
【5.2 批量测试】
python test.py configs/tusimple_res18_4lane_v1.py --test_model log/20250702_165153_lr_1e-05_b_32_ufld_2lanes_res18/best.pth --data_root /home/chengfanglu/DATA/lane0_copy/DATASET --test_list list/test_gt.txt
多包时 data_root 用 lane0_copy例如
python test.py configs/mufld_lane_multi_pack.py --test_model log/xxx/best.pth --data_root /home/chengfanglu/DATA/lane0_copy --test_list lists_merged/train__DATASET.txt
无 test_label.json 时只出预测,不算 TuSimple 官方指标。
【5.3 预测画到图上】
python vis_tusimple_pred.py --pred tmp/tusimple_eval_tmp.0.txt --data_root /home/chengfanglu/DATA/lane0_copy/DATASET --out_dir tmp/vis_pred
六、导出 ONNX
python pth_to_onnx.py --model_path log/20250702_165153_lr_1e-05_b_32_ufld_2lanes_res18/best.pth --output log/20250702_165153_lr_1e-05_b_32_ufld_2lanes_res18/best.onnx
需与训练时 backbone、griding_num、num_lanes 一致。
【6.1 VoVNet backbone】
已从 `BK2/archive/vovnet-detectron2-master` 移植 OSA+eSE 结构(无 detectron2 依赖),与 ResNet 相同接口。
| config `backbone` | 说明 |
|-------------------|------|
| `vov19slim` | V-19-slim-eSE约 52.7M 参数288×800 |
| `vov19slim_dw` | slim + depthwise |
| `vov19` / `vov39` / `vov57` / `vov99` | 更大变体 |
示例配置:`configs/tusimple_vov19slim_4lane_v1.py`
```bash
python train.py configs/tusimple_vov19slim_4lane_v1.py
python profile_model.py --backbone vov19slim --griding_num 100 --num_lanes 4
```
VoVNet **无** torchvision 预训练权重,需从头训或自行转换 detectron2 权重。旧 ResNet 的 `best.pth` **不能**直接用于 VoVNet。
七、路径速查
代码:/home/chengfanglu/DATA/BK2/UFLD
数据父目录:/home/chengfanglu/DATA/lane0_copy
基线数据:/home/chengfanglu/DATA/lane0_copy/DATASET
多包配置configs/mufld_lane_multi_pack.py
已有权重log/20250702_165153_lr_1e-05_b_32_ufld_2lanes_res18/best.pth

View File

@@ -0,0 +1,46 @@
## lane_light + CPU PyTorchUFLD 训练)
### 1. 激活环境
```bash
source /home/chengfanglu/miniconda3/etc/profile.d/conda.sh
conda activate lane_light
```
### 2. 已安装依赖(`lane_light`
- `torch` / `torchvision`**CPU 轮子**,来自 `https://download.pytorch.org/whl/cpu`
- `opencv-python`, `tqdm`, `tensorboard`, `addict`, `scikit-learn`, `pathspec`(与 `requirements.txt` 对齐;`sklearn` 包名在 pip 中为 `scikit-learn`
自检:
```bash
python -c "import torch; print('torch', torch.__version__, 'cuda=', torch.cuda.is_available())"
```
### 3. 数据与配置
- 默认数据根仍指向 `lane0_reorganized/lane_training_pack`(见 `configs/mufld_lane_culane.py`)。
- **CPU 建议**使用 `configs/mufld_lane_culane_cpu.py``batch_size=4`,学习率与 warmup 已按 batch 相对 16 做了粗略缩放)。内存不够可改配置或命令行覆盖:
```bash
cd /home/chengfanglu/DATA/BK2/UFLD
python train.py configs/mufld_lane_culane_cpu.py --batch_size 2
```
### 4. 运行训练
```bash
cd /home/chengfanglu/DATA/BK2/UFLD
python train.py configs/mufld_lane_culane_cpu.py
```
说明:
- `train.py` 已改为在 **无 CUDA** 时使用 `cpu`;原仓库中写死的 `CUDA_VISIBLE_DEVICES=1,2``.cuda()` 已去掉,避免 CPU 机直接报错。
- 首次 `pretrained=True` 会下载 ResNet 骨干权重,需联网。
- CPU 训练很慢,建议先用小 `epoch` / 小 `batch_size` 做通路测试。
### 5. 可选DataLoader `num_workers`
当前 `data/dataloader.py``num_workers=8`。若 CPU 内存紧张或不想多进程读盘,可自行把该值改小(例如 `0``2`)。

View File

@@ -0,0 +1,45 @@
# DATA
dataset = 'CULane'
data_root = 'C:\\data\\Tusimple\\test_set'
# TRAIN
epoch = 50
batch_size = 32
optimizer = 'SGD' #['SGD','Adam']
learning_rate = 0.1
weight_decay = 1e-4
momentum = 0.9
scheduler = 'multi' #['multi', 'cos']
steps = [25,38]
gamma = 0.1
warmup = 'linear'
warmup_iters = 695
# NETWORK
use_aux = True
griding_num = 200
backbone = '18'
# LOSS
sim_loss_w = 0.0
shp_loss_w = 0.0
# EXP
note = ''
log_path = None
# FINETUNE or RESUME MODEL PATH
finetune = None
resume = None
# TEST
test_model = './model/culane_18.pth'
test_work_dir = './tmp'
num_lanes = 4

View File

@@ -0,0 +1,41 @@
# MUFLD lane pack in CULane-style layout for UFLD training.
# Data root layout:
# <data_root>/images/...
# <data_root>/annotations/segmentation_masks/...
# <data_root>/list/train_gt.txt (two columns: training split only)
# <data_root>/list/val_gt.txt (validation pairs, optional custom loop)
# <data_root>/list/test.txt (held-out test images, one per line)
dataset = 'CULane'
data_root = '/home/chengfanglu/DATA/lane0_copy/DATASET'
epoch = 50
batch_size = 16
optimizer = 'SGD'
learning_rate = 0.1
weight_decay = 1e-4
momentum = 0.9
scheduler = 'multi'
steps = [25, 38]
gamma = 0.1
warmup = 'linear'
warmup_iters = 695
use_aux = True
griding_num = 200
backbone = '18'
sim_loss_w = 0.0
shp_loss_w = 0.0
note = 'lane_training_pack_v1'
log_path = './log'
finetune = None
resume = None
test_model = './model/culane_18.pth'
test_work_dir = './tmp'
num_lanes = 4

View File

@@ -0,0 +1,40 @@
# CPU 单机训练示例batch 需显著减小;学习率可按 batch 相对 16 做线性缩放(可选)。
#
# layout 同 configs/mufld_lane_culane.py
# lane_light 环境与安装说明见 TRAIN_ENV_CPU.md
dataset = "CULane"
data_root = "/home/chengfanglu/DATA/lane0_copy/DATASET"
epoch = 50
batch_size = 4
optimizer = "SGD"
# 若在 CPU 上不收敛可先试更小 lr例如 batch=4 时约 0.1 * (4 / 16) = 0.025
learning_rate = 0.025
weight_decay = 1e-4
momentum = 0.9
scheduler = "multi"
steps = [25, 38]
gamma = 0.1
warmup = "linear"
# warmup 与原配置按 batch 比例对齐(原为 695 @ bs=16
warmup_iters = 174
use_aux = True
griding_num = 200
backbone = "18"
sim_loss_w = 0.0
shp_loss_w = 0.0
note = "lane_training_pack_cpu_bs4"
log_path = None
finetune = None
resume = None
test_model = "./model/culane_18.pth"
test_work_dir = "./tmp"
num_lanes = 4

View File

@@ -0,0 +1,54 @@
# Multi-pack training — control merged packs in this config.
# data_root = parent of DATASET / DATASET-AddBy-* / DATASET-A (alias).
from pathlib import Path
dataset = 'CULane'
data_root = str(Path(__file__).resolve().parents[5] / "datasets" / "lane")
# Pack names: directory under data_root, or alias from datasets_registry.json
train_packs = [
'lane_v1',
]
# Default list inside each pack (relative to pack root)
pack_list_name = 'list/train_gt.txt'
# Cached merged list (auto filename from pack names if merged_train_list is None)
merged_list_dir = 'lists_merged'
merged_train_list = None # e.g. 'lists_merged/train_all_v2.txt'
remerge_train_list = False # True to rebuild merged list every run
# Single-pack fallback (ignored when train_packs is set)
train_list = 'list/train_gt.txt'
epoch = 50
batch_size = 16
optimizer = 'SGD'
learning_rate = 0.1
weight_decay = 1e-4
momentum = 0.9
scheduler = 'multi'
steps = [25, 38]
gamma = 0.1
warmup = 'linear'
warmup_iters = 695
use_aux = True
griding_num = 200
backbone = '18'
sim_loss_w = 0.0
shp_loss_w = 0.0
note = 'multi_pack_v2'
log_path = './log'
finetune = None
resume = None
test_model = './model/culane_18.pth'
test_work_dir = './tmp'
num_lanes = 4

View File

@@ -0,0 +1,35 @@
# Smoke test: few samples, 1 epoch, small batch (CPU or GPU).
dataset = "CULane"
data_root = "/home/chengfanglu/DATA/lane0_copy/DATASET"
train_list = "list/train_gt_smoke.txt"
epoch = 1
batch_size = 2
optimizer = "SGD"
learning_rate = 0.025
weight_decay = 1e-4
momentum = 0.9
scheduler = "multi"
steps = [1]
gamma = 0.1
warmup = "linear"
warmup_iters = 10
use_aux = True
griding_num = 200
backbone = "18"
sim_loss_w = 0.0
shp_loss_w = 0.0
note = "dataset_smoke_test"
log_path = "./log"
finetune = None
resume = None
test_model = "./model/culane_18.pth"
test_work_dir = "./tmp"
num_lanes = 4

View File

@@ -0,0 +1,41 @@
# DATA
dataset = 'Tusimple'
data_root = '/mnt/HDisk2T/liuxy51/ganxian/data_luojk'
# TRAIN
epoch = 500 # 10
batch_size = 32 # 4
optimizer = 'Adam' #['SGD','Adam']
learning_rate = 1e-5
weight_decay = 1e-4
momentum = 0.9
scheduler = 'cos' #['multi', 'cos']
# steps = [50,75]
gamma = 0.1
warmup = 'linear'
warmup_iters = 100
# NETWORK
backbone = '18'
griding_num = 100
use_aux = False
# LOSS
sim_loss_w = 1.0
shp_loss_w = 0.0
# EXP
note = '_ufld_2lanes_res18'
log_path = './log'
# FINETUNE or RESUME MODEL PATH
finetune = None
resume = None
# TESTNone
test_model = './model/lane_m599_all.pth'
test_work_dir = './tmp'
num_lanes = 2

View File

@@ -0,0 +1,41 @@
# DATA
dataset = 'Tusimple'
data_root = '/mnt/HDisk2T/liuxy51/ganxian/train_2025_03_13_mufld' # 针对clrnet数据集8.1w帧多车道数据
# TRAIN
epoch = 500 # 10
batch_size = 32 # 4
optimizer = 'Adam' #['SGD','Adam']
learning_rate = 1e-5
weight_decay = 1e-4
momentum = 0.9
scheduler = 'cos' #['multi', 'cos']
# steps = [50,75]
gamma = 0.1
warmup = 'linear'
warmup_iters = 100
# NETWORK
backbone = '18'
griding_num = 100
use_aux = False
# LOSS
sim_loss_w = 1.0
shp_loss_w = 0.0
# EXP
note = '_ufld_2lanes_res18'
log_path = './log'
# FINETUNE or RESUME MODEL PATH
finetune = None
resume = None
# TESTNone
test_model = './model/lane_m599_all.pth'
test_work_dir = './tmp'
num_lanes = 4

View File

@@ -0,0 +1,41 @@
# DATA
dataset = 'Tusimple'
data_root = '/mnt/HDisk2T/liuxy51/ganxian/train_2024_03_06'
# TRAIN
epoch = 500 # 10
batch_size = 32 # 4
optimizer = 'Adam' #['SGD','Adam']
learning_rate = 1e-5
weight_decay = 1e-4
momentum = 0.9
scheduler = 'cos' #['multi', 'cos']
# steps = [50,75]
gamma = 0.1
warmup = 'linear'
warmup_iters = 100
# NETWORK
backbone = '18'
griding_num = 100
use_aux = False
# LOSS
sim_loss_w = 1.0
shp_loss_w = 0.0
# EXP
note = '_ufld_2lanes_res18'
log_path = './log'
# FINETUNE or RESUME MODEL PATH
finetune = None
resume = '/mnt/HDisk2T/liuxy51/ganxian/UFLD/log/20240607_162111_lr_1e-05_b_32_ufld_2lanes_res18/ep304.pth' # None
# TESTNone
test_model = './model/lane_m599_all.pth'
test_work_dir = './tmp'
num_lanes = 2

View File

@@ -0,0 +1,41 @@
# DATA
dataset = 'Tusimple'
data_root = '/mnt/HDisk2T/liuxy51/ganxian/train_2024_03_06_1'
# TRAIN
epoch = 500 # 10
batch_size = 32 # 4
optimizer = 'Adam' #['SGD','Adam']
learning_rate = 1e-5
weight_decay = 1e-4
momentum = 0.9
scheduler = 'cos' #['multi', 'cos']
# steps = [50,75]
gamma = 0.1
warmup = 'linear'
warmup_iters = 100
# NETWORK
backbone = '18'
griding_num = 100
use_aux = False
# LOSS
sim_loss_w = 1.0
shp_loss_w = 0.0
# EXP
note = '_ufld_2lanes_res18'
log_path = './log'
# FINETUNE or RESUME MODEL PATH
finetune = None
resume = None
# TESTNone
test_model = './model/lane_m599_all.pth'
test_work_dir = './tmp'
num_lanes = 2

View File

@@ -0,0 +1,41 @@
# DATA
dataset = 'Tusimple'
data_root = '/mnt/HDisk2T/liuxy51/ganxian/train_2024_03_06_2'
# TRAIN
epoch = 500 # 10
batch_size = 32 # 4
optimizer = 'Adam' #['SGD','Adam']
learning_rate = 1e-5
weight_decay = 1e-4
momentum = 0.9
scheduler = 'cos' #['multi', 'cos']
# steps = [50,75]
gamma = 0.1
warmup = 'linear'
warmup_iters = 100
# NETWORK
backbone = '18'
griding_num = 100
use_aux = False
# LOSS
sim_loss_w = 1.0
shp_loss_w = 0.0
# EXP
note = '_ufld_2lanes_res18'
log_path = './log'
# FINETUNE or RESUME MODEL PATH
finetune = None
resume = None
# TESTNone
test_model = './model/lane_m599_all.pth'
test_work_dir = './tmp'
num_lanes = 2

View File

@@ -0,0 +1,44 @@
# DATA
dataset = 'Tusimple'
# data_root = 'C:\\data\\Tusimple\\test_set'
# data_root = 'C:\\data\\anno\\324lane'
# data_root = 'C:\\data\\Tusimple\\train_set'
data_root = '/data/panh28/yk_syj/data/train_0306'
# TRAIN
epoch = 600
batch_size = 64
optimizer = 'Adam' #['SGD','Adam']
# learning_rate = 0.1
learning_rate = 1e-5
weight_decay = 1e-4
momentum = 0.9
scheduler = 'cos' #['multi', 'cos']
# steps = [50,75]
gamma = 0.1
warmup = 'linear'
warmup_iters = 100
# NETWORK
backbone = '34'
griding_num = 100
use_aux = False
# LOSS
sim_loss_w = 1.0
shp_loss_w = 0.0
# EXP
note = 'lane_res34_2ch_syj_0906_minilearn'
log_path = './log'
# FINETUNE or RESUME MODEL PATH
finetune = None
resume = "/data/panh28/yk_syj/code/UFLD/log/20230906_161808_lr_1e-04_b_64lane_res34_2ch_syj_0906/ep068.pth"
# TESTNone
test_model = './model/lane_m599_all.pth'
# test_model = './model/tusimple_18.pth'
test_work_dir = './tmp'
num_lanes = 2

View File

@@ -0,0 +1,6 @@
# UFLD + VoVNet-19-slim-eSE backbone (train from scratch; no torchvision weights).
from configs.tusimple_res18_4lane_v1 import *
backbone = 'vov19slim'
note = '_ufld_4lanes_vov19slim'

View File

@@ -0,0 +1,45 @@
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
# 自定义函数 e指数形式
def func(x, a, b, c):
return a * np.sqrt(x) * (b * np.square(x) + c)
# 定义x、y散点坐标
x = [20, 30, 40, 50, 60, 70]
x = np.array(x)
num = [453, 482, 503, 508, 498, 479]
y = np.array(num)
def get_curve_fit(x, y):
# 非线性最小二乘法拟合
popt, pcov = curve_fit(func, x, y)
# 获取popt里面是拟合系数
# print(popt)
a = popt[0]
b = popt[1]
c = popt[2]
yvals = func(x, a, b, c) # 拟合y值
# print('popt:', popt)
# print('系数a:', a)
# print('系数b:', b)
# print('系数c:', c)
# print('系数pcov:', pcov)
# print('系数yvals:', yvals)
return yvals
yvals = get_curve_fit(x, y)
print(yvals)
# 绘图
plot1 = plt.plot(x, y, 's', label='original values')
plot2 = plt.plot(x, yvals, 'r', label='polyfit values')
plt.xlabel('x')
plt.ylabel('y')
plt.legend(loc=4) # 指定legend的位置右下角
plt.title('curve_fit')
plt.show()

Some files were not shown because too many files have changed in this diff Show More