"docs/en/tutorials/customize_runtime.md" did not exist on "f482dd78b73f1be2771530119e4b8dcbdb38eea2"
Commit 3cea66d1 authored by Vighnesh Birodkar's avatar Vighnesh Birodkar Committed by TF Object Detection Team
Browse files

Ignore loss computation in overlapping boxes for DeepMAC.

PiperOrigin-RevId: 452829947
parent a62ef994
...@@ -68,7 +68,8 @@ DeepMACParams = collections.namedtuple('DeepMACParams', [ ...@@ -68,7 +68,8 @@ DeepMACParams = collections.namedtuple('DeepMACParams', [
'augmented_self_supervision_loss', 'augmented_self_supervision_loss',
'augmented_self_supervision_scale_min', 'augmented_self_supervision_scale_min',
'augmented_self_supervision_scale_max', 'augmented_self_supervision_scale_max',
'pointly_supervised_keypoint_loss_weight' 'pointly_supervised_keypoint_loss_weight',
'ignore_per_class_box_overlap'
]) ])
...@@ -254,6 +255,36 @@ def filter_masked_classes(masked_class_ids, classes, weights, masks): ...@@ -254,6 +255,36 @@ def filter_masked_classes(masked_class_ids, classes, weights, masks):
) )
def per_instance_no_class_overlap(classes, boxes, height, width):
"""Returns 1s inside boxes but overlapping boxes of same class are zeroed out.
Args:
classes: A [batch_size, num_instances, num_classes] float tensor containing
the one-hot encoded classes.
boxes: A [batch_size, num_instances, 4] shaped float tensor of normalized
boxes.
height: int, height of the desired mask.
width: int, width of the desired mask.
Returns:
mask: A [batch_size, num_instances, height, width] float tensor of 0s and
1s.
"""
box_mask = fill_boxes(boxes, height, width)
per_class_box_mask = (
box_mask[:, :, tf.newaxis, :, :] *
classes[:, :, :, tf.newaxis, tf.newaxis])
per_class_instance_count = tf.reduce_sum(per_class_box_mask, axis=1)
per_class_valid_map = per_class_instance_count < 2
class_indices = tf.argmax(classes, axis=2)
per_instance_valid_map = tf.gather(
per_class_valid_map, class_indices, batch_dims=1)
return tf.cast(per_instance_valid_map, tf.float32)
def flatten_first2_dims(tensor): def flatten_first2_dims(tensor):
"""Flatten first 2 dimensions of a tensor. """Flatten first 2 dimensions of a tensor.
...@@ -1144,13 +1175,15 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch): ...@@ -1144,13 +1175,15 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch):
def predict(self, preprocessed_inputs, true_image_shapes): def predict(self, preprocessed_inputs, true_image_shapes):
prediction_dict = super(DeepMACMetaArch, self).predict( prediction_dict = super(DeepMACMetaArch, self).predict(
preprocessed_inputs, true_image_shapes) preprocessed_inputs, true_image_shapes)
mask_logits = self._predict_mask_logits_from_gt_boxes(prediction_dict)
prediction_dict[MASK_LOGITS_GT_BOXES] = mask_logits
if self._deepmac_params.augmented_self_supervision_loss_weight > 0.0: if self.groundtruth_has_field(fields.BoxListFields.boxes):
prediction_dict[SELF_SUPERVISED_DEAUGMENTED_MASK_LOGITS] = ( mask_logits = self._predict_mask_logits_from_gt_boxes(prediction_dict)
self._predict_deaugmented_mask_logits_on_augmented_inputs( prediction_dict[MASK_LOGITS_GT_BOXES] = mask_logits
preprocessed_inputs, true_image_shapes))
if self._deepmac_params.augmented_self_supervision_loss_weight > 0.0:
prediction_dict[SELF_SUPERVISED_DEAUGMENTED_MASK_LOGITS] = (
self._predict_deaugmented_mask_logits_on_augmented_inputs(
preprocessed_inputs, true_image_shapes))
return prediction_dict return prediction_dict
def _predict_deaugmented_mask_logits_on_augmented_inputs( def _predict_deaugmented_mask_logits_on_augmented_inputs(
...@@ -1349,14 +1382,17 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch): ...@@ -1349,14 +1382,17 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch):
raise ValueError('Unknown loss aggregation - {}'.format(method)) raise ValueError('Unknown loss aggregation - {}'.format(method))
def _compute_mask_prediction_loss( def _compute_mask_prediction_loss(
self, boxes, mask_logits, mask_gt): self, boxes, mask_logits, mask_gt, classes):
"""Compute the per-instance mask loss. """Compute the per-instance mask loss.
Args: Args:
boxes: A [batch_size, num_instances, 4] float tensor of GT boxes. boxes: A [batch_size, num_instances, 4] float tensor of GT boxes in
mask_logits: A [batch_suze, num_instances, height, width] float tensor of normalized coordinates.
mask_logits: A [batch_size, num_instances, height, width] float tensor of
predicted masks predicted masks
mask_gt: The groundtruth mask of same shape as mask_logits. mask_gt: The groundtruth mask of same shape as mask_logits.
classes: A [batch_size, num_instances, num_classes] shaped tensor of
one-hot encoded classes.
Returns: Returns:
loss: A [batch_size, num_instances] shaped tensor with the loss for each loss: A [batch_size, num_instances] shaped tensor with the loss for each
...@@ -1369,9 +1405,19 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch): ...@@ -1369,9 +1405,19 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch):
batch_size, num_instances = tf.shape(boxes)[0], tf.shape(boxes)[1] batch_size, num_instances = tf.shape(boxes)[0], tf.shape(boxes)[1]
mask_logits = self._resize_logits_like_gt(mask_logits, mask_gt) mask_logits = self._resize_logits_like_gt(mask_logits, mask_gt)
height, width = tf.shape(mask_logits)[2], tf.shape(mask_logits)[3]
if self._deepmac_params.ignore_per_class_box_overlap:
mask_logits *= per_instance_no_class_overlap(
classes, boxes, height, width)
height, wdith = tf.shape(mask_gt)[2], tf.shape(mask_gt)[3]
mask_logits *= per_instance_no_class_overlap(
classes, boxes, height, wdith)
mask_logits = tf.reshape(mask_logits, [batch_size * num_instances, -1, 1]) mask_logits = tf.reshape(mask_logits, [batch_size * num_instances, -1, 1])
mask_gt = tf.reshape(mask_gt, [batch_size * num_instances, -1, 1]) mask_gt = tf.reshape(mask_gt, [batch_size * num_instances, -1, 1])
loss = self._deepmac_params.classification_loss( loss = self._deepmac_params.classification_loss(
prediction_tensor=mask_logits, prediction_tensor=mask_logits,
target_tensor=mask_gt, target_tensor=mask_gt,
...@@ -1660,7 +1706,7 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch): ...@@ -1660,7 +1706,7 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch):
return tf.reshape(loss, [batch_size, num_instances]) return tf.reshape(loss, [batch_size, num_instances])
def _compute_deepmac_losses( def _compute_deepmac_losses(
self, boxes, masks_logits, masks_gt, image, self, boxes, masks_logits, masks_gt, classes, image,
self_supervised_masks_logits=None, keypoints_gt=None, self_supervised_masks_logits=None, keypoints_gt=None,
keypoints_depth_gt=None): keypoints_depth_gt=None):
"""Returns the mask loss per instance. """Returns the mask loss per instance.
...@@ -1674,6 +1720,8 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch): ...@@ -1674,6 +1720,8 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch):
masks_gt: A [batch_size, num_instances, output_height, output_width] float masks_gt: A [batch_size, num_instances, output_height, output_width] float
tensor containing the groundtruth masks. If masks_gt is None, tensor containing the groundtruth masks. If masks_gt is None,
DEEP_MASK_ESTIMATION is filled with 0s. DEEP_MASK_ESTIMATION is filled with 0s.
classes: A [batch_size, num_instances, num_classes] tensor of one-hot
encoded classes.
image: [batch_size, output_height, output_width, channels] float tensor image: [batch_size, output_height, output_width, channels] float tensor
denoting the input image. denoting the input image.
self_supervised_masks_logits: Optional self-supervised mask logits to self_supervised_masks_logits: Optional self-supervised mask logits to
...@@ -1712,7 +1760,7 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch): ...@@ -1712,7 +1760,7 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch):
masks_gt = self._get_groundtruth_mask_output( masks_gt = self._get_groundtruth_mask_output(
boxes_for_crop, masks_gt) boxes_for_crop, masks_gt)
mask_prediction_loss = self._compute_mask_prediction_loss( mask_prediction_loss = self._compute_mask_prediction_loss(
boxes_for_crop, masks_logits, masks_gt) boxes_for_crop, masks_logits, masks_gt, classes)
box_consistency_loss = self._compute_box_consistency_loss( box_consistency_loss = self._compute_box_consistency_loss(
boxes, boxes_for_crop, masks_logits) boxes, boxes_for_crop, masks_logits)
...@@ -1803,7 +1851,8 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch): ...@@ -1803,7 +1851,8 @@ class DeepMACMetaArch(center_net_meta_arch.CenterNetMetaArch):
gt_weights, gt_masks) gt_weights, gt_masks)
sample_loss_dict = self._compute_deepmac_losses( sample_loss_dict = self._compute_deepmac_losses(
gt_boxes, mask_logits, gt_masks, image, boxes=gt_boxes, masks_logits=mask_logits, masks_gt=gt_masks,
classes=gt_classes, image=image,
self_supervised_masks_logits=self_supervised_mask_logits, self_supervised_masks_logits=self_supervised_mask_logits,
keypoints_gt=gt_keypoints, keypoints_depth_gt=gt_depths) keypoints_gt=gt_keypoints, keypoints_depth_gt=gt_depths)
......
...@@ -109,7 +109,8 @@ def build_meta_arch(**override_params): ...@@ -109,7 +109,8 @@ def build_meta_arch(**override_params):
augmented_self_supervision_loss='loss_dice', augmented_self_supervision_loss='loss_dice',
augmented_self_supervision_scale_min=1.0, augmented_self_supervision_scale_min=1.0,
augmented_self_supervision_scale_max=1.0, augmented_self_supervision_scale_max=1.0,
pointly_supervised_keypoint_loss_weight=1.0) pointly_supervised_keypoint_loss_weight=1.0,
ignore_per_class_box_overlap=False)
params.update(override_params) params.update(override_params)
...@@ -199,6 +200,7 @@ DEEPMAC_PROTO_TEXT = """ ...@@ -199,6 +200,7 @@ DEEPMAC_PROTO_TEXT = """
augmented_self_supervision_scale_min: 0.42 augmented_self_supervision_scale_min: 0.42
augmented_self_supervision_scale_max: 1.42 augmented_self_supervision_scale_max: 1.42
pointly_supervised_keypoint_loss_weight: 0.13 pointly_supervised_keypoint_loss_weight: 0.13
ignore_per_class_box_overlap: true
""" """
...@@ -229,6 +231,7 @@ class DeepMACUtilsTest(tf.test.TestCase, parameterized.TestCase): ...@@ -229,6 +231,7 @@ class DeepMACUtilsTest(tf.test.TestCase, parameterized.TestCase):
params.augmented_self_supervision_scale_max, 1.42) params.augmented_self_supervision_scale_max, 1.42)
self.assertAlmostEqual( self.assertAlmostEqual(
params.pointly_supervised_keypoint_loss_weight, 0.13) params.pointly_supervised_keypoint_loss_weight, 0.13)
self.assertTrue(params.ignore_per_class_box_overlap)
def test_subsample_trivial(self): def test_subsample_trivial(self):
"""Test subsampling masks.""" """Test subsampling masks."""
...@@ -531,6 +534,18 @@ class DeepMACUtilsTest(tf.test.TestCase, parameterized.TestCase): ...@@ -531,6 +534,18 @@ class DeepMACUtilsTest(tf.test.TestCase, parameterized.TestCase):
expected_output = np.reshape(expected_output, (1, 1, 1, 1)) expected_output = np.reshape(expected_output, (1, 1, 1, 1))
self.assertAllClose(expected_output, out) self.assertAllClose(expected_output, out)
def test_per_instance_no_class_overlap(self):
boxes = tf.constant([[[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 0.4, 0.4]],
[[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0]]],
dtype=tf.float32)
classes = tf.constant([[[0, 1, 0], [0, 1, 0]], [[0, 1, 0], [1, 0, 0]]],
dtype=tf.float32)
output = deepmac_meta_arch.per_instance_no_class_overlap(
classes, boxes, 2, 2)
self.assertEqual(output.shape, (2, 2, 2, 2))
self.assertAllClose(output[1], np.ones((2, 2, 2)))
self.assertAllClose(output[0, 1], [[0., 1.0], [1.0, 1.0]])
@unittest.skipIf(tf_version.is_tf1(), 'Skipping TF2.X only test.') @unittest.skipIf(tf_version.is_tf1(), 'Skipping TF2.X only test.')
class DeepMACMaskHeadTest(tf.test.TestCase, parameterized.TestCase): class DeepMACMaskHeadTest(tf.test.TestCase, parameterized.TestCase):
...@@ -943,6 +958,7 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase): ...@@ -943,6 +958,7 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase):
def test_predict_self_supervised_deaugmented_mask_logits(self): def test_predict_self_supervised_deaugmented_mask_logits(self):
tf.keras.backend.set_learning_phase(True)
model = build_meta_arch( model = build_meta_arch(
augmented_self_supervision_loss_weight=1.0, augmented_self_supervision_loss_weight=1.0,
predict_full_resolution_masks=True) predict_full_resolution_masks=True)
...@@ -967,9 +983,10 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase): ...@@ -967,9 +983,10 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase):
masks[0, 0, :16, :16] = 1.0 masks[0, 0, :16, :16] = 1.0
masks[0, 1, 16:, 16:] = 1.0 masks[0, 1, 16:, 16:] = 1.0
masks_pred = tf.fill((1, 2, 32, 32), 0.9) masks_pred = tf.fill((1, 2, 32, 32), 0.9)
classes = tf.zeros((1, 2, 5))
loss_dict = model._compute_deepmac_losses( loss_dict = model._compute_deepmac_losses(
boxes, masks_pred, masks, tf.zeros((1, 16, 16, 3))) boxes, masks_pred, masks, classes, tf.zeros((1, 16, 16, 3)))
self.assertAllClose( self.assertAllClose(
loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION], loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION],
np.zeros((1, 2)) - tf.math.log(tf.nn.sigmoid(0.9))) np.zeros((1, 2)) - tf.math.log(tf.nn.sigmoid(0.9)))
...@@ -980,9 +997,10 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase): ...@@ -980,9 +997,10 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase):
boxes = tf.constant([[[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0]]]) boxes = tf.constant([[[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0]]])
masks = tf.ones((1, 2, 128, 128), dtype=tf.float32) masks = tf.ones((1, 2, 128, 128), dtype=tf.float32)
masks_pred = tf.fill((1, 2, 32, 32), 0.9) masks_pred = tf.fill((1, 2, 32, 32), 0.9)
classes = tf.zeros((1, 2, 5))
loss_dict = model._compute_deepmac_losses( loss_dict = model._compute_deepmac_losses(
boxes, masks_pred, masks, tf.zeros((1, 32, 32, 3))) boxes, masks_pred, masks, classes, tf.zeros((1, 32, 32, 3)))
self.assertAllClose( self.assertAllClose(
loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION], loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION],
np.zeros((1, 2)) - tf.math.log(tf.nn.sigmoid(0.9))) np.zeros((1, 2)) - tf.math.log(tf.nn.sigmoid(0.9)))
...@@ -995,9 +1013,10 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase): ...@@ -995,9 +1013,10 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase):
masks = np.ones((1, 2, 128, 128), dtype=np.float32) masks = np.ones((1, 2, 128, 128), dtype=np.float32)
masks = tf.constant(masks) masks = tf.constant(masks)
masks_pred = tf.fill((1, 2, 32, 32), 0.9) masks_pred = tf.fill((1, 2, 32, 32), 0.9)
classes = tf.zeros((1, 2, 5))
loss_dict = model._compute_deepmac_losses( loss_dict = model._compute_deepmac_losses(
boxes, masks_pred, masks, tf.zeros((1, 32, 32, 3))) boxes, masks_pred, masks, classes, tf.zeros((1, 32, 32, 3)))
pred = tf.nn.sigmoid(0.9) pred = tf.nn.sigmoid(0.9)
expected = (1.0 - ((2.0 * pred) / (1.0 + pred))) expected = (1.0 - ((2.0 * pred) / (1.0 + pred)))
self.assertAllClose(loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION], self.assertAllClose(loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION],
...@@ -1007,9 +1026,10 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase): ...@@ -1007,9 +1026,10 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase):
boxes = tf.zeros([1, 0, 4]) boxes = tf.zeros([1, 0, 4])
masks = tf.zeros([1, 0, 128, 128]) masks = tf.zeros([1, 0, 128, 128])
classes = tf.zeros((1, 2, 5))
loss_dict = self.model._compute_deepmac_losses( loss_dict = self.model._compute_deepmac_losses(
boxes, masks, masks, boxes, masks, masks, classes,
tf.zeros((1, 16, 16, 3))) tf.zeros((1, 16, 16, 3)))
self.assertEqual(loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION].shape, self.assertEqual(loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION].shape,
(1, 0)) (1, 0))
...@@ -1476,6 +1496,33 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase): ...@@ -1476,6 +1496,33 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase):
self.assertGreater(loss['Loss/' + weak_loss], 0.0, self.assertGreater(loss['Loss/' + weak_loss], 0.0,
'{} was <= 0'.format(weak_loss)) '{} was <= 0'.format(weak_loss))
def test_eval_loss_and_postprocess_keys(self):
model = build_meta_arch(
use_dice_loss=True,
augmented_self_supervision_loss_weight=1.0,
augmented_self_supervision_max_translation=0.5,
predict_full_resolution_masks=True)
true_image_shapes = tf.constant([[32, 32, 3]], dtype=tf.int32)
prediction_dict = model.predict(
tf.zeros((1, 32, 32, 3)), true_image_shapes)
output = model.postprocess(prediction_dict, true_image_shapes)
self.assertEqual(output['detection_boxes'].shape, (1, 5, 4))
self.assertEqual(output['detection_masks'].shape, (1, 5, 128, 128))
model.provide_groundtruth(
groundtruth_boxes_list=[
tf.convert_to_tensor([[0., 0., 1., 1.]] * 5)] * 1,
groundtruth_classes_list=[tf.one_hot([1, 0, 1, 1, 1], depth=6)] * 1,
groundtruth_weights_list=[tf.ones(5)] * 1,
groundtruth_masks_list=[tf.ones((5, 32, 32))] * 1,
groundtruth_keypoints_list=[tf.zeros((5, 10, 2))] * 1,
groundtruth_keypoint_depths_list=[tf.zeros((5, 10))] * 1)
prediction_dict = model.predict(
tf.zeros((1, 32, 32, 3)), true_image_shapes)
model.loss(prediction_dict, true_image_shapes)
def test_loss_weight_response(self): def test_loss_weight_response(self):
tf.random.set_seed(12) tf.random.set_seed(12)
model = build_meta_arch( model = build_meta_arch(
...@@ -1661,6 +1708,30 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase): ...@@ -1661,6 +1708,30 @@ class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase):
self.assertEqual(loss.shape, (1, 1)) self.assertEqual(loss.shape, (1, 1))
self.assertAllClose(expected_loss, loss) self.assertAllClose(expected_loss, loss)
def test_ignore_per_class_box_overlap(self):
tf.keras.backend.set_learning_phase(True)
model = build_meta_arch(
use_dice_loss=False,
predict_full_resolution_masks=True,
network_type='cond_inst1',
dim=9,
pixel_embedding_dim=8,
use_instance_embedding=False,
use_xy=False,
pointly_supervised_keypoint_loss_weight=1.0,
ignore_per_class_box_overlap=True)
self.assertTrue(model._deepmac_params.ignore_per_class_box_overlap)
mask_logits = tf.zeros((2, 3, 16, 16))
mask_gt = tf.zeros((2, 3, 32, 32))
boxes = tf.zeros((2, 3, 4))
classes = tf.zeros((2, 3, 5))
loss = model._compute_mask_prediction_loss(
boxes, mask_logits, mask_gt, classes)
self.assertEqual(loss.shape, (2, 3))
@unittest.skipIf(tf_version.is_tf1(), 'Skipping TF2.X only test.') @unittest.skipIf(tf_version.is_tf1(), 'Skipping TF2.X only test.')
class FullyConnectedMaskHeadTest(tf.test.TestCase): class FullyConnectedMaskHeadTest(tf.test.TestCase):
......
...@@ -403,7 +403,7 @@ message CenterNet { ...@@ -403,7 +403,7 @@ message CenterNet {
// Mask prediction support using DeepMAC. See https://arxiv.org/abs/2104.00613 // Mask prediction support using DeepMAC. See https://arxiv.org/abs/2104.00613
// Next ID 34 // Next ID 35
message DeepMACMaskEstimation { message DeepMACMaskEstimation {
// The loss used for penalizing mask predictions. // The loss used for penalizing mask predictions.
optional ClassificationLoss classification_loss = 1; optional ClassificationLoss classification_loss = 1;
...@@ -531,6 +531,10 @@ message CenterNet { ...@@ -531,6 +531,10 @@ message CenterNet {
// Depth = -1 is assumed to be background. // Depth = -1 is assumed to be background.
optional float pointly_supervised_keypoint_loss_weight = 33 [default = 0.0]; optional float pointly_supervised_keypoint_loss_weight = 33 [default = 0.0];
// When set, loss computation is ignored at pixels that fall within
// 2 boxes of the same class.
optional bool ignore_per_class_box_overlap = 34 [default = false];
} }
optional DeepMACMaskEstimation deepmac_mask_estimation = 14; optional DeepMACMaskEstimation deepmac_mask_estimation = 14;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment