feat: HSAP platform v2 — modular navigation, quality review, audit log, world model simulation
Major changes: - New frontend (platform/web/): Vite + React 18 + TypeScript + Tailwind - 4-module navigation: 数据送标 / 模型管理 / 车队管理 / 系统管理 - Data catalog with charts (DMS/ADAS/Lane 3-tab view) - Quality review workflow (标注质检): Good/Fine/Bad scoring with auto-advance - Audit enhancements: batch operations, rejection categories, Feishu notifications - Operation audit log (操作日志) - World model simulation studio (仿真工坊) - Dataset version management with snapshots and diff - ADAS 7-class dataset integration (138K images organized + compressed) - User management with Feishu integration and pagination - CRUD/search/filter on all pages, card layout redesign - PIL-optimized image overlay rendering - Auto-snapshot on build, in_review workflow stage - Removed embedded algorithm code (now in workspace)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from .config import Config
|
||||
from .registry import Registry, build_from_cfg
|
||||
@@ -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)
|
||||
418
algorithms/lane_ufld/code.embedded.bak/CLRNet-main/clrnet/utils/config.py
Executable file
418
algorithms/lane_ufld/code.embedded.bak/CLRNet-main/clrnet/utils/config.py
Executable 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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 "")
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user