def loss_clc(truth_box,gt_lables): true_box, non_ze = utils.trim_zeros_graph(truth_box) gt_lables = tf.boolean_mask(gt_lables, non_ze) priors = utils.get_prio_box() priors_xywh = tf.concat((priors[:, :2] - priors[:, 2:] / 2, priors[:, :2] + priors[:, 2:] / 2), axis=1) ops = utils.overlaps_graph(true_box,priors_xywh) # 获取prior box 最佳匹配index size = trueth best_prior_idx = tf.argmax(ops,axis=1) # 获取truth box 最佳匹配index size = prior best_truth_idx = tf.argmax(ops,axis=0) best_truth_overlab = tf.reduce_max(ops,axis=0) matches = tf.gather(truth_box,best_truth_idx) conf = tf.gather(gt_lables,best_truth_idx) conf = tf.cast(conf,tf.float32) zer = tf.zeros(shape=(tf.shape(conf)),dtype=tf.float32) conf = tf.where(tf.less(best_truth_overlab,0.5),zer,conf) loc = utils.encode_box(matched=matches,prios=priors) return conf,loc
def detect_target(proposals, gt_class_ids, gt_boxes, gt_masks, config): """ Generates detection targets for one image. Subsamples proposals and generates target class IDs, bounding box deltas, and masks for each. Inputs: proposals: [POST_NMS_ROIS_TRAINING, (y1, x1, y2, x2)] in normalized coordinates. Might be zero padded if there are not enough proposals. gt_class_ids: [MAX_GT_INSTANCES] int class IDs gt_boxes: [MAX_GT_INSTANCES, (y1, x1, y2, x2)] in normalized coordinates. gt_masks: [MAX_GT_INSTANCES, height, width] of boolean type. Returns: Target ROIs and corresponding class IDs, bounding box shifts, and masks. rois: [TRAIN_ROIS_PER_IMAGE, (y1, x1, y2, x2)] in normalized coordinates class_ids: [TRAIN_ROIS_PER_IMAGE]. Integer class IDs. Zero padded. deltas: [TRAIN_ROIS_PER_IMAGE, (dy, dx, log(dh), log(dw))] masks: [TRAIN_ROIS_PER_IMAGE, height, width]. Masks cropped to bbox boundaries and resized to neural network output size. Note: Returned arrays might be zero padded if not enough target ROIs. """ # Assertions asserts = [tf.Assert(tf.greater(tf.shape(proposals)[0], 0), [proposals],\ name = 'roi_assertion')] with tf.control_dependencies(asserts): proposals = tf.identity(proposals) # Remove zero padding proposals, _ = utils.trim_zeros_graph(proposals, name='trim_proposals') gt_boxes, non_zeros = utils.trim_zeros_graph(gt_boxes, name='trim_gt_boxes') gt_class_ids = tf.boolean_mask(gt_class_ids, non_zeros,\ name='trim_gt_class_ids') gt_masks = tf.gather(gt_masks, tf.where(non_zeros)[:,0], axis=2,\ name='trim_gt_masks') # Compute overlaps matrix [proposals, gt_boxes] overlaps = utils.overlaps_graph(proposals, gt_boxes) # Determin positive and negative ROIs roi_iou_max = tf.reduce_max(overlaps, axis=1) # 1. Positive ROIs are those with >= 0.5 IoU with a GT box positive_roi_bool = (roi_iou_max >= 0.5) positive_indices = tf.where(positive_roi_bool)[:, 0] # 2. Negative ROIs are those with < 0.5 with every GT box. negative_indices = tf.where(roi_iou_max < 0.5)[:, 0] # Subsample ROIs, Aim for 33% positive # Positive ROIs positive_count = int(config.TRAIN_ROIS_PER_IMAGE * config.ROI_POSITIVE_RATIO) positive_indices = tf.random_shuffle(positive_indices)[:positive_count] positive_count = tf.shape(positive_indices)[0] # Negative ROIs. Add enough to maintain positive:negative ratio r = 1.0 / config.ROI_POSITIVE_RATIO negative_count = tf.cast(r * tf.cast(positive_count, tf.float32), tf.int32)\ - positive_count negative_indices = tf.random_shuffle(negative_indices)[:negative_count] # Gather selected ROIs positive_rois = tf.gather(proposals, positive_indices) negative_rois = tf.gather(proposals, negative_indices) # Assign positive ROIs to GT boxes positive_overlaps = tf.gather(overlaps, positive_indices) roi_gt_box_assignment = tf.cond(\ tf.greater(tf.shape(positive_overlaps)[1], 0),\ true_fn = lambda : tf.argmax(positive_overlaps, axis=1),\ false_fn = lambda: tf.cast(tf.constant([]),tf.int64) ) roi_gt_boxes = tf.gather(gt_boxes, roi_gt_box_assignment) roi_gt_class_ids = tf.gather(gt_class_ids, roi_gt_box_assignment) # Compute bbox refinement for positive ROIs deltas = utils.box_refinement_graph(positive_rois, roi_gt_boxes) deltas /= config.BBOX_STD_DEV # Assign positive ROIs to GT masks # Permute masks to [N, height, width, 1] transposed_masks = tf.expand_dims(tf.transpose(gt_masks, [2, 0, 1]), -1) # Pick the right mask for each ROI roi_masks = tf.gather(transposed_masks, roi_gt_box_assignment) # Compute mask targets boxes = positive_rois if config.USE_MINI_MASK: # Transform ROI coordinates from normalized image space # to normalized mini_mask space y1, x1, y2, x2 = tf.split(positive_rois, 4, axis=1) gt_y1, gt_x1, gt_y2, gt_x2 = tf.split(roi_gt_boxes, 4, axis=1) gt_h = gt_y2 - gt_y1 gt_w = gt_x2 - gt_x1 y1 = (y1 - gt_y1) / gt_h x1 = (x1 - gt_x1) / gt_w y2 = (y2 - gt_y2) / gt_h x2 = (x2 - gt_x2) / gt_w boxes = tf.concat([y1, x1, y2, x2], 1) box_ids = tf.range(0, tf.shape(roi_masks)[0]) masks = tf.image.crop_and_resize(tf.cast(roi_masks, tf.float32), boxes,\ box_ids,\ config.MASK_SHAPE) # Remove the extra dimension from masks masks = tf.squeeze(masks, axis=3) # Threshold mask pixels at 0.5 to have GT masks be 0 or 1\ # to use with binary cross entropy loss masks = tf.round(masks) # Append negative ROIs and pad bbox deltas and masks that # are not used for negative ROIs with zeros. # positive_rois = tf.Print(positive_rois, ['positive_rois: ', tf.shape(positive_rois), "negative_rois: ", tf.shape(negative_rois)]) rois = tf.concat([positive_rois, negative_rois], axis=0) N = tf.shape(negative_rois)[0] P = tf.maximum(config.TRAIN_ROIS_PER_IMAGE - tf.shape(rois)[0], 0) rois = tf.pad(rois, [(0, P), (0, 0)]) roi_gt_boxes = tf.pad(roi_gt_boxes, [(0, N + P), (0, 0)]) roi_gt_class_ids = tf.pad(roi_gt_class_ids, [(0, N + P)]) deltas = tf.pad(deltas, [(0, N + P), (0, 0)]) masks = tf.pad(masks, [(0, N + P), (0, 0), (0, 0)]) return rois, roi_gt_class_ids, deltas, masks
def target_detection(proposals, gt_class_ids, gt_boxes, gt_masks): # assert_ = tf.Assert(tf.greater(tf.shape(proposals)[0], 0), [proposals], name='roi_assert') # with tf.control_dependencies(assert_): # proposals = tf.identity(proposals) proposals, _ = utils.trim_zeros_graph(proposals, name="trim_proposals") gt_boxes, non_zeros = utils.trim_zeros_graph(gt_boxes, name="trim_gt_boxes") gt_class_ids = tf.boolean_mask(gt_class_ids, non_zeros, name="trim_gt_class_ids") gt_masks = tf.gather(gt_masks, tf.where(non_zeros)[:, 0], axis=2, name="trim_gt_masks") crowd_ix = tf.where(gt_class_ids < 0)[:, 0] non_crowd_ix = tf.where(gt_class_ids > 0)[:, 0] crowd_boxes = tf.gather(gt_boxes, crowd_ix) # crowd_masks = tf.gather(gt_masks, crowd_ix, axis=2) gt_class_ids = tf.gather(gt_class_ids, non_crowd_ix) gt_boxes = tf.gather(gt_boxes, non_crowd_ix) gt_masks = tf.gather(gt_masks, non_crowd_ix, axis=2) ious = ops.iou(proposals, gt_boxes) crowd_ious = ops.iou(proposals, crowd_boxes) crowd_iou_max = tf.reduce_max(crowd_ious, axis=1) no_crowd_bool = (crowd_iou_max < 0.001) roi_iou_max = tf.reduce_max(ious, axis=1) positive_roi_bool = (roi_iou_max >= 0.5) positive_indices = tf.where(positive_roi_bool)[:, 0] negative_indices = tf.where(tf.logical_and(roi_iou_max < 0.5, no_crowd_bool))[:, 0] positive_count = int(hyper_parameters.FLAGS.TRAIN_ROIS_PER_IMAGE * hyper_parameters.FLAGS.ROI_POSITIVE_RATIO) positive_indices = tf.random_shuffle(positive_indices)[:positive_count] positive_count = tf.shape(positive_indices)[0] r = 1.0 / hyper_parameters.FLAGS.ROI_POSITIVE_RATIO negative_count = tf.cast(r * tf.cast(positive_count, tf.float32), tf.int32) - positive_count negative_indices = tf.random_shuffle(negative_indices)[:negative_count] positive_rois = tf.gather(proposals, positive_indices) negative_rois = tf.gather(proposals, negative_indices) positive_ious = tf.gather(ious, positive_indices) roi_gt_box_assignment = tf.cond( tf.greater(tf.shape(positive_ious)[1], 0), true_fn=lambda: tf.argmax(positive_ious, axis=1), false_fn=lambda: tf.cast(tf.constant([]), tf.int64) ) roi_gt_boxes = tf.gather(gt_boxes, roi_gt_box_assignment) roi_gt_class_ids = tf.gather(gt_class_ids, roi_gt_box_assignment) deltas = utils.box_refinement_graph(positive_rois, roi_gt_boxes) deltas /= np.array(hyper_parameters.FLAGS.BBOX_STD_DEV) transposed_masks = tf.expand_dims(tf.transpose(gt_masks, [2, 0, 1]), -1) roi_masks = tf.gather(transposed_masks, roi_gt_box_assignment) boxes = positive_rois if hyper_parameters.FLAGS.USE_MINI_MASK: y1, x1, y2, x2 = tf.split(positive_rois, 4, axis=1) gt_y1, gt_x1, gt_y2, gt_x2 = tf.split(roi_gt_boxes, 4, axis=1) gt_h = gt_y2 - gt_y1 gt_w = gt_x2 - gt_x1 y1 = (y1 - gt_y1) / gt_h x1 = (x1 - gt_x1) / gt_w y2 = (y2 - gt_y1) / gt_h x2 = (x2 - gt_x1) / gt_w boxes = tf.concat([y1, x1, y2, x2], 1) box_ids = tf.range(0, tf.shape(roi_masks)[0]) masks = tf.image.crop_and_resize(tf.cast(roi_masks, tf.float32), boxes, box_ids, hyper_parameters.FLAGS.MASK_SHAPE) masks = tf.squeeze(masks, axis=3) masks = tf.round(masks) rois = tf.concat([positive_rois, negative_rois], axis=0) n = tf.shape(negative_rois)[0] p = tf.maximum(hyper_parameters.FLAGS.TRAIN_ROIS_PER_IMAGE - tf.shape(rois)[0], 0) rois = tf.pad(rois, [(0, p), (0, 0)]) # roi_gt_boxes = tf.pad(roi_gt_boxes, [(0, n + p), (0, 0)]) roi_gt_class_ids = tf.pad(roi_gt_class_ids, [(0, n + p)]) deltas = tf.pad(deltas, [(0, n + p), (0, 0)]) masks = tf.pad(masks, [[0, n + p], (0, 0), (0, 0)]) return rois, roi_gt_class_ids, deltas, masks
def level_select(cls_pred, regr_pred, gt_boxes, feature_shapes, strides, pos_scale=0.2): MAX_NUM_GT_BOXES = gt_boxes.shape[0] gt_boxes = gt_boxes[gt_boxes[:, 4] != -1] gt_labels = gt_boxes[:, 4].int() gt_boxes = gt_boxes[:, :4] focal_loss = FocalLoss(alpha=0.25) iou_loss = IoULoss() gt_boxes, non_zeros = trim_zeros_graph(gt_boxes) num_gt_boxes = gt_boxes.shape[0] gt_labels = gt_labels[non_zeros] level_losses = [] total_loss = 0 for level_id in range(len(strides)): stride = strides[level_id] fh = math.ceil(feature_shapes[level_id][0]) fw = math.ceil(feature_shapes[level_id][1]) fa = torch.prod(torch.ceil(feature_shapes), dim=1) start_index = torch.sum(fa[:level_id]).int() start_index = start_index.item() end_index = start_index + fh * fw cls_pred_i = cls_pred[start_index:end_index, :].reshape(fh, fw, -1) regr_pred_i = regr_pred[start_index:end_index, :].reshape(fh, fw, -1) proj_boxes = gt_boxes / stride x1, y1, x2, y2 = prop_box_graph(proj_boxes, pos_scale, fw, fh) level_loss = [] for i in range(num_gt_boxes): x1_ = x1[i] y1_ = y1[i] x2_ = x2[i] y2_ = y2[i] gt_box = gt_boxes[i] gt_label = gt_labels[i] locs_cls_pred_i = cls_pred_i[y1_:y2_, x1_:x2_, :].reshape(-1, cls_pred_i.shape[2]) locs_a, num_class = locs_cls_pred_i.shape locs_cls_true_i = torch.zeros(num_class).cuda() locs_cls_true_i[gt_label] = 1 locs_cls_true_i = locs_cls_true_i.repeat(locs_a, 1) loss_cls = focal_loss(locs_cls_pred_i, locs_cls_true_i) locs_regr_pred_i = regr_pred_i[y1_:y2_, x1_:x2_, :].reshape(-1, 4) shift_x = (np.arange(x1_.cpu(), x2_.cpu()) + 0.5) * stride shift_y = (np.arange(y1_.cpu(), y2_.cpu()) + 0.5) * stride shift_x, shift_y = np.meshgrid(shift_x, shift_y) shifts = np.vstack(( shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel() )).transpose() shifts = torch.from_numpy(shifts).cuda() l = shifts[:, 0] - gt_box[0] t = shifts[:, 1] - gt_box[1] r = gt_box[2] - shifts[:, 2] b = gt_box[3] - shifts[:, 3] locs_regr_true_i = torch.stack([l, t, r, b], dim=1) locs_regr_true_i /= 4.0 loss_regr = iou_loss(locs_regr_pred_i, locs_regr_true_i) level_loss.append(loss_cls + loss_regr) #print(loss_cls.item(), loss_regr.item()) total_loss += (loss_cls + loss_regr) level_losses.append(level_loss) level_losses = torch.FloatTensor(level_losses) if level_losses.shape[1] != 0: gt_box_levels = torch.argmin(level_losses, dim=0).int() else: gt_box_levels = torch.zeros(0).int() padding_gt_box_levels = torch.ones((MAX_NUM_GT_BOXES - num_gt_boxes), dtype=torch.int32) * -1 gt_box_levels = torch.cat((gt_box_levels, padding_gt_box_levels)) if total_loss != 0: total_loss /= 1.0 * (num_gt_boxes * len(strides)) return gt_box_levels, total_loss
def detection_targets_graph(proposals, gt_class_ids, gt_boxes, gt_masks, config): """Generates detection targets for one image. Subsamples proposals and generates target class IDs, bounding box deltas, and masks for each. Inputs: proposals: [POST_NMS_ROIS_TRAINING, (y1, x1, y2, x2)] in normalized coordinates. Might be zero padded if there are not enough proposals. gt_class_ids: [MAX_GT_INSTANCES] int class IDs gt_boxes: [MAX_GT_INSTANCES, (y1, x1, y2, x2)] in normalized coordinates. gt_masks: [height, width, MAX_GT_INSTANCES] of boolean type. Returns: Target ROIs and corresponding class IDs, bounding box shifts, and masks. rois: [TRAIN_ROIS_PER_IMAGE, (y1, x1, y2, x2)] in normalized coordinates class_ids: [TRAIN_ROIS_PER_IMAGE]. Integer class IDs. Zero padded. deltas: [TRAIN_ROIS_PER_IMAGE, (dy, dx, log(dh), log(dw))] masks: [TRAIN_ROIS_PER_IMAGE, height, width]. Masks cropped to bbox boundaries and resized to neural network output size. Note: Returned arrays might be zero padded if not enough target ROIs. """ # Assertions asserts = [ tf.Assert(tf.greater(tf.shape(proposals)[0], 0), [proposals], name='roi_assertion'), ] with tf.control_dependencies(asserts): proposals = tf.identity(proposals) # Remove zero padding proposals, _ = trim_zeros_graph(proposals, name='trim_proposals') gt_boxes, non_zeros = trim_zeros_graph(gt_boxes, name='trim_gt_boxes') gt_class_ids = tf.boolean_mask(gt_class_ids, non_zeros, name='trim_gt_class_ids') gt_masks = tf.gather(gt_masks, tf.where(non_zeros)[:, 0], axis=2, name='trim_gt_masks') crowd_ix = tf.where(gt_class_ids < 0)[:, 0] non_crowd_ix = tf.where(gt_class_ids > 0)[:, 0] crowd_boxes = tf.gather(gt_boxes, crowd_ix) gt_class_ids = tf.gather(gt_class_ids, non_crowd_ix) gt_boxes = tf.gather(gt_boxes, non_crowd_ix) gt_masks = tf.gather(gt_masks, non_crowd_ix, axis=2) overlaps = overlaps_graph(proposals, gt_boxes) crowd_overlaps = overlaps_graph(proposals, crowd_boxes) crowd_iou_max = tf.reduce_max(crowd_overlaps, axis=1) no_crowd_bool = (crowd_iou_max < 0.001) roi_iou_max = tf.reduce_max(overlaps, axis=1) positive_roi_bool = (roi_iou_max >= 0.5) positive_indices = tf.where(positive_roi_bool)[:, 0] negative_indices = tf.where( tf.logical_and(roi_iou_max < 0.5, no_crowd_bool))[:, 0] positive_count = int(config.TRAIN_ROIS_PER_IMAGE * config.ROI_POSITIVE_RATIO) positive_indices = tf.random.shuffle(positive_indices)[:positive_count] positive_count = tf.shape(positive_indices)[0] r = 1.0 / config.ROI_POSITIVE_RATIO negative_count = tf.cast(r * tf.cast(positive_count, tf.float32), tf.int32) - positive_count negative_indices = tf.random.shuffle(negative_indices)[:negative_count] positive_rois = tf.gather(proposals, positive_indices) negative_rois = tf.gather(proposals, negative_indices) positive_overlaps = tf.gather(overlaps, positive_indices) roi_gt_box_assignment = tf.cond( tf.greater(tf.shape(positive_overlaps)[1], 0), true_fn=lambda: tf.argmax(positive_overlaps, axis=1), false_fn=lambda: tf.cast(tf.constant([]), tf.int64)) roi_gt_boxes = tf.gather(gt_boxes, roi_gt_box_assignment) roi_gt_class_ids = tf.gather(gt_class_ids, roi_gt_box_assignment) deltas = utils.box_refinement_graph(positive_rois, roi_gt_boxes) deltas /= config.BBOX_STD_DEV transposed_masks = tf.expand_dims(tf.transpose(gt_masks, [2, 0, 1]), -1) roi_masks = tf.gather(transposed_masks, roi_gt_box_assignment) boxes = positive_rois if config.USE_MINI_MASK: # Transform ROI coordinates from normalized image space # to normalized mini-mask space. y1, x1, y2, x2 = tf.split(positive_rois, 4, axis=1) gt_y1, gt_x1, gt_y2, gt_x2 = tf.split(roi_gt_boxes, 4, axis=1) gt_h = gt_y2 - gt_y1 gt_w = gt_x2 - gt_x1 y1 = (y1 - gt_y1) / gt_h x1 = (x1 - gt_x1) / gt_w y2 = (y2 - gt_y1) / gt_h x2 = (x2 - gt_x1) / gt_w boxes = tf.concat([y1, x1, y2, x2], 1) box_ids = tf.range(0, tf.shape(roi_masks)[0]) masks = tf.image.crop_and_resize(tf.cast(roi_masks, tf.float32), boxes, box_ids, config.MASK_SHAPE) masks = tf.squeeze(masks, axis=3) masks = tf.round(masks) rois = tf.concat([positive_rois, negative_rois], axis=0) N = tf.shape(negative_rois)[0] P = tf.maximum(config.TRAIN_ROIS_PER_IMAGE - tf.shape(rois)[0], 0) rois = tf.pad(rois, [(0, P), (0, 0)]) roi_gt_boxes = tf.pad(roi_gt_boxes, [(0, N + P), (0, 0)]) roi_gt_class_ids = tf.pad(roi_gt_class_ids, [(0, N + P)]) deltas = tf.pad(deltas, [(0, N + P), (0, 0)]) masks = tf.pad(masks, [[0, N + P], (0, 0), (0, 0)]) return rois, roi_gt_class_ids, deltas, masks
def detection_target(input_proposals, input_gt_class_ids, input_gt_boxes): roiss = [] roi_gt_class_idss = [] deltass = [] for b in range(cfg.batch_size): proposals = input_proposals[b, :, :] gt_class_ids = input_gt_class_ids[b, :] gt_boxes = input_gt_boxes[b, :, :] proposals, _ = utils.trim_zeros_graph(proposals, name="trim_proposals") gt_boxes, non_zeros = utils.trim_zeros(gt_boxes, name="trim_gt_boxes") gt_class_ids = tf.boolean_mask(gt_class_ids, non_zeros, name="trim_gt_class_ids") crowd_ix = tf.where(gt_class_ids < 0)[:, 0] non_crowd_ix = tf.where(gt_class_ids > 0)[:, 0] crowd_boxes = tf.gather(gt_boxes, crowd_ix) gt_class_ids = tf.gather(gt_class_ids, non_crowd_ix) gt_boxes = tf.gather(gt_boxes, non_crowd_ix) overlaps = utils.overlaps_graph(proposals, gt_boxes) crowd_overlaps = utils.overlaps_graph(proposals, crowd_boxes) crowd_iou_max = tf.reduce_max(crowd_overlaps, axis=1) no_crowd_bool = (crowd_iou_max < 0.001) roi_iou_max = tf.reduce_max(overlaps, axis=1) positive_roi_bool = (roi_iou_max >= 0.5) positive_indices = tf.where(positive_roi_bool)[:, 0] negative_indices = tf.where( tf.logical_and(roi_iou_max < 0.5, no_crowd_bool))[:, 0] positive_count = int(cfg.TRAIN_ROIS_PER_IMAGE * cfg.ROI_POSITIVE_RATIO) positive_indices = tf.random_shuffle(positive_indices)[:positive_count] positive_count = tf.shape(positive_indices)[0] r = 1.0 / cfg.ROI_POSITIVE_RATIO negative_count = tf.cast(r * tf.cast(positive_count, tf.float32), tf.int32) - positive_count negative_indices = tf.random_shuffle(negative_indices)[:negative_count] positive_rois = tf.gather(proposals, positive_indices) negative_rois = tf.gather(proposals, negative_indices) # Assign positive ROIs to GT boxes. positive_overlaps = tf.gather(overlaps, positive_indices) roi_gt_box_assignment = tf.argmax(positive_overlaps, axis=1) roi_gt_boxes = tf.gather(gt_boxes, roi_gt_box_assignment) roi_gt_class_ids = tf.gather(gt_class_ids, roi_gt_box_assignment) deltas = utils.box_refinement_graph(positive_rois, roi_gt_boxes) deltas /= cfg.BBOX_STD_DEV rois = tf.concat([positive_rois, negative_rois], axis=0) N = tf.shape(negative_rois)[0] P = tf.maximum(cfg.TRAIN_ROIS_PER_IMAGE - tf.shape(rois)[0], 0) rois = tf.pad(rois, [(0, P), (0, 0)]) roi_gt_boxes = tf.pad(roi_gt_boxes, [(0, N + P), (0, 0)]) roi_gt_class_ids = tf.pad(roi_gt_class_ids, [(0, N + P)]) deltas = tf.pad(deltas, [(0, N + P), (0, 0)]) roiss.append(rois) roi_gt_class_idss.append(roi_gt_class_ids) deltass.append(deltas) return tf.stack(roiss, axis=0), tf.stack(roi_gt_class_idss, axis=0), tf.stack(deltass, axis=0)