Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
OpenDAS
mmdetection3d
Commits
9db93054
Commit
9db93054
authored
May 11, 2020
by
wuyuefeng
Committed by
zhangwenwei
May 11, 2020
Browse files
iou piece-wise sampler with unittest
parent
df76bd32
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
317 additions
and
28 deletions
+317
-28
mmdet3d/core/bbox/iou_calculators/iou3d_calculator.py
mmdet3d/core/bbox/iou_calculators/iou3d_calculator.py
+22
-6
mmdet3d/core/bbox/samplers/__init__.py
mmdet3d/core/bbox/samplers/__init__.py
+2
-1
mmdet3d/core/bbox/samplers/iou_neg_piecewise_sampler.py
mmdet3d/core/bbox/samplers/iou_neg_piecewise_sampler.py
+154
-0
mmdet3d/ops/iou3d/__init__.py
mmdet3d/ops/iou3d/__init__.py
+6
-3
mmdet3d/ops/iou3d/iou3d_utils.py
mmdet3d/ops/iou3d/iou3d_utils.py
+93
-18
tests/test_samplers.py
tests/test_samplers.py
+40
-0
No files found.
mmdet3d/core/bbox/iou_calculators/iou3d_calculator.py
View file @
9db93054
import
torch
from
mmdet3d.ops.iou3d
import
boxes_iou3d_gpu
from
mmdet3d.ops.iou3d
import
boxes_iou3d_gpu
_camera
,
boxes_iou3d_gpu_lidar
from
mmdet.core.bbox
import
bbox_overlaps
from
mmdet.core.bbox.iou_calculators.builder
import
IOU_CALCULATORS
from
..
import
box_torch_ops
...
...
@@ -22,10 +22,18 @@ class BboxOverlapsNearest3D(object):
@
IOU_CALCULATORS
.
register_module
()
class
BboxOverlaps3D
(
object
):
"""3D IoU Calculator
"""
"""3D IoU Calculator
def
__call__
(
self
,
bboxes1
,
bboxes2
,
mode
=
'iou'
,
is_aligned
=
False
):
return
bbox_overlaps_3d
(
bboxes1
,
bboxes2
,
mode
,
is_aligned
)
Args:
coordinate (str): 'camera' or 'lidar' coordinate system
"""
def
__init__
(
self
,
coordinate
):
assert
coordinate
in
[
'camera'
,
'lidar'
]
self
.
coordinate
=
coordinate
def
__call__
(
self
,
bboxes1
,
bboxes2
,
mode
=
'iou'
):
return
bbox_overlaps_3d
(
bboxes1
,
bboxes2
,
mode
,
self
.
coordinate
)
def
__repr__
(
self
):
repr_str
=
self
.
__class__
.
__name__
...
...
@@ -62,7 +70,7 @@ def bbox_overlaps_nearest_3d(bboxes1, bboxes2, mode='iou', is_aligned=False):
return
ret
def
bbox_overlaps_3d
(
bboxes1
,
bboxes2
,
mode
=
'iou'
):
def
bbox_overlaps_3d
(
bboxes1
,
bboxes2
,
mode
=
'iou'
,
coordinate
=
'camera'
):
"""Calculate 3D IoU using cuda implementation
Args:
...
...
@@ -70,6 +78,7 @@ def bbox_overlaps_3d(bboxes1, bboxes2, mode='iou'):
bboxes2: Tensor, shape (M, 7) [x, y, z, h, w, l, ry]
mode: mode (str): "iou" (intersection over union) or
iof (intersection over foreground).
coordinate (str): 'camera' or 'lidar' coordinate system
Return:
iou: (M, N) not support aligned mode currently
...
...
@@ -77,4 +86,11 @@ def bbox_overlaps_3d(bboxes1, bboxes2, mode='iou'):
# TODO: check the input dimension meanings,
# this is inconsistent with that in bbox_overlaps_nearest_3d
assert
bboxes1
.
size
(
-
1
)
==
bboxes2
.
size
(
-
1
)
==
7
return
boxes_iou3d_gpu
(
bboxes1
,
bboxes2
,
mode
)
assert
coordinate
in
[
'camera'
,
'lidar'
]
if
coordinate
==
'camera'
:
return
boxes_iou3d_gpu_camera
(
bboxes1
,
bboxes2
,
mode
)
elif
coordinate
==
'lidar'
:
return
boxes_iou3d_gpu_lidar
(
bboxes1
,
bboxes2
,
mode
)
else
:
raise
NotImplementedError
mmdet3d/core/bbox/samplers/__init__.py
View file @
9db93054
...
...
@@ -3,9 +3,10 @@ from mmdet.core.bbox.samplers import (BaseSampler, CombinedSampler,
IoUBalancedNegSampler
,
OHEMSampler
,
PseudoSampler
,
RandomSampler
,
SamplingResult
)
from
.iou_neg_piecewise_sampler
import
IoUNegPiecewiseSampler
__all__
=
[
'BaseSampler'
,
'PseudoSampler'
,
'RandomSampler'
,
'InstanceBalancedPosSampler'
,
'IoUBalancedNegSampler'
,
'CombinedSampler'
,
'OHEMSampler'
,
'SamplingResult'
'OHEMSampler'
,
'SamplingResult'
,
'IoUNegPiecewiseSampler'
]
mmdet3d/core/bbox/samplers/iou_neg_piecewise_sampler.py
0 → 100644
View file @
9db93054
import
torch
from
mmdet.core.bbox.builder
import
BBOX_SAMPLERS
from
.
import
RandomSampler
,
SamplingResult
@
BBOX_SAMPLERS
.
register_module
class
IoUNegPiecewiseSampler
(
RandomSampler
):
"""IoU Piece-wise Sampling
Sampling negtive proposals according to a list of IoU thresholds.
The negtive proposals are divided into several pieces according
to `neg_iou_piece_thrs`. And the ratio of each piece is indicated
by `neg_piece_fractions`.
Args:
num (int): number of proposals.
pos_fraction (float): the fraction of positive proposals.
neg_piece_fractions (list): a list contains fractions that indicates
the ratio of each piece of total negtive samplers.
neg_iou_piece_thrs (list): a list contains IoU thresholds that
indicate the upper bound of this piece.
neg_pos_ub (float): the total ratio to limit the upper bound
number of negtive samples
add_gt_as_proposals (bool): whether to add gt as proposals.
"""
def
__init__
(
self
,
num
,
pos_fraction
=
None
,
neg_piece_fractions
=
None
,
neg_iou_piece_thrs
=
None
,
neg_pos_ub
=-
1
,
add_gt_as_proposals
=
False
,
return_iou
=
False
):
super
(
IoUNegPiecewiseSampler
,
self
).
__init__
(
num
,
pos_fraction
,
neg_pos_ub
,
add_gt_as_proposals
)
assert
isinstance
(
neg_piece_fractions
,
list
)
assert
len
(
neg_piece_fractions
)
==
len
(
neg_iou_piece_thrs
)
self
.
neg_piece_fractions
=
neg_piece_fractions
self
.
neg_iou_thr
=
neg_iou_piece_thrs
self
.
return_iou
=
return_iou
self
.
neg_piece_num
=
len
(
self
.
neg_piece_fractions
)
def
_sample_pos
(
self
,
assign_result
,
num_expected
,
**
kwargs
):
"""Randomly sample some positive samples."""
pos_inds
=
torch
.
nonzero
(
assign_result
.
gt_inds
>
0
,
as_tuple
=
False
)
if
pos_inds
.
numel
()
!=
0
:
pos_inds
=
pos_inds
.
squeeze
(
1
)
if
pos_inds
.
numel
()
<=
num_expected
:
return
pos_inds
else
:
return
self
.
random_choice
(
pos_inds
,
num_expected
)
def
_sample_neg
(
self
,
assign_result
,
num_expected
,
**
kwargs
):
neg_inds
=
torch
.
nonzero
(
assign_result
.
gt_inds
==
0
)
if
neg_inds
.
numel
()
!=
0
:
neg_inds
=
neg_inds
.
squeeze
(
1
)
if
len
(
neg_inds
)
<=
num_expected
:
return
neg_inds
else
:
neg_inds_choice
=
neg_inds
.
new_zeros
([
0
])
extend_num
=
0
max_overlaps
=
assign_result
.
max_overlaps
[
neg_inds
]
for
piece_inds
in
range
(
self
.
neg_piece_num
):
if
piece_inds
==
self
.
neg_piece_num
-
1
:
# for the last piece
piece_expected_num
=
num_expected
-
len
(
neg_inds_choice
)
min_iou_thr
=
0
else
:
# if the numbers of negative samplers in previous
# pieces are less than the expected number, extend
# the same number in the current piece.
piece_expected_num
=
int
(
num_expected
*
self
.
neg_piece_fractions
[
piece_inds
])
+
extend_num
min_iou_thr
=
self
.
neg_iou_thr
[
piece_inds
+
1
]
max_iou_thr
=
self
.
neg_iou_thr
[
piece_inds
]
piece_neg_inds
=
torch
.
nonzero
(
(
max_overlaps
>=
min_iou_thr
)
&
(
max_overlaps
<
max_iou_thr
)).
view
(
-
1
)
if
len
(
piece_neg_inds
)
<
piece_expected_num
:
neg_inds_choice
=
torch
.
cat
(
[
neg_inds_choice
,
neg_inds
[
piece_neg_inds
]],
dim
=
0
)
extend_num
+=
piece_expected_num
-
len
(
piece_neg_inds
)
else
:
piece_choice
=
self
.
random_choice
(
piece_neg_inds
,
piece_expected_num
)
neg_inds_choice
=
torch
.
cat
(
[
neg_inds_choice
,
neg_inds
[
piece_choice
]],
dim
=
0
)
extend_num
=
0
return
neg_inds_choice
def
sample
(
self
,
assign_result
,
bboxes
,
gt_bboxes
,
gt_labels
=
None
,
**
kwargs
):
"""Sample positive and negative bboxes.
This is a simple implementation of bbox sampling given candidates,
assigning results and ground truth bboxes.
Args:
assign_result (:obj:`AssignResult`): Bbox assigning results.
bboxes (Tensor): Boxes to be sampled from.
gt_bboxes (Tensor): Ground truth bboxes.
gt_labels (Tensor, optional): Class labels of ground truth bboxes.
Returns:
:obj:`SamplingResult`: Sampling result.
"""
if
len
(
bboxes
.
shape
)
<
2
:
bboxes
=
bboxes
[
None
,
:]
gt_flags
=
bboxes
.
new_zeros
((
bboxes
.
shape
[
0
],
),
dtype
=
torch
.
bool
)
if
self
.
add_gt_as_proposals
and
len
(
gt_bboxes
)
>
0
:
if
gt_labels
is
None
:
raise
ValueError
(
'gt_labels must be given when add_gt_as_proposals is True'
)
bboxes
=
torch
.
cat
([
gt_bboxes
,
bboxes
],
dim
=
0
)
assign_result
.
add_gt_
(
gt_labels
)
gt_ones
=
bboxes
.
new_ones
(
gt_bboxes
.
shape
[
0
],
dtype
=
torch
.
bool
)
gt_flags
=
torch
.
cat
([
gt_ones
,
gt_flags
])
num_expected_pos
=
int
(
self
.
num
*
self
.
pos_fraction
)
pos_inds
=
self
.
pos_sampler
.
_sample_pos
(
assign_result
,
num_expected_pos
,
bboxes
=
bboxes
,
**
kwargs
)
# We found that sampled indices have duplicated items occasionally.
# (may be a bug of PyTorch)
pos_inds
=
pos_inds
.
unique
()
num_sampled_pos
=
pos_inds
.
numel
()
num_expected_neg
=
self
.
num
-
num_sampled_pos
if
self
.
neg_pos_ub
>=
0
:
_pos
=
max
(
1
,
num_sampled_pos
)
neg_upper_bound
=
int
(
self
.
neg_pos_ub
*
_pos
)
if
num_expected_neg
>
neg_upper_bound
:
num_expected_neg
=
neg_upper_bound
neg_inds
=
self
.
neg_sampler
.
_sample_neg
(
assign_result
,
num_expected_neg
,
bboxes
=
bboxes
,
**
kwargs
)
neg_inds
=
neg_inds
.
unique
()
sampling_result
=
SamplingResult
(
pos_inds
,
neg_inds
,
bboxes
,
gt_bboxes
,
assign_result
,
gt_flags
)
if
self
.
return_iou
:
# PartA2 needs iou score to regression.
sampling_result
.
iou
=
assign_result
.
max_overlaps
[
torch
.
cat
(
[
pos_inds
,
neg_inds
])]
sampling_result
.
iou
.
detach_
()
return
sampling_result
mmdet3d/ops/iou3d/__init__.py
View file @
9db93054
from
.iou3d_utils
import
(
boxes_iou3d_gpu
,
boxes_iou
_bev
,
nms_gpu
,
nms_normal_gpu
)
from
.iou3d_utils
import
(
boxes_iou3d_gpu
_camera
,
boxes_iou
3d_gpu_lidar
,
boxes_iou_bev
,
nms_gpu
,
nms_normal_gpu
)
__all__
=
[
'boxes_iou_bev'
,
'boxes_iou3d_gpu'
,
'nms_gpu'
,
'nms_normal_gpu'
]
__all__
=
[
'boxes_iou_bev'
,
'boxes_iou3d_gpu_camera'
,
'nms_gpu'
,
'nms_normal_gpu'
,
'boxes_iou3d_gpu_lidar'
]
mmdet3d/ops/iou3d/iou3d_utils.py
View file @
9db93054
...
...
@@ -20,17 +20,22 @@ def boxes_iou_bev(boxes_a, boxes_b):
return
ans_iou
def
boxes_iou3d_gpu
(
boxes_a
,
boxes_b
,
mode
=
'iou'
):
"""
:param boxes_a: (N, 7) [x, y, z, h, w, l, ry]
:param boxes_b: (M, 7) [x, y, z, h, w, l, ry]
:param mode "iou" (intersection over union) or iof (intersection over
def
boxes_iou3d_gpu_camera
(
boxes_a
,
boxes_b
,
mode
=
'iou'
):
"""Calculate 3d iou of boxes in camera coordinate
Args:
boxes_a (FloatTensor): (N, 7) [x, y, z, h, w, l, ry]
in LiDAR coordinate
boxes_b (FloatTensor): (M, 7) [x, y, z, h, w, l, ry]
mode (str): "iou" (intersection over union) or iof (intersection over
foreground).
:return:
ans_iou: (M, N)
Returns:
FloatTensor: (M, N)
"""
boxes_a_bev
=
boxes3d_to_bev_torch
(
boxes_a
)
boxes_b_bev
=
boxes3d_to_bev_torch
(
boxes_b
)
boxes_a_bev
=
boxes3d_to_bev_torch_camera
(
boxes_a
)
boxes_b_bev
=
boxes3d_to_bev_torch_camera
(
boxes_b
)
# bev overlap
overlaps_bev
=
torch
.
cuda
.
FloatTensor
(
...
...
@@ -51,15 +56,62 @@ def boxes_iou3d_gpu(boxes_a, boxes_b, mode='iou'):
# 3d iou
overlaps_3d
=
overlaps_bev
*
overlaps_h
vol_a
=
(
boxes_a
[:,
3
]
*
boxes_a
[:,
4
]
*
boxes_a
[:,
5
]).
view
(
-
1
,
1
)
vol_b
=
(
boxes_b
[:,
3
]
*
boxes_b
[:,
4
]
*
boxes_b
[:,
5
]).
view
(
1
,
-
1
)
vol
ume
_a
=
(
boxes_a
[:,
3
]
*
boxes_a
[:,
4
]
*
boxes_a
[:,
5
]).
view
(
-
1
,
1
)
vol
ume
_b
=
(
boxes_b
[:,
3
]
*
boxes_b
[:,
4
]
*
boxes_b
[:,
5
]).
view
(
1
,
-
1
)
if
mode
==
'iou'
:
# the clamp func is used to avoid division of 0
iou3d
=
overlaps_3d
/
torch
.
clamp
(
vol_a
+
vol_b
-
overlaps_3d
,
min
=
1e-8
)
vol
ume
_a
+
vol
ume
_b
-
overlaps_3d
,
min
=
1e-8
)
else
:
iou3d
=
overlaps_3d
/
torch
.
clamp
(
vol_a
,
min
=
1e-8
)
iou3d
=
overlaps_3d
/
torch
.
clamp
(
volume_a
,
min
=
1e-8
)
return
iou3d
def
boxes_iou3d_gpu_lidar
(
boxes_a
,
boxes_b
,
mode
=
'iou'
):
"""Calculate 3d iou of boxes in lidar coordinate
Args:
boxes_a (FloatTensor): (N, 7) [x, y, z, w, l, h, ry]
in LiDAR coordinate
boxes_b (FloatTensor): (M, 7) [x, y, z, w, l, h, ry]
mode (str): "iou" (intersection over union) or iof (intersection over
foreground).
:Returns:
FloatTensor: (M, N)
"""
boxes_a_bev
=
boxes3d_to_bev_torch_lidar
(
boxes_a
)
boxes_b_bev
=
boxes3d_to_bev_torch_lidar
(
boxes_b
)
# height overlap
boxes_a_height_max
=
(
boxes_a
[:,
2
]
+
boxes_a
[:,
5
]).
view
(
-
1
,
1
)
boxes_a_height_min
=
boxes_a
[:,
2
].
view
(
-
1
,
1
)
boxes_b_height_max
=
(
boxes_b
[:,
2
]
+
boxes_b
[:,
5
]).
view
(
1
,
-
1
)
boxes_b_height_min
=
boxes_b
[:,
2
].
view
(
1
,
-
1
)
# bev overlap
overlaps_bev
=
boxes_a
.
new_zeros
(
torch
.
Size
((
boxes_a
.
shape
[
0
],
boxes_b
.
shape
[
0
])))
# (N, M)
iou3d_cuda
.
boxes_overlap_bev_gpu
(
boxes_a_bev
.
contiguous
(),
boxes_b_bev
.
contiguous
(),
overlaps_bev
)
max_of_min
=
torch
.
max
(
boxes_a_height_min
,
boxes_b_height_min
)
min_of_max
=
torch
.
min
(
boxes_a_height_max
,
boxes_b_height_max
)
overlaps_h
=
torch
.
clamp
(
min_of_max
-
max_of_min
,
min
=
0
)
# 3d iou
overlaps_3d
=
overlaps_bev
*
overlaps_h
volume_a
=
(
boxes_a
[:,
3
]
*
boxes_a
[:,
4
]
*
boxes_a
[:,
5
]).
view
(
-
1
,
1
)
volume_b
=
(
boxes_b
[:,
3
]
*
boxes_b
[:,
4
]
*
boxes_b
[:,
5
]).
view
(
1
,
-
1
)
if
mode
==
'iou'
:
# the clamp func is used to avoid division of 0
iou3d
=
overlaps_3d
/
torch
.
clamp
(
volume_a
+
volume_b
-
overlaps_3d
,
min
=
1e-8
)
else
:
iou3d
=
overlaps_3d
/
torch
.
clamp
(
volume_a
,
min
=
1e-8
)
return
iou3d
...
...
@@ -98,16 +150,39 @@ def nms_normal_gpu(boxes, scores, thresh):
return
order
[
keep
[:
num_out
].
cuda
()].
contiguous
()
def
boxes3d_to_bev_torch
(
boxes3d
):
"""
:param boxes3d: (N, 7) [x, y, z, h, w, l, ry] in camera coords
:return:
boxes_bev: (N, 5) [x1, y1, x2, y2, ry]
def
boxes3d_to_bev_torch_camera
(
boxes3d
):
"""covert boxes3d to bev in in camera coords
Args:
boxes3d (FloartTensor): (N, 7) [x, y, z, h, w, l, ry] in camera coords
Return:
FloartTensor: (N, 5) [x1, y1, x2, y2, ry]
"""
boxes_bev
=
boxes3d
.
new
(
torch
.
Size
((
boxes3d
.
shape
[
0
],
5
)))
cu
,
cv
=
boxes3d
[:,
0
],
boxes3d
[:,
2
]
half_l
,
half_w
=
boxes3d
[:,
5
]
/
2
,
boxes3d
[:,
4
]
/
2
boxes_bev
[:,
0
],
boxes_bev
[:,
1
]
=
cu
-
half_l
,
cv
-
half_w
boxes_bev
[:,
2
],
boxes_bev
[:,
3
]
=
cu
+
half_l
,
cv
+
half_w
boxes_bev
[:,
4
]
=
boxes3d
[:,
6
]
return
boxes_bev
def
boxes3d_to_bev_torch_lidar
(
boxes3d
):
"""covert boxes3d to bev in in LiDAR coords
Args:
boxes3d (FloartTensor): (N, 7) [x, y, z, w, l, h, ry] in LiDAR coords
Returns:
FloartTensor: (N, 5) [x1, y1, x2, y2, ry]
"""
boxes_bev
=
boxes3d
.
new
(
torch
.
Size
((
boxes3d
.
shape
[
0
],
5
)))
x
,
y
=
boxes3d
[:,
0
],
boxes3d
[:,
1
]
half_l
,
half_w
=
boxes3d
[:,
4
]
/
2
,
boxes3d
[:,
3
]
/
2
boxes_bev
[:,
0
],
boxes_bev
[:,
1
]
=
x
-
half_w
,
y
-
half_l
boxes_bev
[:,
2
],
boxes_bev
[:,
3
]
=
x
+
half_w
,
y
+
half_l
boxes_bev
[:,
4
]
=
boxes3d
[:,
6
]
return
boxes_bev
tests/test_samplers.py
0 → 100644
View file @
9db93054
import
torch
from
mmdet3d.core.bbox.assigners
import
MaxIoUAssigner
from
mmdet3d.core.bbox.samplers
import
IoUNegPiecewiseSampler
def
test_iou_piecewise_sampler
():
assigner
=
MaxIoUAssigner
(
pos_iou_thr
=
0.55
,
neg_iou_thr
=
0.55
,
min_pos_iou
=
0.55
,
ignore_iof_thr
=-
1
,
iou_calculator
=
dict
(
type
=
'BboxOverlaps3D'
,
coordinate
=
'lidar'
))
bboxes
=
torch
.
tensor
(
[[
32
,
32
,
16
,
8
,
38
,
42
,
-
0.3
],
[
32
,
32
,
16
,
8
,
38
,
42
,
-
0.3
],
[
32
,
32
,
16
,
8
,
38
,
42
,
-
0.3
],
[
32
,
32
,
16
,
8
,
38
,
42
,
-
0.3
],
[
0
,
0
,
0
,
10
,
10
,
10
,
0.2
],
[
10
,
10
,
10
,
20
,
20
,
15
,
0.6
],
[
5
,
5
,
5
,
15
,
15
,
15
,
0.7
],
[
5
,
5
,
5
,
15
,
15
,
15
,
0.7
],
[
5
,
5
,
5
,
15
,
15
,
15
,
0.7
],
[
32
,
32
,
16
,
8
,
38
,
42
,
-
0.3
],
[
32
,
32
,
16
,
8
,
38
,
42
,
-
0.3
],
[
32
,
32
,
16
,
8
,
38
,
42
,
-
0.3
]],
dtype
=
torch
.
float32
).
cuda
()
gt_bboxes
=
torch
.
tensor
(
[[
0
,
0
,
0
,
10
,
10
,
9
,
0.2
],
[
5
,
10
,
10
,
20
,
20
,
15
,
0.6
]],
dtype
=
torch
.
float32
).
cuda
()
gt_labels
=
torch
.
tensor
([
1
,
1
],
dtype
=
torch
.
int64
).
cuda
()
assign_result
=
assigner
.
assign
(
bboxes
,
gt_bboxes
,
gt_labels
=
gt_labels
)
sampler
=
IoUNegPiecewiseSampler
(
num
=
10
,
pos_fraction
=
0.55
,
neg_piece_fractions
=
[
0.8
,
0.2
],
neg_iou_piece_thrs
=
[
0.55
,
0.1
],
neg_pos_ub
=-
1
,
add_gt_as_proposals
=
False
)
sample_result
=
sampler
.
sample
(
assign_result
,
bboxes
,
gt_bboxes
,
gt_labels
)
assert
sample_result
.
pos_inds
==
4
assert
len
(
sample_result
.
pos_bboxes
)
==
len
(
sample_result
.
pos_inds
)
assert
len
(
sample_result
.
neg_bboxes
)
==
len
(
sample_result
.
neg_inds
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment