Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
O
opencv
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
submodule
opencv
Commits
f3eef792
Commit
f3eef792
authored
Feb 16, 2020
by
Dmitry Kurtaev
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Enable Mask R-CNN with Inference Engine. Full coverage with nGraph
parent
a6f3a212
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
119 additions
and
33 deletions
+119
-33
dnn.cpp
modules/dnn/src/dnn.cpp
+25
-7
ie_ngraph.cpp
modules/dnn/src/ie_ngraph.cpp
+4
-5
crop_and_resize_layer.cpp
modules/dnn/src/layers/crop_and_resize_layer.cpp
+41
-0
scale_layer.cpp
modules/dnn/src/layers/scale_layer.cpp
+22
-17
test_tf_importer.cpp
modules/dnn/test/test_tf_importer.cpp
+27
-4
No files found.
modules/dnn/src/dnn.cpp
View file @
f3eef792
...
@@ -1294,13 +1294,15 @@ struct Net::Impl
...
@@ -1294,13 +1294,15 @@ struct Net::Impl
#endif
#endif
clear
();
clear
();
this
->
blobsToKeep
=
blobsToKeep_
;
allocateLayers
(
blobsToKeep_
);
allocateLayers
(
blobsToKeep_
);
MapIdToLayerData
::
iterator
it
=
layers
.
find
(
0
);
MapIdToLayerData
::
iterator
it
=
layers
.
find
(
0
);
CV_Assert
(
it
!=
layers
.
end
());
CV_Assert
(
it
!=
layers
.
end
());
it
->
second
.
skip
=
netInputLayer
->
skip
;
it
->
second
.
skip
=
netInputLayer
->
skip
;
initBackend
();
initBackend
(
blobsToKeep_
);
if
(
!
netWasAllocated
)
if
(
!
netWasAllocated
)
{
{
...
@@ -1313,7 +1315,6 @@ struct Net::Impl
...
@@ -1313,7 +1315,6 @@ struct Net::Impl
}
}
netWasAllocated
=
true
;
netWasAllocated
=
true
;
this
->
blobsToKeep
=
blobsToKeep_
;
if
(
DNN_NETWORK_DUMP
>
0
)
if
(
DNN_NETWORK_DUMP
>
0
)
{
{
...
@@ -1440,7 +1441,7 @@ struct Net::Impl
...
@@ -1440,7 +1441,7 @@ struct Net::Impl
ldOut
.
consumers
.
push_back
(
LayerPin
(
inLayerId
,
outNum
));
ldOut
.
consumers
.
push_back
(
LayerPin
(
inLayerId
,
outNum
));
}
}
void
initBackend
()
void
initBackend
(
const
std
::
vector
<
LayerPin
>&
blobsToKeep_
)
{
{
CV_TRACE_FUNCTION
();
CV_TRACE_FUNCTION
();
if
(
preferableBackend
==
DNN_BACKEND_OPENCV
)
if
(
preferableBackend
==
DNN_BACKEND_OPENCV
)
...
@@ -1450,7 +1451,7 @@ struct Net::Impl
...
@@ -1450,7 +1451,7 @@ struct Net::Impl
else
if
(
preferableBackend
==
DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019
)
else
if
(
preferableBackend
==
DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019
)
{
{
#ifdef HAVE_INF_ENGINE
#ifdef HAVE_INF_ENGINE
initInfEngineBackend
();
initInfEngineBackend
(
blobsToKeep_
);
#else
#else
CV_Assert
(
false
&&
"This OpenCV version is built without Inference Engine API support"
);
CV_Assert
(
false
&&
"This OpenCV version is built without Inference Engine API support"
);
#endif
#endif
...
@@ -1458,7 +1459,7 @@ struct Net::Impl
...
@@ -1458,7 +1459,7 @@ struct Net::Impl
else
if
(
preferableBackend
==
DNN_BACKEND_INFERENCE_ENGINE_NGRAPH
)
else
if
(
preferableBackend
==
DNN_BACKEND_INFERENCE_ENGINE_NGRAPH
)
{
{
#ifdef HAVE_DNN_NGRAPH
#ifdef HAVE_DNN_NGRAPH
initNgraphBackend
();
initNgraphBackend
(
blobsToKeep_
);
#else
#else
CV_Error
(
Error
::
StsNotImplemented
,
"This OpenCV version is built without support of Inference Engine + nGraph"
);
CV_Error
(
Error
::
StsNotImplemented
,
"This OpenCV version is built without support of Inference Engine + nGraph"
);
#endif
#endif
...
@@ -1560,7 +1561,7 @@ struct Net::Impl
...
@@ -1560,7 +1561,7 @@ struct Net::Impl
}
}
}
}
void
initInfEngineBackend
()
void
initInfEngineBackend
(
const
std
::
vector
<
LayerPin
>&
blobsToKeep_
)
{
{
CV_TRACE_FUNCTION
();
CV_TRACE_FUNCTION
();
CV_Assert_N
(
preferableBackend
==
DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019
,
haveInfEngine
());
CV_Assert_N
(
preferableBackend
==
DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019
,
haveInfEngine
());
...
@@ -1750,6 +1751,15 @@ struct Net::Impl
...
@@ -1750,6 +1751,15 @@ struct Net::Impl
CV_Assert
(
!
ieNode
.
empty
());
CV_Assert
(
!
ieNode
.
empty
());
ieNode
->
net
=
net
;
ieNode
->
net
=
net
;
for
(
const
auto
&
pin
:
blobsToKeep_
)
{
if
(
pin
.
lid
==
ld
.
id
)
{
ieNode
->
net
->
addOutput
(
ieNode
->
layer
.
getName
());
break
;
}
}
// Convert weights in FP16 for specific targets.
// Convert weights in FP16 for specific targets.
if
((
preferableTarget
==
DNN_TARGET_OPENCL_FP16
||
if
((
preferableTarget
==
DNN_TARGET_OPENCL_FP16
||
preferableTarget
==
DNN_TARGET_MYRIAD
||
preferableTarget
==
DNN_TARGET_MYRIAD
||
...
@@ -1856,7 +1866,7 @@ struct Net::Impl
...
@@ -1856,7 +1866,7 @@ struct Net::Impl
}
}
}
}
void
initNgraphBackend
()
void
initNgraphBackend
(
const
std
::
vector
<
LayerPin
>&
blobsToKeep_
)
{
{
CV_TRACE_FUNCTION
();
CV_TRACE_FUNCTION
();
CV_Assert_N
(
preferableBackend
==
DNN_BACKEND_INFERENCE_ENGINE_NGRAPH
,
haveInfEngine
());
CV_Assert_N
(
preferableBackend
==
DNN_BACKEND_INFERENCE_ENGINE_NGRAPH
,
haveInfEngine
());
...
@@ -2045,6 +2055,14 @@ struct Net::Impl
...
@@ -2045,6 +2055,14 @@ struct Net::Impl
// TF EAST_text_detection
// TF EAST_text_detection
ieNode
->
net
->
setUnconnectedNodes
(
ieNode
);
ieNode
->
net
->
setUnconnectedNodes
(
ieNode
);
}
}
for
(
const
auto
&
pin
:
blobsToKeep_
)
{
if
(
pin
.
lid
==
ld
.
id
)
{
ieNode
->
net
->
addOutput
(
ieNode
->
node
->
get_friendly_name
());
break
;
}
}
ieNode
->
net
->
setNodePtr
(
&
ieNode
->
node
);
ieNode
->
net
->
setNodePtr
(
&
ieNode
->
node
);
net
->
addBlobs
(
ld
.
inputBlobsWrappers
);
net
->
addBlobs
(
ld
.
inputBlobsWrappers
);
...
...
modules/dnn/src/ie_ngraph.cpp
View file @
f3eef792
...
@@ -231,11 +231,10 @@ void InfEngineNgraphNet::init(Target targetId)
...
@@ -231,11 +231,10 @@ void InfEngineNgraphNet::init(Target targetId)
}
}
}
}
}
}
}
else
{
}
for
(
const
auto
&
name
:
requestedOutputs
)
for
(
const
auto
&
name
:
requestedOutputs
)
{
{
cnn
.
addOutput
(
name
);
cnn
.
addOutput
(
name
);
}
}
}
for
(
const
auto
&
it
:
cnn
.
getInputsInfo
())
for
(
const
auto
&
it
:
cnn
.
getInputsInfo
())
...
...
modules/dnn/src/layers/crop_and_resize_layer.cpp
View file @
f3eef792
...
@@ -5,6 +5,7 @@
...
@@ -5,6 +5,7 @@
// Copyright (C) 2018, Intel Corporation, all rights reserved.
// Copyright (C) 2018, Intel Corporation, all rights reserved.
// Third party copyrights are property of their respective owners.
// Third party copyrights are property of their respective owners.
#include "../precomp.hpp"
#include "../precomp.hpp"
#include "../ie_ngraph.hpp"
#include "layers_common.hpp"
#include "layers_common.hpp"
namespace
cv
{
namespace
dnn
{
namespace
cv
{
namespace
dnn
{
...
@@ -20,6 +21,11 @@ public:
...
@@ -20,6 +21,11 @@ public:
outHeight
=
params
.
get
<
float
>
(
"height"
);
outHeight
=
params
.
get
<
float
>
(
"height"
);
}
}
virtual
bool
supportBackend
(
int
backendId
)
CV_OVERRIDE
{
return
backendId
==
DNN_BACKEND_OPENCV
||
backendId
==
DNN_BACKEND_INFERENCE_ENGINE_NGRAPH
;
}
bool
getMemoryShapes
(
const
std
::
vector
<
MatShape
>
&
inputs
,
bool
getMemoryShapes
(
const
std
::
vector
<
MatShape
>
&
inputs
,
const
int
requiredOutputs
,
const
int
requiredOutputs
,
std
::
vector
<
MatShape
>
&
outputs
,
std
::
vector
<
MatShape
>
&
outputs
,
...
@@ -111,6 +117,41 @@ public:
...
@@ -111,6 +117,41 @@ public:
}
}
}
}
#ifdef HAVE_DNN_NGRAPH
virtual
Ptr
<
BackendNode
>
initNgraph
(
const
std
::
vector
<
Ptr
<
BackendWrapper
>
>&
inputs
,
const
std
::
vector
<
Ptr
<
BackendNode
>
>&
nodes
)
CV_OVERRIDE
{
// Slice second input: from 1x1xNx7 to 1x1xNx5
auto
input
=
nodes
[
0
].
dynamicCast
<
InfEngineNgraphNode
>
()
->
node
;
auto
rois
=
nodes
[
1
].
dynamicCast
<
InfEngineNgraphNode
>
()
->
node
;
std
::
vector
<
size_t
>
dims
=
rois
->
get_shape
(),
offsets
(
4
,
0
);
offsets
[
3
]
=
2
;
dims
[
3
]
=
7
;
auto
lower_bounds
=
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
i64
,
ngraph
::
Shape
{
offsets
.
size
()},
offsets
.
data
());
auto
upper_bounds
=
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
i64
,
ngraph
::
Shape
{
dims
.
size
()},
dims
.
data
());
auto
strides
=
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
i64
,
ngraph
::
Shape
{
dims
.
size
()},
std
::
vector
<
int64_t
>
((
int64_t
)
dims
.
size
(),
1
));
auto
slice
=
std
::
make_shared
<
ngraph
::
op
::
v1
::
StridedSlice
>
(
rois
,
lower_bounds
,
upper_bounds
,
strides
,
std
::
vector
<
int64_t
>
{},
std
::
vector
<
int64_t
>
{});
// Reshape rois from 4D to 2D
std
::
vector
<
size_t
>
shapeData
=
{
dims
[
2
],
5
};
auto
shape
=
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
i64
,
ngraph
::
Shape
{
2
},
shapeData
.
data
());
auto
reshape
=
std
::
make_shared
<
ngraph
::
op
::
v1
::
Reshape
>
(
slice
,
shape
,
true
);
auto
roiPooling
=
std
::
make_shared
<
ngraph
::
op
::
v0
::
ROIPooling
>
(
input
,
reshape
,
ngraph
::
Shape
{(
size_t
)
outHeight
,
(
size_t
)
outWidth
},
1.0
f
,
"bilinear"
);
return
Ptr
<
BackendNode
>
(
new
InfEngineNgraphNode
(
roiPooling
));
}
#endif // HAVE_DNN_NGRAPH
private
:
private
:
int
outWidth
,
outHeight
;
int
outWidth
,
outHeight
;
};
};
...
...
modules/dnn/src/layers/scale_layer.cpp
View file @
f3eef792
...
@@ -53,7 +53,8 @@ public:
...
@@ -53,7 +53,8 @@ public:
virtual
bool
supportBackend
(
int
backendId
)
CV_OVERRIDE
virtual
bool
supportBackend
(
int
backendId
)
CV_OVERRIDE
{
{
return
backendId
==
DNN_BACKEND_OPENCV
||
backendId
==
DNN_BACKEND_HALIDE
||
return
backendId
==
DNN_BACKEND_OPENCV
||
backendId
==
DNN_BACKEND_HALIDE
||
((
backendId
==
DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019
||
backendId
==
DNN_BACKEND_INFERENCE_ENGINE_NGRAPH
)
&&
axis
==
1
);
(
backendId
==
DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019
&&
axis
==
1
)
||
(
backendId
==
DNN_BACKEND_INFERENCE_ENGINE_NGRAPH
&&
axis
>
0
);
}
}
void
forward
(
InputArrayOfArrays
inputs_arr
,
OutputArrayOfArrays
outputs_arr
,
OutputArrayOfArrays
internals_arr
)
CV_OVERRIDE
void
forward
(
InputArrayOfArrays
inputs_arr
,
OutputArrayOfArrays
outputs_arr
,
OutputArrayOfArrays
internals_arr
)
CV_OVERRIDE
...
@@ -233,22 +234,26 @@ public:
...
@@ -233,22 +234,26 @@ public:
auto
ieInpNode
=
nodes
[
0
].
dynamicCast
<
InfEngineNgraphNode
>
()
->
node
;
auto
ieInpNode
=
nodes
[
0
].
dynamicCast
<
InfEngineNgraphNode
>
()
->
node
;
std
::
vector
<
size_t
>
shape
(
ieInpNode
->
get_shape
().
size
(),
1
);
std
::
vector
<
size_t
>
shape
(
ieInpNode
->
get_shape
().
size
(),
1
);
shape
[
1
]
=
numChannels
;
int
cAxis
=
clamp
(
axis
,
shape
.
size
());
auto
weight
=
hasWeights
?
shape
[
cAxis
]
=
numChannels
;
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
f32
,
ngraph
::
Shape
(
shape
),
blobs
[
0
].
data
)
:
auto
node
=
ieInpNode
;
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
f32
,
if
(
hasWeights
)
ngraph
::
Shape
(
shape
),
std
::
vector
<
float
>
(
numChannels
,
1
).
data
());
{
auto
weight
=
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
f32
,
auto
bias
=
hasBias
?
ngraph
::
Shape
(
shape
),
blobs
[
0
].
data
);
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
f32
,
node
=
std
::
make_shared
<
ngraph
::
op
::
v1
::
Multiply
>
(
node
,
weight
,
ngraph
::
op
::
AutoBroadcastType
::
NUMPY
);
ngraph
::
Shape
(
shape
),
blobs
.
back
().
data
)
:
}
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
f32
,
if
(
hasBias
||
!
hasWeights
)
ngraph
::
Shape
(
shape
),
std
::
vector
<
float
>
(
numChannels
,
0
).
data
());
{
auto
bias
=
hasBias
?
auto
scale_node
=
std
::
make_shared
<
ngraph
::
op
::
v1
::
Multiply
>
(
ieInpNode
,
weight
,
ngraph
::
op
::
AutoBroadcastType
::
NUMPY
);
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
f32
,
auto
scale_shift
=
std
::
make_shared
<
ngraph
::
op
::
v1
::
Add
>
(
scale_node
,
bias
,
ngraph
::
op
::
AutoBroadcastType
::
NUMPY
);
ngraph
::
Shape
(
shape
),
blobs
.
back
().
data
)
:
return
Ptr
<
BackendNode
>
(
new
InfEngineNgraphNode
(
scale_shift
));
std
::
make_shared
<
ngraph
::
op
::
Constant
>
(
ngraph
::
element
::
f32
,
ngraph
::
Shape
(
shape
),
std
::
vector
<
float
>
(
numChannels
,
0
).
data
());
node
=
std
::
make_shared
<
ngraph
::
op
::
v1
::
Add
>
(
node
,
bias
,
ngraph
::
op
::
AutoBroadcastType
::
NUMPY
);
}
return
Ptr
<
BackendNode
>
(
new
InfEngineNgraphNode
(
node
));
}
}
#endif // HAVE_DNN_NGRAPH
#endif // HAVE_DNN_NGRAPH
...
...
modules/dnn/test/test_tf_importer.cpp
View file @
f3eef792
...
@@ -914,8 +914,16 @@ TEST(Test_TensorFlow, two_inputs)
...
@@ -914,8 +914,16 @@ TEST(Test_TensorFlow, two_inputs)
normAssert
(
out
,
firstInput
+
secondInput
);
normAssert
(
out
,
firstInput
+
secondInput
);
}
}
TEST
(
Test_TensorFlow
,
Mask_RCNN
)
TEST
_P
(
Test_TensorFlow_nets
,
Mask_RCNN
)
{
{
static
const
double
kMaskThreshold
=
0.5
;
if
(
backend
==
DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019
&&
target
==
DNN_TARGET_MYRIAD
)
applyTestTag
(
CV_TEST_TAG_DNN_SKIP_IE_MYRIAD
,
CV_TEST_TAG_DNN_SKIP_IE_NN_BUILDER
);
if
(
target
==
DNN_TARGET_MYRIAD
&&
getInferenceEngineVPUType
()
==
CV_DNN_INFERENCE_ENGINE_VPU_TYPE_MYRIAD_X
)
applyTestTag
(
CV_TEST_TAG_DNN_SKIP_IE_MYRIAD_X
);
applyTestTag
(
CV_TEST_TAG_MEMORY_1GB
,
CV_TEST_TAG_DEBUG_VERYLONG
);
applyTestTag
(
CV_TEST_TAG_MEMORY_1GB
,
CV_TEST_TAG_DEBUG_VERYLONG
);
Mat
img
=
imread
(
findDataFile
(
"dnn/street.png"
));
Mat
img
=
imread
(
findDataFile
(
"dnn/street.png"
));
std
::
string
proto
=
findDataFile
(
"dnn/mask_rcnn_inception_v2_coco_2018_01_28.pbtxt"
);
std
::
string
proto
=
findDataFile
(
"dnn/mask_rcnn_inception_v2_coco_2018_01_28.pbtxt"
);
...
@@ -926,7 +934,8 @@ TEST(Test_TensorFlow, Mask_RCNN)
...
@@ -926,7 +934,8 @@ TEST(Test_TensorFlow, Mask_RCNN)
Mat
refMasks
=
blobFromNPY
(
path
(
"mask_rcnn_inception_v2_coco_2018_01_28.detection_masks.npy"
));
Mat
refMasks
=
blobFromNPY
(
path
(
"mask_rcnn_inception_v2_coco_2018_01_28.detection_masks.npy"
));
Mat
blob
=
blobFromImage
(
img
,
1.0
f
,
Size
(
800
,
800
),
Scalar
(),
true
,
false
);
Mat
blob
=
blobFromImage
(
img
,
1.0
f
,
Size
(
800
,
800
),
Scalar
(),
true
,
false
);
net
.
setPreferableBackend
(
DNN_BACKEND_OPENCV
);
net
.
setPreferableBackend
(
backend
);
net
.
setPreferableTarget
(
target
);
net
.
setInput
(
blob
);
net
.
setInput
(
blob
);
...
@@ -940,7 +949,10 @@ TEST(Test_TensorFlow, Mask_RCNN)
...
@@ -940,7 +949,10 @@ TEST(Test_TensorFlow, Mask_RCNN)
Mat
outDetections
=
outs
[
0
];
Mat
outDetections
=
outs
[
0
];
Mat
outMasks
=
outs
[
1
];
Mat
outMasks
=
outs
[
1
];
normAssertDetections
(
refDetections
,
outDetections
,
""
,
/*threshold for zero confidence*/
1e-5
);
double
scoreDiff
=
(
target
==
DNN_TARGET_OPENCL_FP16
||
target
==
DNN_TARGET_MYRIAD
)
?
0.019
:
2e-5
;
double
iouDiff
=
(
target
==
DNN_TARGET_OPENCL_FP16
||
target
==
DNN_TARGET_MYRIAD
)
?
0.018
:
default_lInf
;
normAssertDetections
(
refDetections
,
outDetections
,
""
,
/*threshold for zero confidence*/
1e-5
,
scoreDiff
,
iouDiff
);
// Output size of masks is NxCxHxW where
// Output size of masks is NxCxHxW where
// N - number of detected boxes
// N - number of detected boxes
...
@@ -964,7 +976,18 @@ TEST(Test_TensorFlow, Mask_RCNN)
...
@@ -964,7 +976,18 @@ TEST(Test_TensorFlow, Mask_RCNN)
outMasks
(
srcRanges
).
copyTo
(
masks
(
dstRanges
));
outMasks
(
srcRanges
).
copyTo
(
masks
(
dstRanges
));
}
}
cv
::
Range
topRefMasks
[]
=
{
Range
::
all
(),
Range
(
0
,
numDetections
),
Range
::
all
(),
Range
::
all
()};
cv
::
Range
topRefMasks
[]
=
{
Range
::
all
(),
Range
(
0
,
numDetections
),
Range
::
all
(),
Range
::
all
()};
normAssert
(
masks
,
refMasks
(
&
topRefMasks
[
0
]));
refMasks
=
refMasks
(
&
topRefMasks
[
0
]);
// make binary masks
cv
::
threshold
(
masks
.
reshape
(
1
,
1
),
masks
,
kMaskThreshold
,
1
,
THRESH_BINARY
);
cv
::
threshold
(
refMasks
.
reshape
(
1
,
1
),
refMasks
,
kMaskThreshold
,
1
,
THRESH_BINARY
);
double
inter
=
cv
::
countNonZero
(
masks
&
refMasks
);
double
area
=
cv
::
countNonZero
(
masks
|
refMasks
);
EXPECT_GE
(
inter
/
area
,
0.99
);
if
(
backend
==
DNN_BACKEND_INFERENCE_ENGINE_NGRAPH
)
expectNoFallbacks
(
net
);
}
}
}
}
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