Generalized plaque digitization framework for multi-dimensional mesoscopic images.
The 6 matches
- [1] § Materials and methods › 3D plaque spatial localization ↔ 2D_detection/mrcnn/config.py, lines 7–157 · score 0.80 · gradient clipping, ground truth, maximum suppression, decay, NMS, head
- [2] § Materials and methods › 2D plaque spatial localization ↔ 2D_detection/mrcnn/model.py, lines 1792–1858 · score 0.78 · attention maps, feature map, fused, sigmoid, activated, Pyramid
- [3] § Materials and methods › 3D plaque spatial localization ↔ 2D_detection/samples/Plaques/Plaques.py, lines 63–120 · score 0.58 · gradient clipping, decay, NMS, configurations, anchor, confidence
- [4] § Results › Multi-scale feature fusion and enhancement improves complex plaque detection ↔ 2D_detection/mrcnn/model.py, lines 1792–1858 · score 0.58 · attention maps, feature map, fuses, FPN, Fusion, enhancement
- [5] § Materials and methods › Foreground signal segmentation ↔ segmentation/seg_block.py, lines 436–489 · score 0.56 · Connected component, holes, edge, filling, volume, signals
- [6] § Results › Plaque-oriented training strategies facilitate model convergence and generalization ↔ 3D_detection/util.py, lines 253–367 · score 0.53 · Focal Loss, confidence loss, smoothly, weight, class, batches
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 2,888 lines · 117 KB · no license · 2 matches
- '''
- Model network construction.
- '''
- import os
- import random
- import datetime
- import re
- import math
- import logging
- from collections import OrderedDict
- import multiprocessing
- import numpy as np
- import tensorflow as tf
- import keras
- import keras.backend as K
- import keras.layers as KL
- import keras.engine as KE
- import keras.models as KM
- import logging
- import matplotlib.pyplot as plt
- from mrcnn import utils
- # Requires TensorFlow 1.3+ and Keras 2.0.8+.
- from distutils.version import LooseVersion
- assert LooseVersion(tf.__version__) >= LooseVersion("1.3")
- assert LooseVersion(keras.__version__) >= LooseVersion('2.0.8')
- def log(text, array=None):
- if array is not None:
- text = text.ljust(25)
- text += ("shape: {:20} ".format(str(array.shape)))
- if array.size:
- text += ("min: {:10.5f} max: {:10.5f}".format(array.min(),array.max()))
- else:
- text += ("min: {:10} max: {:10}".format("",""))
- text += " {}".format(array.dtype)
- print(text)
- class BatchNorm(KL.BatchNormalization):
- def call(self, inputs, training=None):
- return super(self.__class__, self).call(inputs, training=training)
- def compute_backbone_shapes(config, image_shape):
- if callable(config.BACKBONE):
- return config.COMPUTE_BACKBONE_SHAPE(image_shape)
- # Currently supports ResNet only
- assert config.BACKBONE in ["resnet50", "resnet101"]
- return np.array(
- [[int(math.ceil(image_shape[0] / stride)),
- int(math.ceil(image_shape[1] / stride))]
- for stride in config.BACKBONE_STRIDES])
- # Resnet Graph
- def identity_block(input_tensor, kernel_size, filters, stage, block,
- use_bias=True, train_bn=True):
- nb_filter1, nb_filter2, nb_filter3 = filters
- conv_name_base = 'res' + str(stage) + block + '_branch'
- bn_name_base = 'bn' + str(stage) + block + '_branch'
- x = KL.Conv2D(nb_filter1, (1, 1), name=conv_name_base + '2a',
- use_bias=use_bias)(input_tensor)
- x = BatchNorm(name=bn_name_base + '2a')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- x = KL.Conv2D(nb_filter2, (kernel_size, kernel_size), padding='same',
- name=conv_name_base + '2b', use_bias=use_bias)(x)
- x = BatchNorm(name=bn_name_base + '2b')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- x = KL.Conv2D(nb_filter3, (1, 1), name=conv_name_base + '2c',
- use_bias=use_bias)(x)
- x = BatchNorm(name=bn_name_base + '2c')(x, training=train_bn)
- x = KL.Add()([x, input_tensor])
- x = KL.Activation('relu', name='res' + str(stage) + block + '_out')(x)
- return x
- def conv_block(input_tensor, kernel_size, filters, stage, block,
- strides=(2, 2), use_bias=True, train_bn=True):
- nb_filter1, nb_filter2, nb_filter3 = filters
- conv_name_base = 'res' + str(stage) + block + '_branch'
- bn_name_base = 'bn' + str(stage) + block + '_branch'
- x = KL.Conv2D(nb_filter1, (1, 1), strides=strides,
- name=conv_name_base + '2a', use_bias=use_bias)(input_tensor)
- x = BatchNorm(name=bn_name_base + '2a')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- x = KL.Conv2D(nb_filter2, (kernel_size, kernel_size), padding='same',
- name=conv_name_base + '2b', use_bias=use_bias)(x)
- x = BatchNorm(name=bn_name_base + '2b')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- x = KL.Conv2D(nb_filter3, (1, 1), name=conv_name_base +
- '2c', use_bias=use_bias)(x)
- x = BatchNorm(name=bn_name_base + '2c')(x, training=train_bn)
- shortcut = KL.Conv2D(nb_filter3, (1, 1), strides=strides,
- name=conv_name_base + '1', use_bias=use_bias)(input_tensor)
- shortcut = BatchNorm(name=bn_name_base + '1')(shortcut, training=train_bn)
- x = KL.Add()([x, shortcut])
- x = KL.Activation('relu', name='res' + str(stage) + block + '_out')(x)
- return x
- def resnet_graph(input_image, architecture, stage5=False, train_bn=True):
- assert architecture in ["resnet50", "resnet101"]
- # Stage 1
- x = KL.ZeroPadding2D((3, 3))(input_image)
- x = KL.Conv2D(64, (7, 7), strides=(2, 2), name='conv1', use_bias=True)(x)
- x = BatchNorm(name='bn_conv1')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- C1 = x = KL.MaxPooling2D((3, 3), strides=(2, 2), padding="same")(x)
- # Stage 2
- x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1), train_bn=train_bn)
- x = identity_block(x, 3, [64, 64, 256], stage=2, block='b', train_bn=train_bn)
- C2 = x = identity_block(x, 3, [64, 64, 256], stage=2, block='c', train_bn=train_bn)
- # Stage 3
- x = conv_block(x, 3, [128, 128, 512], stage=3, block='a', train_bn=train_bn)
- x = identity_block(x, 3, [128, 128, 512], stage=3, block='b', train_bn=train_bn)
- x = identity_block(x, 3, [128, 128, 512], stage=3, block='c', train_bn=train_bn)
- C3 = x = identity_block(x, 3, [128, 128, 512], stage=3, block='d', train_bn=train_bn)
- # Stage 4
- x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a', train_bn=train_bn)
- block_count = {"resnet50": 5, "resnet101": 22}[architecture]
- for i in range(block_count):
- x = identity_block(x, 3, [256, 256, 1024], stage=4, block=chr(98 + i), train_bn=train_bn)
- C4 = x
- # Stage 5
- if stage5:
- x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a', train_bn=train_bn)
- x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b', train_bn=train_bn)
- C5 = x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c', train_bn=train_bn)
- else:
- C5 = None
- return [C1, C2, C3, C4, C5]
- # Proposal Layer
- def apply_box_deltas_graph(boxes, deltas):
- # Convert to y, x, h, w
- height = boxes[:, 2] - boxes[:, 0]
- width = boxes[:, 3] - boxes[:, 1]
- center_y = boxes[:, 0] + 0.5 * height
- center_x = boxes[:, 1] + 0.5 * width
- # Apply deltas
- center_y += deltas[:, 0] * height
- center_x += deltas[:, 1] * width
- height *= tf.exp(deltas[:, 2])
- width *= tf.exp(deltas[:, 3])
- # Convert back to y1, x1, y2, x2
- y1 = center_y - 0.5 * height
- x1 = center_x - 0.5 * width
- y2 = y1 + height
- x2 = x1 + width
- result = tf.stack([y1, x1, y2, x2], axis=1, name="apply_box_deltas_out")
- return result
- def clip_boxes_graph(boxes, window):
- # Split
- wy1, wx1, wy2, wx2 = tf.split(window, 4)
- y1, x1, y2, x2 = tf.split(boxes, 4, axis=1)
- # Clip
- y1 = tf.maximum(tf.minimum(y1, wy2), wy1)
- x1 = tf.maximum(tf.minimum(x1, wx2), wx1)
- y2 = tf.maximum(tf.minimum(y2, wy2), wy1)
- x2 = tf.maximum(tf.minimum(x2, wx2), wx1)
- clipped = tf.concat([y1, x1, y2, x2], axis=1, name="clipped_boxes")
- clipped.set_shape((clipped.shape[0], 4))
- return clipped
- class ProposalLayer(KE.Layer):
- def __init__(self, proposal_count, nms_threshold, config=None, **kwargs):
- super(ProposalLayer, self).__init__(**kwargs)
- self.config = config
- self.proposal_count = proposal_count
- self.nms_threshold = nms_threshold
- def call(self, inputs):
- # Box Scores. Use the foreground class confidence. [Batch, num_rois, 1]
- scores = inputs[0][:, :, 1]
- # Box deltas [batch, num_rois, 4]
- deltas = inputs[1]
- # [0.1 0.1 0.2 0.2]
- deltas = deltas * np.reshape(self.config.RPN_BBOX_STD_DEV, [1, 1, 4])
- # Anchors
- anchors = inputs[2]
- # Improve performance by trimming to top anchors by score
- # and doing the rest on the smaller subset.
- pre_nms_limit = tf.minimum(self.config.PRE_NMS_LIMIT, tf.shape(anchors)[1])
- ix = tf.nn.top_k(scores, pre_nms_limit, sorted=True,
- name="top_anchors").indices
- scores = utils.batch_slice([scores, ix], lambda x, y: tf.gather(x, y),
- self.config.IMAGES_PER_GPU)
- deltas = utils.batch_slice([deltas, ix], lambda x, y: tf.gather(x, y),
- self.config.IMAGES_PER_GPU)
- pre_nms_anchors = utils.batch_slice([anchors, ix], lambda a, x: tf.gather(a, x),
- self.config.IMAGES_PER_GPU,
- names=["pre_nms_anchors"])
- # Apply deltas to anchors to get refined anchors.
- # [batch, N, (y1, x1, y2, x2)]
- boxes = utils.batch_slice([pre_nms_anchors, deltas],
- lambda x, y: apply_box_deltas_graph(x, y),
- self.config.IMAGES_PER_GPU,
- names=["refined_anchors"])
- # Clip to image boundaries. Since we're in normalized coordinates,
- # clip to 0..1 range. [batch, N, (y1, x1, y2, x2)]
- window = np.array([0, 0, 1, 1], dtype=np.float32)
- boxes = utils.batch_slice(boxes,
- lambda x: clip_boxes_graph(x, window),
- self.config.IMAGES_PER_GPU,
- names=["refined_anchors_clipped"])
- # Filter out small boxes
- # According to Xinlei Chen's paper, this reduces detection accuracy
- # for small objects, so we're skipping it.
- # Non-max suppression
- def nms(boxes, scores):
- indices = tf.image.non_max_suppression(
- boxes, scores, self.proposal_count,
- self.nms_threshold, name="rpn_non_max_suppression")
- proposals = tf.gather(boxes, indices)
- # Pad if needed
- padding = tf.maximum(self.proposal_count - tf.shape(proposals)[0], 0)
- proposals = tf.pad(proposals, [(0, padding), (0, 0)])
- return proposals
- proposals = utils.batch_slice([boxes, scores], nms,
- self.config.IMAGES_PER_GPU)
- return proposals
- def compute_output_shape(self, input_shape):
- return (None, self.proposal_count, 4)
- # ROIAlign Layer
- def log2_graph(x):
- return tf.log(x) / tf.log(2.0)
- class PyramidROIAlign(KE.Layer):
- def __init__(self, pool_shape, **kwargs):
- super(PyramidROIAlign, self).__init__(**kwargs)
- self.pool_shape = tuple(pool_shape)
- def call(self, inputs):
- # Crop boxes [batch, num_boxes, (y1, x1, y2, x2)] in normalized coords
- boxes = inputs[0]
- # Image meta
- # Holds details about the image. See compose_image_meta()
- image_meta = inputs[1]
- # Feature Maps. List of feature maps from different level of the
- # feature pyramid. Each is [batch, height, width, channels]
- feature_maps = inputs[2:]
- # Assign each ROI to a level in the pyramid based on the ROI area.
- y1, x1, y2, x2 = tf.split(boxes, 4, axis=2)
- h = y2 - y1
- w = x2 - x1
- # Use shape of first image. Images in a batch must have the same size.
- image_shape = parse_image_meta_graph(image_meta)['image_shape'][0]
- # Equation 1 in the Feature Pyramid Networks paper. Account for
- # the fact that our coordinates are normalized here.
- image_area = tf.cast(image_shape[0] * image_shape[1], tf.float32)
- roi_level = log2_graph(tf.sqrt(h * w) / (224.0 / tf.sqrt(image_area)))
- roi_level = tf.minimum(5, tf.maximum(
- 2, 4 + tf.cast(tf.round(roi_level), tf.int32)))
- roi_level = tf.squeeze(roi_level, 2)
- # Loop through levels and apply ROI pooling to each. P2 to P5.
- pooled = []
- box_to_level = []
- for i, level in enumerate(range(2, 6)):
- ix = tf.where(tf.equal(roi_level, level))
- level_boxes = tf.gather_nd(boxes, ix)
- # Box indices for crop_and_resize.
- box_indices = tf.cast(ix[:, 0], tf.int32)
- # Keep track of which box is mapped to which level
- box_to_level.append(ix)
- # Stop gradient propogation to ROI proposals
- level_boxes = tf.stop_gradient(level_boxes)
- box_indices = tf.stop_gradient(box_indices)
- # Crop and Resize
- # Here we use the simplified approach of a single value per bin,
- # which is how it's done in tf.crop_and_resize()
- # Result: [batch * num_boxes, pool_height, pool_width, channels]
- pooled.append(tf.image.crop_and_resize(
- feature_maps[i], level_boxes, box_indices, self.pool_shape,
- method="bilinear"))
- # Pack pooled features into one tensor
- pooled = tf.concat(pooled, axis=0)
- # Pack box_to_level mapping into one array and add another
- # column representing the order of pooled boxes
- box_to_level = tf.concat(box_to_level, axis=0)
- box_range = tf.expand_dims(tf.range(tf.shape(box_to_level)[0]), 1)
- box_to_level = tf.concat([tf.cast(box_to_level, tf.int32), box_range],
- axis=1)
- # Rearrange pooled features to match the order of the original boxes
- # Sort box_to_level by batch then box index
- # TF doesn't have a way to sort by two columns, so merge them and sort.
- sorting_tensor = box_to_level[:, 0] * 100000 + box_to_level[:, 1]
- ix = tf.nn.top_k(sorting_tensor, k=tf.shape(
- box_to_level)[0]).indices[::-1]
- ix = tf.gather(box_to_level[:, 2], ix)
- pooled = tf.gather(pooled, ix)
- # Re-add the batch dimension reshape[batch, num_rois, POOL_SIZE, POOL_SIZE, channels]
- shape = tf.concat([tf.shape(boxes)[:2], tf.shape(pooled)[1:]], axis=0)
- pooled = tf.reshape(pooled, shape)
- return pooled
- def compute_output_shape(self, input_shape):
- return input_shape[0][:2] + self.pool_shape + (input_shape[2][-1], )
- # Detection Target Layer
- def overlaps_graph(boxes1, boxes2):
- # 1. Tile boxes2 and repeat boxes1.
- b1 = tf.reshape(tf.tile(tf.expand_dims(boxes1, 1),
- [1, 1, tf.shape(boxes2)[0]]), [-1, 4])
- b2 = tf.tile(boxes2, [tf.shape(boxes1)[0], 1])
- # 2. Compute intersections
- b1_y1, b1_x1, b1_y2, b1_x2 = tf.split(b1, 4, axis=1)
- b2_y1, b2_x1, b2_y2, b2_x2 = tf.split(b2, 4, axis=1)
- y1 = tf.maximum(b1_y1, b2_y1)
- x1 = tf.maximum(b1_x1, b2_x1)
- y2 = tf.minimum(b1_y2, b2_y2)
- x2 = tf.minimum(b1_x2, b2_x2)
- intersection = tf.maximum(x2 - x1, 0) * tf.maximum(y2 - y1, 0)
- # 3. Compute unions
- b1_area = (b1_y2 - b1_y1) * (b1_x2 - b1_x1)
- b2_area = (b2_y2 - b2_y1) * (b2_x2 - b2_x1)
- union = b1_area + b2_area - intersection
- # 4. Compute IoU and reshape to [boxes1, boxes2]
- iou = intersection / union
- overlaps = tf.reshape(iou, [tf.shape(boxes1)[0], tf.shape(boxes2)[0]])
- return overlaps
- def detection_targets_graph(proposals, gt_class_ids, gt_boxes, gt_masks, config):
- if tf.equal(tf.shape(proposals)[0], 0):
- rois = tf.zeros([config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
- target_class_ids = tf.zeros([config.TRAIN_ROIS_PER_IMAGE], dtype=tf.int32)
- target_bbox = tf.zeros([config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
- target_mask = tf.zeros([config.TRAIN_ROIS_PER_IMAGE, config.MASK_SHAPE[0],
- config.MASK_SHAPE[1]], dtype=tf.float32)
- return rois, target_class_ids, target_bbox, target_mask
- # 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")
- # Handle COCO crowds
- 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)
- # Compute overlaps matrix [proposals, gt_boxes]
- overlaps = overlaps_graph(proposals, gt_boxes)
- # Compute overlaps with crowd boxes [proposals, crowd_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)
- # Determine 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. Skip crowds.
- negative_indices = tf.where(tf.logical_and(roi_iou_max < 0.5, no_crowd_bool))[:, 0]
- # Subsample ROIs. Aim for 33% positive
- 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
- transposed_masks = tf.expand_dims(tf.transpose(gt_masks, [2, 0, 1]), -1)
- 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_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)
- # 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.
- 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
- class DetectionTargetLayer(KE.Layer):
- def __init__(self, config, **kwargs):
- super(DetectionTargetLayer, self).__init__(**kwargs)
- self.config = config
- def call(self, inputs):
- proposals = inputs[0]
- gt_class_ids = inputs[1]
- gt_boxes = inputs[2]
- gt_masks = inputs[3]
- def empty_output():
- rois = tf.zeros([self.config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
- target_class_ids = tf.zeros([self.config.TRAIN_ROIS_PER_IMAGE], dtype=tf.int32)
- target_bbox = tf.zeros([self.config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
- target_mask = tf.zeros([self.config.TRAIN_ROIS_PER_IMAGE,
- self.config.MASK_SHAPE[0],
- self.config.MASK_SHAPE[1]], dtype=tf.float32)
- return rois, target_class_ids, target_bbox, target_mask
- def normal_output():
- # Slice the batch and run a graph for each slice
- names = ["rois", "target_class_ids", "target_bbox", "target_mask"]
- outputs = utils.batch_slice(
- [proposals, gt_class_ids, gt_boxes, gt_masks],
- lambda w, x, y, z: detection_targets_graph(
- w, x, y, z, self.config),
- self.config.IMAGES_PER_GPU, names=names)
- return outputs
- is_empty = tf.equal(tf.shape(proposals)[0], 0)
- return tf.cond(is_empty, empty_output, normal_output)
- def compute_output_shape(self, input_shape):
- return [
- (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # rois
- (None, self.config.TRAIN_ROIS_PER_IMAGE), # class_ids
- (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # deltas
- (None, self.config.TRAIN_ROIS_PER_IMAGE, self.config.MASK_SHAPE[0],
- self.config.MASK_SHAPE[1]) # masks
- ]
- def compute_mask(self, inputs, mask=None):
- return [None, None, None, None]
- class DetectionTargetLayerNoMask_old(KE.Layer):
- """DetectionTargetLayer without a mask"""
- def __init__(self, config, **kwargs):
- super(DetectionTargetLayerNoMask, self).__init__(**kwargs)
- self.config = config
- def call(self, inputs):
- proposals = inputs[0]
- gt_class_ids = inputs[1]
- gt_boxes = inputs[2]
- gt_class_ids = tf.where(
- gt_class_ids > 0,
- tf.ones_like(gt_class_ids),
- tf.zeros_like(gt_class_ids)
- )
- def get_normal_output():
- # Retrieve the batch size and num_rois for the proposals
- proposal_shape = tf.shape(proposals)
- batch_size = proposal_shape[0]
- num_rois = proposal_shape[1]
- # Create a dummy_masks shape that matches the proposals [batch, num_rois, 0, 0]
- # shape[0] = batch_size > 0
- dummy_masks = tf.zeros([batch_size, num_rois, 0, 0])
- names = ["rois", "target_class_ids", "target_bbox", "target_mask"]
- outputs = utils.batch_slice(
- [proposals, gt_class_ids, gt_boxes, dummy_masks],
- lambda w, x, y, z: detection_targets_graph(
- w, x, y, z, self.config),
- self.config.IMAGES_PER_GPU,
- names=names)
- return [outputs[0], outputs[1], outputs[2]]
- def get_empty_output():
- batch_size = self.config.IMAGES_PER_GPU
- rois = tf.zeros([batch_size, self.config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
- target_class_ids = tf.zeros([batch_size, self.config.TRAIN_ROIS_PER_IMAGE], dtype=tf.int32)
- target_bbox = tf.zeros([batch_size, self.config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
- return [rois, target_class_ids, target_bbox]
- is_empty = tf.equal(tf.shape(proposals)[0], 0)
- return tf.cond(is_empty, get_empty_output, get_normal_output)
- def compute_output_shape(self, input_shape):
- return [
- (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # rois
- (None, self.config.TRAIN_ROIS_PER_IMAGE), # class_ids
- (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # deltas
- ]
- class DetectionTargetLayerNoMask(KE.Layer):
- """DetectionTargetLayer (without mask)"""
- def __init__(self, config, **kwargs):
- super(DetectionTargetLayerNoMask, self).__init__(**kwargs)
- self.config = config
- def call(self, inputs):
- proposals = inputs[0]
- gt_class_ids = inputs[1]
- gt_boxes = inputs[2]
- gt_class_ids = tf.where(
- gt_class_ids > 0,
- tf.ones_like(gt_class_ids),
- tf.zeros_like(gt_class_ids)
- )
- def process_single_sample(prop, gt_cls, gt_box):
- """Proposals for processing individual samples"""
- num_proposals = tf.shape(prop)[0]
- num_rois = self.config.TRAIN_ROIS_PER_IMAGE
- indices = tf.random_shuffle(tf.range(num_proposals))[:num_rois]
- selected_rois = tf.gather(prop, indices)
- # Create target classes (for simplicity: set all to 1, i.e. foreground)
- num_selected = tf.shape(selected_rois)[0]
- target_class_ids = tf.ones([num_selected], dtype=tf.int32)
- # Create bbox deltas (simplified: all zeros, i.e. no adjustment)
- target_bbox = tf.zeros([num_selected, 4], dtype=tf.float32)
- # Padding
- pad_size = num_rois - num_selected
- rois_padded = tf.pad(selected_rois, [[0, pad_size], [0, 0]])
- target_class_ids_padded = tf.pad(target_class_ids, [[0, pad_size]])
- target_bbox_padded = tf.pad(target_bbox, [[0, pad_size], [0, 0]])
- return rois_padded, target_class_ids_padded, target_bbox_padded
- # batch
- names = ["rois", "target_class_ids", "target_bbox"]
- outputs = utils.batch_slice(
- [proposals, gt_class_ids, gt_boxes],
- lambda p, c, b: process_single_sample(p, c, b),
- self.config.IMAGES_PER_GPU,
- names=names)
- return [outputs[0], outputs[1], outputs[2]]
- def compute_output_shape(self, input_shape):
- return [
- (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # rois
- (None, self.config.TRAIN_ROIS_PER_IMAGE), # class_ids
- (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # deltas
- ]
- def compute_mask(self, inputs, mask=None):
- return [None, None, None]
- # Detection Layer
- def refine_detections_graph(rois, probs, deltas, window, config):
- # Class IDs per ROI
- class_ids = tf.argmax(probs, axis=1, output_type=tf.int32)
- # Class probability of the top class of each ROI
- indices = tf.stack([tf.range(probs.shape[0]), class_ids], axis=1)
- class_scores = tf.gather_nd(probs, indices)
- # Class-specific bounding box deltas
- deltas_specific = tf.gather_nd(deltas, indices)
- # Apply bounding box deltas
- # Shape: [boxes, (y1, x1, y2, x2)] in normalized coordinates
- refined_rois = apply_box_deltas_graph(
- rois, deltas_specific * config.BBOX_STD_DEV)
- # Clip boxes to image window 防止超出0-1
- refined_rois = clip_boxes_graph(refined_rois, window)
- # Filter out background boxes
- keep = tf.where(class_ids > 0)[:, 0]
- # Filter out low confidence boxes
- if config.DETECTION_MIN_CONFIDENCE:
- conf_keep = tf.where(class_scores >= config.DETECTION_MIN_CONFIDENCE)[:, 0]
- keep = tf.sets.set_intersection(tf.expand_dims(keep, 0),
- tf.expand_dims(conf_keep, 0))
- keep = tf.sparse_tensor_to_dense(keep)[0]
- # Apply per-class NMS
- # 1. Prepare variables
- pre_nms_class_ids = tf.gather(class_ids, keep)
- pre_nms_scores = tf.gather(class_scores, keep)
- pre_nms_rois = tf.gather(refined_rois, keep)
- unique_pre_nms_class_ids = tf.unique(pre_nms_class_ids)[0]
- def nms_keep_map(class_id):
- """Apply Non-Maximum Suppression on ROIs of the given class."""
- # Indices of ROIs of the given class
- ixs = tf.where(tf.equal(pre_nms_class_ids, class_id))[:, 0]
- # Apply NMS
- class_keep = tf.image.non_max_suppression(
- tf.gather(pre_nms_rois, ixs),
- tf.gather(pre_nms_scores, ixs),
- max_output_size=config.DETECTION_MAX_INSTANCES,
- iou_threshold=config.DETECTION_NMS_THRESHOLD)
- # Map indices
- class_keep = tf.gather(keep, tf.gather(ixs, class_keep))
- # Pad with -1 so returned tensors have the same shape
- gap = config.DETECTION_MAX_INSTANCES - tf.shape(class_keep)[0]
- class_keep = tf.pad(class_keep, [(0, gap)],
- mode='CONSTANT', constant_values=-1)
- # Set shape so map_fn() can infer result shape
- class_keep.set_shape([config.DETECTION_MAX_INSTANCES])
- return class_keep
- # 2. Map over class IDs 进行nms
- nms_keep = tf.map_fn(nms_keep_map, unique_pre_nms_class_ids,
- dtype=tf.int64)
- # 3. Merge results into one list, and remove -1 padding
- nms_keep = tf.reshape(nms_keep, [-1])
- nms_keep = tf.gather(nms_keep, tf.where(nms_keep > -1)[:, 0])
- # 4. Compute intersection between keep and nms_keep
- keep = tf.sets.set_intersection(tf.expand_dims(keep, 0),
- tf.expand_dims(nms_keep, 0))
- keep = tf.sparse_tensor_to_dense(keep)[0]
- # Keep top detections
- roi_count = config.DETECTION_MAX_INSTANCES
- class_scores_keep = tf.gather(class_scores, keep)
- num_keep = tf.minimum(tf.shape(class_scores_keep)[0], roi_count)
- top_ids = tf.nn.top_k(class_scores_keep, k=num_keep, sorted=True)[1]
- keep = tf.gather(keep, top_ids)
- # Arrange output as [N, (y1, x1, y2, x2, class_id, score)]
- # Coordinates are normalized.
- detections = tf.concat([
- tf.gather(refined_rois, keep),
- tf.to_float(tf.gather(class_ids, keep))[..., tf.newaxis],
- tf.gather(class_scores, keep)[..., tf.newaxis]
- ], axis=1)
- # Pad with zeros if detections < DETECTION_MAX_INSTANCES
- gap = config.DETECTION_MAX_INSTANCES - tf.shape(detections)[0]
- detections = tf.pad(detections, [(0, gap), (0, 0)], "CONSTANT")
- return detections
- def refine_detections_graph_binary(rois, scores, deltas, window, config):
- """
- The binary classification version of refine_detections_graph
- rois: [num_rois, 4]
- scores: [num_rois]
- deltas: [num_rois, 4]
- """
- # bbox deltas
- refined_rois = apply_box_deltas_graph(rois, deltas * config.BBOX_STD_DEV)
- refined_rois = clip_boxes_graph(refined_rois, window)
- y1, x1, y2, x2 = tf.split(refined_rois, 4, axis=1)
- areas = (y2 - y1) * (x2 - x1)
- valid_mask = tf.squeeze(areas > 0, axis=1)
- confidence_mask = scores >= config.DETECTION_MIN_CONFIDENCE
- keep_mask = tf.logical_and(valid_mask, confidence_mask)
- keep = tf.where(keep_mask)[:, 0]
- def true_fn():
- # NMS
- nms_keep = tf.image.non_max_suppression(
- tf.gather(refined_rois, keep),
- tf.gather(scores, keep),
- max_output_size=config.DETECTION_MAX_INSTANCES,
- iou_threshold=config.DETECTION_NMS_THRESHOLD)
- keep_filtered = tf.gather(keep, nms_keep)
- # [N, (y1, x1, y2, x2, class_id, score)]
- # class_id = 1
- detections = tf.concat([
- tf.gather(refined_rois, keep_filtered),
- tf.ones([tf.shape(keep_filtered)[0], 1], dtype=tf.float32), # class_id=1
- tf.gather(scores, keep_filtered)[..., tf.newaxis]
- ], axis=1)
- return detections
- def false_fn():
- detections = tf.zeros([0, 6], dtype=tf.float32)
- return detections
- detections = tf.cond(
- tf.greater(tf.size(keep), 0),
- true_fn,
- false_fn
- )
- gap = config.DETECTION_MAX_INSTANCES - tf.shape(detections)[0]
- detections = tf.pad(detections, [(0, gap), (0, 0)], "CONSTANT")
- return detections
- class DetectionLayer(KE.Layer):
- def __init__(self, config=None, **kwargs):
- super(DetectionLayer, self).__init__(**kwargs)
- self.config = config
- def call(self, inputs):
- rois = inputs[0]
- mrcnn_class = inputs[1]
- mrcnn_bbox = inputs[2]
- image_meta = inputs[3]
- # Get windows of images in normalized coordinates. Windows are the area
- # in the image that excludes the padding.
- # Use the shape of the first image in the batch to normalize the window
- # because we know that all images get resized to the same size.
- m = parse_image_meta_graph(image_meta)
- image_shape = m['image_shape'][0]
- window = norm_boxes_graph(m['window'], image_shape[:2])
- foreground_scores = mrcnn_class[:, :, 1]
- foreground_bbox = mrcnn_bbox[:, :, 1, :]
- # Run detection refinement graph on each item in the batch
- # detections_batch = utils.batch_slice(
- # [rois, mrcnn_class, mrcnn_bbox, window],
- # lambda x, y, w, z: refine_detections_graph(x, y, w, z, self.config),
- # self.config.IMAGES_PER_GPU)
- detections_batch = utils.batch_slice(
- [rois, foreground_scores, foreground_bbox, window],
- lambda x, y, w, z: refine_detections_graph_binary(x, y, w, z, self.config),
- self.config.IMAGES_PER_GPU)
- # Reshape output
- # [batch, num_detections, (y1, x1, y2, x2, class_id, class_score)] in
- # normalized coordinates
- return tf.reshape(
- detections_batch,
- [self.config.BATCH_SIZE, self.config.DETECTION_MAX_INSTANCES, 6])
- def compute_output_shape(self, input_shape):
- return (None, self.config.DETECTION_MAX_INSTANCES, 6)
- # Region Proposal Network (RPN)
- def rpn_graph(feature_map, anchors_per_location, anchor_stride):
- # Shared convolutional base of the RPN
- shared = KL.Conv2D(512, (3, 3), padding='same', activation='relu',
- strides=anchor_stride,
- name='rpn_conv_shared')(feature_map)
- # Anchor Score. [batch, height, width, anchors per location * 2].
- x = KL.Conv2D(2 * anchors_per_location, (1, 1), padding='valid',
- activation='linear', name='rpn_class_raw')(shared)
- # Reshape to [batch, anchors, 2]
- rpn_class_logits = KL.Lambda(
- lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 2]))(x)
- # Softmax on last dimension of BG/FG.
- rpn_probs = KL.Activation(
- "softmax", name="rpn_class_xxx")(rpn_class_logits)
- # Bounding box refinement. [batch, H, W, anchors per location * depth]
- # where depth is [x, y, log(w), log(h)]
- x = KL.Conv2D(anchors_per_location * 4, (1, 1), padding="valid",
- activation='linear', name='rpn_bbox_pred')(shared)
- # Reshape to [batch, anchors, 4]
- rpn_bbox = KL.Lambda(lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 4]))(x)
- return [rpn_class_logits, rpn_probs, rpn_bbox]
- def build_rpn_model(anchor_stride, anchors_per_location, depth):
- input_feature_map = KL.Input(shape=[None, None, depth],
- name="input_rpn_feature_map")
- outputs = rpn_graph(input_feature_map, anchors_per_location, anchor_stride)
- return KM.Model([input_feature_map], outputs, name="rpn_model")
- # Feature Pyramid Network Heads
- def fpn_classifier_graph(rois, feature_maps, image_meta,
- pool_size, num_classes, train_bn=True,
- fc_layers_size=1024):
- """
- num_classes = 2
- Returns:
- logits: [batch, num_rois, NUM_CLASSES] classifier logits (before softmax)
- probs: [batch, num_rois, NUM_CLASSES] classifier probabilities
- bbox_deltas: [batch, num_rois, NUM_CLASSES, (dy, dx, log(dh), log(dw))] Deltas to apply to
- proposal boxes
- """
- # ROI Pooling
- # Shape: [batch, num_rois, POOL_SIZE, POOL_SIZE, channels]
- x = PyramidROIAlign([pool_size, pool_size],
- name="roi_align_classifier")([rois, image_meta] + feature_maps)
- # Two 1024 FC layers (implemented with Conv2D for consistency)
- # Shape:[batch, num_rois, 1, 1, fc_layers_size]
- x = KL.TimeDistributed(KL.Conv2D(fc_layers_size, (pool_size, pool_size), padding="valid"),
- name="mrcnn_class_conv1")(x)
- x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn1')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- x = KL.TimeDistributed(KL.Conv2D(fc_layers_size, (1, 1)),
- name="mrcnn_class_conv2")(x)
- x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn2')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- # Shape:[batch, num_rois, fc_layers_size]
- shared = KL.Lambda(lambda x: K.squeeze(K.squeeze(x, 3), 2),
- name="pool_squeeze")(x)
- # Classifier head
- mrcnn_class_logits = KL.TimeDistributed(KL.Dense(num_classes),
- name='mrcnn_class_logits')(shared)
- mrcnn_probs = KL.TimeDistributed(KL.Activation("softmax"),
- name="mrcnn_class")(mrcnn_class_logits)
- # BBox head
- # [batch, num_rois, NUM_CLASSES * (dy, dx, log(dh), log(dw))]
- x = KL.TimeDistributed(KL.Dense(num_classes * 4, activation='linear'),
- name='mrcnn_bbox_fc')(shared)
- # Reshape to [batch, num_rois, NUM_CLASSES, (dy, dx, log(dh), log(dw))]
- s = K.int_shape(x)
- mrcnn_bbox = KL.Reshape((s[1], num_classes, 4), name="mrcnn_bbox")(x)
- return mrcnn_class_logits, mrcnn_probs, mrcnn_bbox
- def build_fpn_mask_graph(rois, feature_maps, image_meta,
- pool_size, num_classes, train_bn=True):
- """
- Returns: Masks [batch, num_rois, MASK_POOL_SIZE, MASK_POOL_SIZE, NUM_CLASSES]
- """
- # ROI Pooling
- # Shape: [batch, num_rois, MASK_POOL_SIZE, MASK_POOL_SIZE, channels]
- x = PyramidROIAlign([pool_size, pool_size],
- name="roi_align_mask")([rois, image_meta] + feature_maps)
- # Conv layers
- x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
- name="mrcnn_mask_conv1")(x)
- x = KL.TimeDistributed(BatchNorm(),
- name='mrcnn_mask_bn1')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
- name="mrcnn_mask_conv2")(x)
- x = KL.TimeDistributed(BatchNorm(),
- name='mrcnn_mask_bn2')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
- name="mrcnn_mask_conv3")(x)
- x = KL.TimeDistributed(BatchNorm(),
- name='mrcnn_mask_bn3')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
- name="mrcnn_mask_conv4")(x)
- x = KL.TimeDistributed(BatchNorm(),
- name='mrcnn_mask_bn4')(x, training=train_bn)
- x = KL.Activation('relu')(x)
- # Shape: [batch, num_rois, 2*MASK_POOL_SIZE, MASK_POOL_SIZE, channels]
- x = KL.TimeDistributed(KL.Conv2DTranspose(256, (2, 2), strides=2, activation="relu"),
- name="mrcnn_mask_deconv")(x)
- x = KL.TimeDistributed(KL.Conv2D(num_classes, (1, 1), strides=1, activation="sigmoid"),
- name="mrcnn_mask")(x)
- return x
- # Loss Functions
- def smooth_l1_loss(y_true, y_pred):
- diff = K.abs(y_true - y_pred)
- less_than_one = K.cast(K.less(diff, 1.0), "float32")
- loss = (less_than_one * 0.5 * diff**2) + (1 - less_than_one) * (diff - 0.5)
- return loss
- def rpn_class_loss_graph(rpn_match, rpn_class_logits):
- # Squeeze last dim to simplify
- rpn_match = tf.squeeze(rpn_match, -1)
- # Get anchor classes. Convert the -1/+1 match to 0/1 values.
- anchor_class = K.cast(K.equal(rpn_match, 1), tf.int32)
- # Positive and Negative anchors contribute to the loss,
- # but neutral anchors (match value = 0) don't.
- indices = tf.where(K.not_equal(rpn_match, 0))
- # Pick rows that contribute to the loss and filter out the rest.
- rpn_class_logits = tf.gather_nd(rpn_class_logits, indices)
- anchor_class = tf.gather_nd(anchor_class, indices)
- # Cross entropy loss
- loss = K.sparse_categorical_crossentropy(target=anchor_class,
- output=rpn_class_logits,
- from_logits=True)
- loss = K.switch(tf.size(loss) > 0, K.mean(loss), tf.constant(0.0))
- return loss
- def rpn_bbox_loss_graph(config, target_bbox, rpn_match, rpn_bbox):
- """
- config: the model config object.
- target_bbox: [batch, max positive anchors, (dy, dx, log(dh), log(dw))].
- Uses 0 padding to fill in unsed bbox deltas.
- rpn_match: [batch, anchors, 1]. Anchor match type. 1=positive,
- -1=negative, 0=neutral anchor.
- rpn_bbox: [batch, anchors, (dy, dx, log(dh), log(dw))]
- """
- # Positive anchors contribute to the loss, but negative and
- # neutral anchors (match value of 0 or -1) don't.
- rpn_match = K.squeeze(rpn_match, -1)
- indices = tf.where(K.equal(rpn_match, 1))
- # Pick bbox deltas that contribute to the loss
- rpn_bbox = tf.gather_nd(rpn_bbox, indices)
- # Trim target bounding box deltas to the same length as rpn_bbox.
- batch_counts = K.sum(K.cast(K.equal(rpn_match, 1), tf.int32), axis=1)
- target_bbox = batch_pack_graph(target_bbox, batch_counts,
- config.IMAGES_PER_GPU)
- loss = smooth_l1_loss(target_bbox, rpn_bbox)
- loss = K.switch(tf.size(loss) > 0, K.mean(loss), tf.constant(0.0))
- return loss
- def mrcnn_class_loss_graph(target_class_ids, pred_class_logits,
- active_class_ids):
- """
- All predictions are included in the loss calculation
- """
- target_class_ids = tf.cast(target_class_ids, 'int64')
- # Standard cross-entropy loss
- loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
- labels=target_class_ids, logits=pred_class_logits)
- # Create a mask for valid ROIs (target_class_ids >= 0)
- valid_mask = tf.cast(tf.greater_equal(target_class_ids, 0), tf.float32)
- # use mask
- loss = loss * valid_mask
- # Calculate the average loss (calculate only for valid ROIs)
- total_loss = tf.reduce_sum(loss)
- num_valid = tf.reduce_sum(valid_mask)
- loss = tf.cond(tf.greater(num_valid, 0),
- lambda: total_loss / num_valid,
- lambda: tf.constant(0.0))
- return loss
- def mrcnn_bbox_loss_graph(target_bbox, target_class_ids, pred_bbox):
- """
- target_bbox: [batch, num_rois, (dy, dx, log(dh), log(dw))]
- target_class_ids: [batch, num_rois]. Integer class IDs.
- pred_bbox: [batch, num_rois, num_classes, (dy, dx, log(dh), log(dw))]
- """
- # Reshape to merge batch and roi dimensions for simplicity.
- target_class_ids = K.reshape(target_class_ids, (-1,))
- target_bbox = K.reshape(target_bbox, (-1, 4))
- pred_bbox = K.reshape(pred_bbox, (-1, K.int_shape(pred_bbox)[2], 4))
- # Only positive ROIs contribute to the loss. And only
- # the right class_id of each ROI. Get their indices.
- positive_roi_ix = tf.where(target_class_ids > 0)[:, 0]
- positive_roi_class_ids = tf.cast(
- tf.gather(target_class_ids, positive_roi_ix), tf.int64)
- indices = tf.stack([positive_roi_ix, positive_roi_class_ids], axis=1)
- # Gather the deltas (predicted and true) that contribute to loss
- target_bbox = tf.gather(target_bbox, positive_roi_ix)
- pred_bbox = tf.gather_nd(pred_bbox, indices)
- # Smooth-L1 Loss
- loss = K.switch(tf.size(target_bbox) > 0,
- smooth_l1_loss(y_true=target_bbox, y_pred=pred_bbox),
- tf.constant(0.0))
- loss = K.mean(loss)
- return loss
- def mrcnn_mask_loss_graph(target_masks, target_class_ids, pred_masks):
- """
- target_masks: [batch, num_rois, height, width].
- A float32 tensor of values 0 or 1. Uses zero padding to fill array.
- target_class_ids: [batch, num_rois]. Integer class IDs. Zero padded.
- pred_masks: [batch, proposals, height, width, num_classes] float32 tensor
- with values from 0 to 1.
- """
- # Reshape for simplicity. Merge first two dimensions into one.
- target_class_ids = K.reshape(target_class_ids, (-1,))
- mask_shape = tf.shape(target_masks)
- target_masks = K.reshape(target_masks, (-1, mask_shape[2], mask_shape[3]))
- pred_shape = tf.shape(pred_masks)
- pred_masks = K.reshape(pred_masks,
- (-1, pred_shape[2], pred_shape[3], pred_shape[4]))
- # Permute predicted masks to [N, num_classes, height, width]
- pred_masks = tf.transpose(pred_masks, [0, 3, 1, 2])
- # Only positive ROIs contribute to the loss. And only
- # the class specific mask of each ROI.
- positive_ix = tf.where(target_class_ids > 0)[:, 0]
- positive_class_ids = tf.cast(
- tf.gather(target_class_ids, positive_ix), tf.int64)
- indices = tf.stack([positive_ix, positive_class_ids], axis=1)
- # Gather the masks (predicted and true) that contribute to loss
- y_true = tf.gather(target_masks, positive_ix)
- y_pred = tf.gather_nd(pred_masks, indices)
- # Compute binary cross entropy. If no positive ROIs, then return 0.
- # shape: [batch, roi, num_classes]
- loss = K.switch(tf.size(y_true) > 0,
- K.binary_crossentropy(target=y_true, output=y_pred),
- tf.constant(0.0))
- loss = K.mean(loss)
- return loss
- # Data Generator
- def load_image_gt(dataset, config, image_id, augment=False, augmentation=None,
- use_mini_mask=False):
- # Load image and mask
- image = dataset.load_image(image_id)
- mask, class_ids = dataset.load_mask(image_id)
- box, class_ids = dataset.load_box(image_id)
- original_shape = image.shape
- image, window, scale, padding, crop = utils.resize_image(
- image,
- min_dim=config.IMAGE_MIN_DIM,
- min_scale=config.IMAGE_MIN_SCALE,
- max_dim=config.IMAGE_MAX_DIM,
- mode=config.IMAGE_RESIZE_MODE)
- mask = utils.resize_mask(mask, scale, padding, crop)
- # Random horizontal flips.
- if augment:
- logging.warning("'augment' is deprecated. Use 'augmentation' instead.")
- if random.randint(0, 1):
- image = np.fliplr(image)
- mask = np.fliplr(mask)
- # Augmentation
- if augmentation:
- import imgaug
- # Augmenters that are safe to apply to masks
- MASK_AUGMENTERS = ["Sequential", "SomeOf", "OneOf", "Sometimes",
- "Fliplr", "Flipud", "CropAndPad",
- "Affine", "PiecewiseAffine"]
- def hook(images, augmenter, parents, default):
- """Determines which augmenters to apply to masks."""
- return augmenter.__class__.__name__ in MASK_AUGMENTERS
- # Store shapes before augmentation to compare
- image_shape = image.shape
- mask_shape = mask.shape
- # Make augmenters deterministic to apply similarly to images and masks
- det = augmentation.to_deterministic()
- image = det.augment_image(image)
- # Change mask to np.uint8 because imgaug doesn't support np.bool
- mask = det.augment_image(mask.astype(np.uint8),
- hooks=imgaug.HooksImages(activator=hook))
- # Verify that shapes didn't change
- assert image.shape == image_shape, "Augmentation shouldn't change image size"
- assert mask.shape == mask_shape, "Augmentation shouldn't change mask size"
- # Change mask back to bool
- mask = mask.astype(np.bool)
- # Note that some boxes might be all zeros if the corresponding mask got cropped out.
- # and here is to filter them out
- _idx = np.sum(mask, axis=(0, 1)) > 0
- mask = mask[:, :, _idx]
- class_ids = class_ids[_idx]
- # Bounding boxes.
- # bbox: [num_instances, (y1, x1, y2, x2)]
- # bbox = utils.extract_bboxes(mask)
- bbox = utils.extract_bboxes_1(box)
- # Active classes
- # Different datasets have different classes, so track the
- # classes supported in the dataset of this image.
- active_class_ids = np.zeros([dataset.num_classes], dtype=np.int32)
- source_class_ids = dataset.source_class_ids[dataset.image_info[image_id]["source"]]
- active_class_ids[source_class_ids] = 1
- # Resize masks to smaller size to reduce memory usage
- if use_mini_mask:
- mask = utils.minimize_mask(bbox, mask, config.MINI_MASK_SHAPE)
- # Image meta data
- image_meta = compose_image_meta(image_id, original_shape, image.shape,
- window, scale, active_class_ids)
- return image, image_meta, class_ids, bbox, mask
- def load_image_gt_no_mask(dataset, config, image_id, augment=False, augmentation=None):
- """Load only the image and box, not the mask – for use with the optimised version of data_generator"""
- image = dataset.load_image(image_id)
- boxes, class_ids = dataset.load_box(image_id)
- if boxes is None or len(boxes) == 0 or (hasattr(boxes, 'shape') and boxes.shape[0] == 0):
- boxes = np.zeros((0, 4), dtype=np.int32)
- class_ids = np.zeros((0,), dtype=np.int32)
- elif hasattr(boxes, 'shape') and len(boxes.shape) == 2:
- if boxes.shape[0] == 4 and boxes.shape[1] > 0:
- boxes = boxes.T
- elif boxes.shape[1] != 4:
- if boxes.shape[0] == 4 and boxes.shape[1] == 0:
- boxes = np.zeros((0, 4), dtype=np.int32)
- class_ids = np.zeros((0,), dtype=np.int32)
- else:
- print(f"Warning: Unexpected boxes shape {boxes.shape} for image {image_id}")
- boxes = np.zeros((0, 4), dtype=np.int32)
- class_ids = np.zeros((0,), dtype=np.int32)
- else:
- boxes = np.zeros((0, 4), dtype=np.int32)
- class_ids = np.zeros((0,), dtype=np.int32)
- original_shape = image.shape
- # Resize the image
- image, window, scale, padding, crop = utils.resize_image(
- image,
- min_dim=config.IMAGE_MIN_DIM,
- min_scale=config.IMAGE_MIN_SCALE,
- max_dim=config.IMAGE_MAX_DIM,
- mode=config.IMAGE_RESIZE_MODE
- )
- if augmentation:
- image = augmentation(image)
- if len(boxes) > 0:
- # Filter out invalid boxes (ensuring that the coordinates are valid and correctly formatted)
- valid_boxes = []
- valid_class_ids = []
- for i in range(len(boxes)):
- box = boxes[i]
- if len(box) >= 4 and box[2] > box[0] and box[3] > box[1]:
- valid_boxes.append(box)
- valid_class_ids.append(class_ids[i])
- if valid_boxes:
- boxes = np.array(valid_boxes, dtype=np.int32)
- class_ids = np.array(valid_class_ids, dtype=np.int32)
- # padding : [(top, bottom), (left, right), (0, 0)]
- top_pad, bottom_pad = padding[0]
- left_pad, right_pad = padding[1]
- boxes = boxes * scale + np.array([top_pad, left_pad, top_pad, left_pad])
- boxes = boxes.astype(np.int32)
- else:
- boxes = np.zeros((0, 4), dtype=np.int32)
- class_ids = np.zeros((0,), dtype=np.int32)
- else:
- boxes = np.zeros((0, 4), dtype=np.int32)
- class_ids = np.zeros((0,), dtype=np.int32)
- # Active classes
- active_class_ids = np.zeros([dataset.num_classes], dtype=np.int32)
- source_class_ids = dataset.source_class_ids[dataset.image_info[image_id]["source"]]
- active_class_ids[source_class_ids] = 1
- # Image meta data
- image_meta = compose_image_meta(
- image_id, original_shape, image.shape,
- window, scale, active_class_ids
- )
- return image, image_meta, class_ids, boxes
- def build_detection_targets(rpn_rois, gt_class_ids, gt_boxes, gt_masks, config):
- assert rpn_rois.shape[0] > 0
- assert gt_class_ids.dtype == np.int32, "Expected int but got {}".format(
- gt_class_ids.dtype)
- assert gt_boxes.dtype == np.int32, "Expected int but got {}".format(
- gt_boxes.dtype)
- assert gt_masks.dtype == np.bool_, "Expected bool but got {}".format(
- gt_masks.dtype)
- # It's common to add GT Boxes to ROIs but we don't do that here because
- # according to XinLei Chen's paper, it doesn't help.
- # Trim empty padding in gt_boxes and gt_masks parts
- instance_ids = np.where(gt_class_ids > 0)[0]
- assert instance_ids.shape[0] > 0, "Image must contain instances."
- gt_class_ids = gt_class_ids[instance_ids]
- gt_boxes = gt_boxes[instance_ids]
- gt_masks = gt_masks[:, :, instance_ids]
- # Compute areas of ROIs and ground truth boxes.
- rpn_roi_area = (rpn_rois[:, 2] - rpn_rois[:, 0]) * \
- (rpn_rois[:, 3] - rpn_rois[:, 1])
- gt_box_area = (gt_boxes[:, 2] - gt_boxes[:, 0]) * \
- (gt_boxes[:, 3] - gt_boxes[:, 1])
- # Compute overlaps [rpn_rois, gt_boxes]
- overlaps = np.zeros((rpn_rois.shape[0], gt_boxes.shape[0]))
- for i in range(overlaps.shape[1]):
- gt = gt_boxes[i]
- overlaps[:, i] = utils.compute_iou(
- gt, rpn_rois, gt_box_area[i], rpn_roi_area)
- # Assign ROIs to GT boxes
- rpn_roi_iou_argmax = np.argmax(overlaps, axis=1)
- rpn_roi_iou_max = overlaps[np.arange(
- overlaps.shape[0]), rpn_roi_iou_argmax]
- # GT box assigned to each ROI
- rpn_roi_gt_boxes = gt_boxes[rpn_roi_iou_argmax]
- rpn_roi_gt_class_ids = gt_class_ids[rpn_roi_iou_argmax]
- # Positive ROIs are those with >= 0.5 IoU with a GT box.
- fg_ids = np.where(rpn_roi_iou_max > 0.5)[0]
- # Negative ROIs are those with max IoU 0.1-0.5 (hard example mining)
- # bg_ids = np.where((rpn_roi_iou_max >= 0.1) & (rpn_roi_iou_max < 0.5))[0]
- bg_ids = np.where(rpn_roi_iou_max < 0.5)[0]
- # Subsample ROIs. Aim for 33% foreground.
- # FG
- fg_roi_count = int(config.TRAIN_ROIS_PER_IMAGE * config.ROI_POSITIVE_RATIO)
- if fg_ids.shape[0] > fg_roi_count:
- keep_fg_ids = np.random.choice(fg_ids, fg_roi_count, replace=False)
- else:
- keep_fg_ids = fg_ids
- # BG
- remaining = config.TRAIN_ROIS_PER_IMAGE - keep_fg_ids.shape[0]
- if bg_ids.shape[0] > remaining:
- keep_bg_ids = np.random.choice(bg_ids, remaining, replace=False)
- else:
- keep_bg_ids = bg_ids
- # Combine indices of ROIs to keep
- keep = np.concatenate([keep_fg_ids, keep_bg_ids])
- # Need more?
- remaining = config.TRAIN_ROIS_PER_IMAGE - keep.shape[0]
- if remaining > 0:
- # There is a small chance we have neither fg nor bg samples.
- if keep.shape[0] == 0:
- # Pick bg regions with easier IoU threshold
- bg_ids = np.where(rpn_roi_iou_max < 0.5)[0]
- assert bg_ids.shape[0] >= remaining
- keep_bg_ids = np.random.choice(bg_ids, remaining, replace=False)
- assert keep_bg_ids.shape[0] == remaining
- keep = np.concatenate([keep, keep_bg_ids])
- else:
- # Fill the rest with repeated bg rois.
- keep_extra_ids = np.random.choice(
- keep_bg_ids, remaining, replace=True)
- keep = np.concatenate([keep, keep_extra_ids])
- assert keep.shape[0] == config.TRAIN_ROIS_PER_IMAGE, \
- "keep doesn't match ROI batch size {}, {}".format(
- keep.shape[0], config.TRAIN_ROIS_PER_IMAGE)
- # Reset the gt boxes assigned to BG ROIs.
- rpn_roi_gt_boxes[keep_bg_ids, :] = 0
- rpn_roi_gt_class_ids[keep_bg_ids] = 0
- # For each kept ROI, assign a class_id, and for FG ROIs also add bbox refinement.
- rois = rpn_rois[keep]
- roi_gt_boxes = rpn_roi_gt_boxes[keep]
- roi_gt_class_ids = rpn_roi_gt_class_ids[keep]
- roi_gt_assignment = rpn_roi_iou_argmax[keep]
- # Class-aware bbox deltas. [y, x, log(h), log(w)]
- bboxes = np.zeros((config.TRAIN_ROIS_PER_IMAGE,
- config.NUM_CLASSES, 4), dtype=np.float32)
- pos_ids = np.where(roi_gt_class_ids > 0)[0]
- bboxes[pos_ids, roi_gt_class_ids[pos_ids]] = utils.box_refinement(
- rois[pos_ids], roi_gt_boxes[pos_ids, :4])
- # Normalize bbox refinements
- bboxes /= config.BBOX_STD_DEV
- # Generate class-specific target masks
- masks = np.zeros((config.TRAIN_ROIS_PER_IMAGE, config.MASK_SHAPE[0], config.MASK_SHAPE[1], config.NUM_CLASSES),
- dtype=np.float32)
- for i in pos_ids:
- class_id = roi_gt_class_ids[i]
- assert class_id > 0, "class id must be greater than 0"
- gt_id = roi_gt_assignment[i]
- class_mask = gt_masks[:, :, gt_id]
- if config.USE_MINI_MASK:
- # Create a mask placeholder, the size of the image
- placeholder = np.zeros(config.IMAGE_SHAPE[:2], dtype=bool)
- # GT box
- gt_y1, gt_x1, gt_y2, gt_x2 = gt_boxes[gt_id]
- gt_w = gt_x2 - gt_x1
- gt_h = gt_y2 - gt_y1
- # Resize mini mask to size of GT box
- placeholder[gt_y1:gt_y2, gt_x1:gt_x2] = \
- np.round(utils.resize(class_mask, (gt_h, gt_w))).astype(bool)
- # Place the mini batch in the placeholder
- class_mask = placeholder
- # Pick part of the mask and resize it
- y1, x1, y2, x2 = rois[i].astype(np.int32)
- m = class_mask[y1:y2, x1:x2]
- mask = utils.resize(m, config.MASK_SHAPE)
- masks[i, :, :, class_id] = mask
- return rois, roi_gt_class_ids, bboxes, masks
- def build_rpn_targets(image_shape, anchors, gt_class_ids, gt_boxes, config):
- # RPN Match: 1 = positive anchor, -1 = negative anchor, 0 = neutral
- rpn_match = np.zeros([anchors.shape[0]], dtype=np.int32)
- # RPN bounding boxes: [max anchors per image, (dy, dx, log(dh), log(dw))]
- rpn_bbox = np.zeros((config.RPN_TRAIN_ANCHORS_PER_IMAGE, 4))
- # Handle COCO crowds
- crowd_ix = np.where(gt_class_ids < 0)[0]
- if crowd_ix.shape[0] > 0:
- # Filter out crowds from ground truth class IDs and boxes
- non_crowd_ix = np.where(gt_class_ids > 0)[0]
- crowd_boxes = gt_boxes[crowd_ix]
- gt_class_ids = gt_class_ids[non_crowd_ix]
- gt_boxes = gt_boxes[non_crowd_ix]
- # Compute overlaps with crowd boxes [anchors, crowds]
- crowd_overlaps = utils.compute_overlaps(anchors, crowd_boxes)
- crowd_iou_max = np.amax(crowd_overlaps, axis=1)
- no_crowd_bool = (crowd_iou_max < 0.001)
- else:
- # All anchors don't intersect a crowd
- no_crowd_bool = np.ones([anchors.shape[0]], dtype=bool)
- # Compute overlaps [num_anchors, num_gt_boxes] 计算先验框和gt_box的重合程度
- overlaps = utils.compute_overlaps(anchors, gt_boxes)
- # Match anchors to GT Boxes
- # If an anchor overlaps a GT box with IoU >= 0.7 then it's positive.
- # If an anchor overlaps a GT box with IoU < 0.3 then it's negative.
- # 1. Set negative anchors first. They get overwritten below if a GT box is
- # matched to them. Skip boxes in crowd areas.
- anchor_iou_argmax = np.argmax(overlaps, axis=1)
- anchor_iou_max = overlaps[np.arange(overlaps.shape[0]), anchor_iou_argmax]
- rpn_match[(anchor_iou_max < 0.3) & (no_crowd_bool)] = -1
- # 2. Set an anchor for each GT box (regardless of IoU value).
- # If multiple anchors have the same IoU match all of them
- gt_iou_argmax = np.argwhere(overlaps == np.max(overlaps, axis=0))[:,0]
- rpn_match[gt_iou_argmax] = 1
- # 3. Set anchors with high overlap as positive.
- rpn_match[anchor_iou_max >= 0.7] = 1
- # Subsample to balance positive and negative anchors
- # Don't let positives be more than half the anchors
- ids = np.where(rpn_match == 1)[0]
- extra = len(ids) - (config.RPN_TRAIN_ANCHORS_PER_IMAGE // 2)
- if extra > 0:
- # Reset the extra ones to neutral
- ids = np.random.choice(ids, extra, replace=False)
- rpn_match[ids] = 0
- # Same for negative proposals
- ids = np.where(rpn_match == -1)[0]
- extra = len(ids) - (config.RPN_TRAIN_ANCHORS_PER_IMAGE -
- np.sum(rpn_match == 1))
- if extra > 0:
- # Rest the extra ones to neutral
- ids = np.random.choice(ids, extra, replace=False)
- rpn_match[ids] = 0
- # For positive anchors, compute shift and scale needed to transform them
- # to match the corresponding GT boxes.
- ids = np.where(rpn_match == 1)[0]
- ix = 0 # index into rpn_bbox
- for i, a in zip(ids, anchors[ids]):
- # Closest gt box (it might have IoU < 0.7)
- gt = gt_boxes[anchor_iou_argmax[i]]
- # Convert coordinates to center plus width/height.
- # GT Box
- gt_h = gt[2] - gt[0]
- gt_w = gt[3] - gt[1]
- gt_center_y = gt[0] + 0.5 * gt_h
- gt_center_x = gt[1] + 0.5 * gt_w
- # Anchor
- a_h = a[2] - a[0]
- a_w = a[3] - a[1]
- a_center_y = a[0] + 0.5 * a_h
- a_center_x = a[1] + 0.5 * a_w
- rpn_bbox[ix] = [
- (gt_center_y - a_center_y) / a_h,
- (gt_center_x - a_center_x) / a_w,
- np.log(gt_h / a_h),
- np.log(gt_w / a_w),
- ]
- # Normalize
- rpn_bbox[ix] /= config.RPN_BBOX_STD_DEV
- ix += 1
- return rpn_match, rpn_bbox
- def generate_random_rois(image_shape, count, gt_class_ids, gt_boxes):
- """
- image_shape: [Height, Width, Depth]
- count: Number of ROIs to generate
- gt_class_ids: [N] Integer ground truth class IDs
- gt_boxes: [N, (y1, x1, y2, x2)] Ground truth boxes in pixels.
- Returns: [count, (y1, x1, y2, x2)] ROI boxes in pixels.
- """
- # placeholder
- rois = np.zeros((count, 4), dtype=np.int32)
- # Generate random ROIs around GT boxes (90% of count)
- rois_per_box = int(0.9 * count / gt_boxes.shape[0])
- for i in range(gt_boxes.shape[0]):
- gt_y1, gt_x1, gt_y2, gt_x2 = gt_boxes[i]
- h = gt_y2 - gt_y1
- w = gt_x2 - gt_x1
- # random boundaries
- r_y1 = max(gt_y1 - h, 0)
- r_y2 = min(gt_y2 + h, image_shape[0])
- r_x1 = max(gt_x1 - w, 0)
- r_x2 = min(gt_x2 + w, image_shape[1])
- # To avoid generating boxes with zero area, we generate double what
- # we need and filter out the extra. If we get fewer valid boxes
- # than we need, we loop and try again.
- while True:
- y1y2 = np.random.randint(r_y1, r_y2, (rois_per_box * 2, 2))
- x1x2 = np.random.randint(r_x1, r_x2, (rois_per_box * 2, 2))
- # Filter out zero area boxes
- threshold = 1
- y1y2 = y1y2[np.abs(y1y2[:, 0] - y1y2[:, 1]) >=
- threshold][:rois_per_box]
- x1x2 = x1x2[np.abs(x1x2[:, 0] - x1x2[:, 1]) >=
- threshold][:rois_per_box]
- if y1y2.shape[0] == rois_per_box and x1x2.shape[0] == rois_per_box:
- break
- # Sort on axis 1 to ensure x1 <= x2 and y1 <= y2 and then reshape
- # into x1, y1, x2, y2 order
- x1, x2 = np.split(np.sort(x1x2, axis=1), 2, axis=1)
- y1, y2 = np.split(np.sort(y1y2, axis=1), 2, axis=1)
- box_rois = np.hstack([y1, x1, y2, x2])
- rois[rois_per_box * i:rois_per_box * (i + 1)] = box_rois
- # Generate random ROIs anywhere in the image (10% of count)
- remaining_count = count - (rois_per_box * gt_boxes.shape[0])
- # To avoid generating boxes with zero area, we generate double what
- # we need and filter out the extra. If we get fewer valid boxes
- # than we need, we loop and try again.
- while True:
- y1y2 = np.random.randint(0, image_shape[0], (remaining_count * 2, 2))
- x1x2 = np.random.randint(0, image_shape[1], (remaining_count * 2, 2))
- # Filter out zero area boxes
- threshold = 1
- y1y2 = y1y2[np.abs(y1y2[:, 0] - y1y2[:, 1]) >=
- threshold][:remaining_count]
- x1x2 = x1x2[np.abs(x1x2[:, 0] - x1x2[:, 1]) >=
- threshold][:remaining_count]
- if y1y2.shape[0] == remaining_count and x1x2.shape[0] == remaining_count:
- break
- # Sort on axis 1 to ensure x1 <= x2 and y1 <= y2 and then reshape
- # into x1, y1, x2, y2 order
- x1, x2 = np.split(np.sort(x1x2, axis=1), 2, axis=1)
- y1, y2 = np.split(np.sort(y1y2, axis=1), 2, axis=1)
- global_rois = np.hstack([y1, x1, y2, x2])
- rois[-remaining_count:] = global_rois
- return rois
- def data_generator(dataset, config, shuffle=True, augment=False, augmentation=None,
- random_rois=0, batch_size=1, detection_targets=False,
- no_augmentation_sources=None):
- b = 0 # batch item index
- image_index = -1
- image_ids = np.copy(dataset.image_ids)
- error_count = 0
- # Pre-compute anchors (calculate only once)
- backbone_shapes = compute_backbone_shapes(config, config.IMAGE_SHAPE)
- anchors = utils.generate_pyramid_anchors(
- config.RPN_ANCHOR_SCALES,
- config.RPN_ANCHOR_RATIOS,
- backbone_shapes,
- config.BACKBONE_STRIDES,
- config.RPN_ANCHOR_STRIDE
- )
- anchor_count = anchors.shape[0]
- # Pre-allocate the batch array (to avoid reallocation)
- batch_arrays = None
- # Keras requires a generator to run indefinitely.
- while True:
- try:
- # Increment index to pick next image. Shuffle if at the start of an epoch.
- image_index = (image_index + 1) % len(image_ids)
- if shuffle and image_index == 0:
- np.random.shuffle(image_ids)
- # Get GT bounding boxes and masks for image.
- image_id = image_ids[image_index]
- try:
- image, image_meta, gt_class_ids, gt_boxes = \
- load_image_gt_no_mask(dataset, config, image_id)
- except (IndexError, ValueError, TypeError) as e:
- logging.warning("Skipping image {} due to error: {}".format(
- dataset.image_info[image_id]['path'], e))
- continue
- # Skip images that have no instances
- if gt_boxes.shape[0] == 0:
- continue
- # RPN Targets
- rpn_match, rpn_bbox = build_rpn_targets(
- image.shape, anchors, gt_class_ids, gt_boxes, config
- )
- # Allocate the batch array only on the first occasion or when the shape changes
- if b == 0:
- if batch_arrays is None or batch_arrays['image_shape'] != image.shape:
- batch_image_meta = np.zeros(
- (batch_size,) + image_meta.shape, dtype=image_meta.dtype)
- batch_rpn_match = np.zeros(
- [batch_size, anchor_count, 1], dtype=rpn_match.dtype)
- batch_rpn_bbox = np.zeros(
- [batch_size, config.RPN_TRAIN_ANCHORS_PER_IMAGE, 4],
- dtype=rpn_bbox.dtype)
- batch_images = np.zeros(
- (batch_size,) + image.shape, dtype=np.float32)
- batch_gt_class_ids = np.zeros(
- (batch_size, config.MAX_GT_INSTANCES), dtype=np.int32)
- batch_gt_boxes = np.zeros(
- (batch_size, config.MAX_GT_INSTANCES, 4), dtype=np.int32)
- batch_arrays = {
- 'image_meta': batch_image_meta,
- 'rpn_match': batch_rpn_match,
- 'rpn_bbox': batch_rpn_bbox,
- 'images': batch_images,
- 'gt_class_ids': batch_gt_class_ids,
- 'gt_boxes': batch_gt_boxes,
- 'image_shape': image.shape
- }
- else:
- batch_arrays['image_meta'].fill(0)
- batch_arrays['rpn_match'].fill(0)
- batch_arrays['rpn_bbox'].fill(0)
- batch_arrays['images'].fill(0)
- batch_arrays['gt_class_ids'].fill(0)
- batch_arrays['gt_boxes'].fill(0)
- # Limit the number of GTs to prevent them from going out of bounds
- gt_count = min(gt_class_ids.shape[0], config.MAX_GT_INSTANCES)
- # Add to batch
- batch_arrays['image_meta'][b] = image_meta
- batch_arrays['rpn_match'][b] = rpn_match[:, np.newaxis]
- batch_arrays['rpn_bbox'][b] = rpn_bbox
- batch_arrays['images'][b] = mold_image(image.astype(np.float32), config)
- batch_arrays['gt_class_ids'][b, :gt_count] = gt_class_ids[:gt_count]
- batch_arrays['gt_boxes'][b, :gt_count] = gt_boxes[:gt_count]
- b += 1
- # Batch full
- if b >= batch_size:
- inputs = [
- batch_arrays['images'],
- batch_arrays['image_meta'],
- batch_arrays['rpn_match'],
- batch_arrays['rpn_bbox'],
- batch_arrays['gt_class_ids'],
- batch_arrays['gt_boxes']
- ]
- outputs = []
- yield inputs, outputs
- # start a new batch
- b = 0
- except (GeneratorExit, KeyboardInterrupt):
- raise
- except Exception as e:
- # Log it and skip the image
- logging.exception("Error processing image {}".format(
- dataset.image_info[image_id]))
- error_count += 1
- if error_count > 5:
- raise
- # Loss save 预先设置list
- epoch_list = []
- tra_loss_list = []
- tra1_loss_list = []
- tra2_loss_list = []
- tra3_loss_list = []
- tra4_loss_list = []
- val_loss_list = []
- val1_loss_list = []
- val2_loss_list = []
- val3_loss_list = []
- val4_loss_list = []
- def expand_dim_1(x):
- x1 = K.expand_dims(x, axis=-1)
- return x1
- # MaskRCNN Class
- def per_level_enhancement_block(C2, C3, C4, config):
- """
- Independent enhancement of each layer: Generate spatial attention to enhance each layer individually
- Output: Enhanced C2, C3 and C4 (retaining original dimensions and number of channels)
- """
- c2_shape = tf.shape(C2)
- c3_shape = tf.shape(C3)
- c4_shape = tf.shape(C4)
- # Merge them into a single C4 size
- target_h = c4_shape[1]
- target_w = c4_shape[2]
- # Resize
- c2_resized = KL.Lambda(
- lambda x: tf.image.resize_images(x, (target_h, target_w), method=tf.image.ResizeMethod.BILINEAR),
- name='per_resize_c2'
- )(C2)
- c3_resized = KL.Lambda(
- lambda x: tf.image.resize_images(x, (target_h, target_w), method=tf.image.ResizeMethod.BILINEAR),
- name='per_resize_c3'
- )(C3)
- c2_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='per_c2_proj')(c2_resized)
- c3_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='per_c3_proj')(c3_resized)
- c4_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='per_c4_proj')(C4)
- # Fusion-based spatial attention
- fused = KL.Concatenate(axis=-1, name='per_concat')([c2_proj, c3_proj, c4_proj])
- fused = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='per_fusion')(fused)
- # Generate single-channel spatial attention
- spatial_attention = KL.Conv2D(
- 1, (3, 3), padding='same', activation='sigmoid', name='per_spatial_att'
- )(fused)
- # Apply attention at each level
- att_c2 = KL.Lambda(
- lambda x: tf.image.resize_images(x, (c2_shape[1], c2_shape[2]), method=tf.image.ResizeMethod.BILINEAR),
- name='per_att_to_c2'
- )(spatial_attention)
- att_c3 = KL.Lambda(
- lambda x: tf.image.resize_images(x, (c3_shape[1], c3_shape[2]), method=tf.image.ResizeMethod.BILINEAR),
- name='per_att_to_c3'
- )(spatial_attention)
- att_c4 = spatial_attention
- # Residual Connection Enhancement
- enhanced_C2 = KL.Add(name='per_enhanced_c2')([C2, KL.Multiply()([C2, att_c2])])
- enhanced_C3 = KL.Add(name='per_enhanced_c3')([C3, KL.Multiply()([C3, att_c3])])
- enhanced_C4 = KL.Add(name='per_enhanced_c4')([C4, KL.Multiply()([C4, att_c4])])
- return enhanced_C2, enhanced_C3, enhanced_C4
- def top_level_fusion_block(C2, C3, C4, C5, config):
- """
- Top-level fusion enhancement: After fusing the information from C2–C4, enhance P5
- Output: The enhanced P5 (retaining the dimensions of P5)
- """
- # c2_shape = tf.shape(C2)
- # c3_shape = tf.shape(C3)
- c4_shape = tf.shape(C4)
- target_h = c4_shape[1]
- target_w = c4_shape[2]
- # Adjust C2 and C3 to match the dimensions of C4
- c2_down = KL.Lambda(
- lambda x: tf.image.resize_images(x, (target_h, target_w), method=tf.image.ResizeMethod.BILINEAR),
- name='top_resize_c2'
- )(C2)
- c3_down = KL.Lambda(
- lambda x: tf.image.resize_images(x, (target_h, target_w), method=tf.image.ResizeMethod.BILINEAR),
- name='top_resize_c3'
- )(C3)
- c4_current = C4
- # Set the number of channels to TOP_DOWN_PYRAMID_SIZE
- c2_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='top_c2_proj')(c2_down)
- c3_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='top_c3_proj')(c3_down)
- c4_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='top_c4_proj')(c4_current)
- # Feature fusion
- fused = KL.Concatenate(axis=-1, name='top_concat')([c2_proj, c3_proj, c4_proj])
- fused = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='top_fusion_1')(fused)
- attention_3ch = KL.Conv2D(
- 3, (3, 3), padding='same', activation='sigmoid', name='top_attention'
- )(fused) # [B, H/16, W/16, 3]
- # Separate the attention maps and apply them to the corresponding features
- att_c2_raw = KL.Lambda(lambda x: x[:, :, :, 0:1], name='top_att_c2_slice')(attention_3ch)
- att_c3_raw = KL.Lambda(lambda x: x[:, :, :, 1:2], name='top_att_c3_slice')(attention_3ch)
- att_c4_raw = KL.Lambda(lambda x: x[:, :, :, 2:3], name='top_att_c4_slice')(attention_3ch)
- # Apply attention to the corresponding projected features
- attended_c2 = KL.Multiply(name='top_multiply_c2')([c2_proj, att_c2_raw])
- attended_c3 = KL.Multiply(name='top_multiply_c3')([c3_proj, att_c3_raw])
- attended_c4 = KL.Multiply(name='top_multiply_c4')([c4_proj, att_c4_raw])
- # Combined to produce a fusion feature map FS5
- FS5 = KL.Add(name='top_fs5')([attended_c2, attended_c3, attended_c4])
- # Generate P5 and merge with FS5
- # The original P5 (projected from C5)
- P5_original = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c5p5')(C5)
- # Resample the FS5 to P5 dimensions (H/32, W/32)
- p5_shape = tf.shape(P5_original)
- FS5_upsampled = KL.Lambda(
- lambda x: tf.image.resize_images(x, (p5_shape[1], p5_shape[2]), method=tf.image.ResizeMethod.BILINEAR),
- name='top_fs5_upsample'
- )(FS5)
- # The enhanced P5
- enhanced_P5 = KL.Add(name='top_enhanced_p5')([P5_original, FS5_upsampled])
- return enhanced_P5
- class MaskRCNN():
- def __init__(self, mode, config, model_dir):
- assert mode in ['training', 'inference']
- self.mode = mode
- self.config = config
- self.model_dir = model_dir
- self.set_log_dir()
- self.keras_model = self.build(mode=mode, config=config)
- def build(self, mode, config):
- assert mode in ['training', 'inference']
- # Image size must be dividable by 2 multiple times
- h, w = config.IMAGE_SHAPE[:2]
- if h / 2**6 != int(h / 2**6) or w / 2**6 != int(w / 2**6):
- raise Exception("Image size must be dividable by 2 at least 6 times "
- "to avoid fractions when downscaling and upscaling."
- "For example, use 256, 320, 384, 448, 512, ... etc. ")
- # Inputs
- input_image = KL.Input(
- shape=[None, None, config.IMAGE_SHAPE[2]], name="input_image")
- input_image_meta = KL.Input(shape=[config.IMAGE_META_SIZE],
- name="input_image_meta")
- if mode == "training":
- # RPN GT
- input_rpn_match = KL.Input(
- shape=[None, 1], name="input_rpn_match", dtype=tf.int32)
- input_rpn_bbox = KL.Input(
- shape=[None, 4], name="input_rpn_bbox", dtype=tf.float32)
- # Detection GT (class IDs, bounding boxes, and masks)
- # 1. GT Class IDs (zero padded)
- input_gt_class_ids = KL.Input(
- shape=[None], name="input_gt_class_ids", dtype=tf.int32)
- # 2. GT Boxes in pixels (zero padded)
- # [batch, MAX_GT_INSTANCES, (y1, x1, y2, x2)] in image coordinates
- input_gt_boxes = KL.Input(
- shape=[None, 4], name="input_gt_boxes", dtype=tf.float32)
- # Normalize coordinates
- gt_boxes = KL.Lambda(lambda x: norm_boxes_graph(
- x, K.shape(input_image)[1:3]))(input_gt_boxes)
- # 3. GT Masks (zero padded)
- # [batch, height, width, MAX_GT_INSTANCES]
- if config.USE_MASK:
- if config.USE_MINI_MASK:
- input_gt_masks = KL.Input(
- shape=[config.MINI_MASK_SHAPE[0],
- config.MINI_MASK_SHAPE[1], None],
- name="input_gt_masks", dtype=bool)
- else:
- input_gt_masks = KL.Input(
- shape=[config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1], None],
- name="input_gt_masks", dtype=bool)
- # else:
- # input_gt_masks = None
- elif mode == "inference":
- # Anchors in normalized coordinates
- input_anchors = KL.Input(shape=[None, 4], name="input_anchors")
- # Build the shared convolutional layers.
- if callable(config.BACKBONE):
- _, C2, C3, C4, C5 = config.BACKBONE(input_image, stage5=True,
- train_bn=config.TRAIN_BN)
- else:
- _, C2, C3, C4, C5 = resnet_graph(input_image, config.BACKBONE,
- stage5=True, train_bn=config.TRAIN_BN)
- # feature reuse block(FR)
- if config.USE_PER_LEVEL_ENHANCEMENT:
- enhanced_C2, enhanced_C3, enhanced_C4 = per_level_enhancement_block(C2, C3, C4, config)
- # Using enhanced features
- feat_C2, feat_C3, feat_C4 = enhanced_C2, enhanced_C3, enhanced_C4
- else:
- # Use the original features
- feat_C2, feat_C3, feat_C4 = C2, C3, C4
- # FPN
- P5_base = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c5p5_base')(C5)
- # Top-level Fusion Enhancement
- if config.USE_TOP_LEVEL_FUSION:
- # Using enhanced C2-C4
- fusion_input_C2 = enhanced_C2 if config.USE_PER_LEVEL_ENHANCEMENT else C2
- fusion_input_C3 = enhanced_C3 if config.USE_PER_LEVEL_ENHANCEMENT else C3
- fusion_input_C4 = enhanced_C4 if config.USE_PER_LEVEL_ENHANCEMENT else C4
- P5 = top_level_fusion_block(fusion_input_C2, fusion_input_C3, fusion_input_C4, C5, config)
- else:
- P5 = P5_base
- P4 = KL.Add(name="fpn_p4add")([
- KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5),
- KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c4p4')(feat_C4)
- ])
- P3 = KL.Add(name="fpn_p3add")([
- KL.UpSampling2D(size=(2, 2), name="fpn_p4upsampled")(P4),
- KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c3p3')(feat_C3)
- ])
- P2 = KL.Add(name="fpn_p2add")([
- KL.UpSampling2D(size=(2, 2), name="fpn_p3upsampled")(P3),
- KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c2p2')(feat_C2)
- ])
- # Attach 3x3 conv to all P layers to get the final feature maps.
- P2 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p2")(P2)
- P3 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p3")(P3)
- P4 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p4")(P4)
- P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p5")(P5)
- # a, b, c, d = P5_test.shape
- # P5_test = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p5_test")(P5_test)
- # P6 is used for the 5th anchor scale in RPN. Generated by
- # subsampling from P5 with stride of 2.
- P6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2, name="fpn_p6")(P5)
- # Note that P6 is used in RPN, but not in the classifier heads.
- rpn_feature_maps = [P2, P3, P4, P5, P6]
- mrcnn_feature_maps = [P2, P3, P4, P5]
- # Anchors
- if mode == "training":
- anchors = self.get_anchors(config.IMAGE_SHAPE)
- # Duplicate across the batch dimension because Keras requires it
- # TODO: can this be optimized to avoid duplicating the anchors?
- anchors = np.broadcast_to(anchors, (config.BATCH_SIZE,) + anchors.shape)
- # A hack to get around Keras's bad support for constants
- anchors = KL.Lambda(lambda x: tf.Variable(anchors), name="anchors")(input_image)
- else:
- anchors = input_anchors
- # RPN Model
- rpn = build_rpn_model(config.RPN_ANCHOR_STRIDE,
- len(config.RPN_ANCHOR_RATIOS), config.TOP_DOWN_PYRAMID_SIZE)
- # Loop through pyramid layers
- layer_outputs = [] # list of lists
- for p in rpn_feature_maps:
- layer_outputs.append(rpn([p]))
- print(p.shape)
- # Concatenate layer outputs
- # Convert from list of lists of level outputs to list of lists
- # of outputs across levels.
- # e.g. [[a1, b1, c1], [a2, b2, c2]] => [[a1, a2], [b1, b2], [c1, c2]]
- output_names = ["rpn_class_logits", "rpn_class", "rpn_bbox"]
- outputs = list(zip(*layer_outputs))
- outputs = [KL.Concatenate(axis=1, name=n)(list(o))
- for o, n in zip(outputs, output_names)]
- rpn_class_logits, rpn_class, rpn_bbox = outputs
- # Generate proposals
- # Proposals are [batch, N, (y1, x1, y2, x2)] in normalized coordinates
- # and zero padded.
- proposal_count = config.POST_NMS_ROIS_TRAINING if mode == "training"\
- else config.POST_NMS_ROIS_INFERENCE
- rpn_rois = ProposalLayer(
- proposal_count=proposal_count,
- nms_threshold=config.RPN_NMS_THRESHOLD,
- name="ROI",
- config=config)([rpn_class, rpn_bbox, anchors])
- if mode == "training":
- # Class ID mask to mark class IDs supported by the dataset the image
- # came from.
- active_class_ids = KL.Lambda(
- lambda x: parse_image_meta_graph(x)["active_class_ids"]
- )(input_image_meta)
- if not config.USE_RPN_ROIS:
- # Ignore predicted ROIs and use ROIs provided as an input.
- input_rois = KL.Input(shape=[config.POST_NMS_ROIS_TRAINING, 4],
- name="input_roi", dtype=np.int32)
- # Normalize coordinates
- target_rois = KL.Lambda(lambda x: norm_boxes_graph(
- x, K.shape(input_image)[1:3]))(input_rois)
- else:
- target_rois = rpn_rois
- # Generate detection targets 其中target_rois是proposallayer输出结果
- # Subsamples proposals and generates target outputs for training
- # Note that proposal class IDs, gt_boxes, and gt_masks are zero
- # padded. Equally, returned rois and targets are zero padded.
- if config.USE_MASK:
- rois, target_class_ids, target_bbox, target_mask =\
- DetectionTargetLayer(config, name="proposal_targets")([
- target_rois, input_gt_class_ids, gt_boxes, input_gt_masks])
- else:
- # NO MASK
- rois, target_class_ids, target_bbox = \
- DetectionTargetLayerNoMask(config, name="proposal_targets")([
- target_rois, input_gt_class_ids, gt_boxes])
- # target_mask = None
- # Network Heads fpn_classifier_graph + build_fpn_mask_graph
- mrcnn_class_logits, mrcnn_class, mrcnn_bbox =\
- fpn_classifier_graph(rois, mrcnn_feature_maps, input_image_meta,
- config.POOL_SIZE, config.NUM_CLASSES,
- train_bn=config.TRAIN_BN,
- fc_layers_size=config.FPN_CLASSIF_FC_LAYERS_SIZE)
- if config.USE_MASK:
- mrcnn_mask = build_fpn_mask_graph(rois, mrcnn_feature_maps,
- input_image_meta,
- config.MASK_POOL_SIZE,
- config.NUM_CLASSES,
- train_bn=config.TRAIN_BN)
- # else:
- # mrcnn_mask = KL.Lambda(lambda x: tf.constant(0.0), name="dummy_mask")(rois)
- # TODO: clean up (use tf.identify if necessary)
- output_rois = KL.Lambda(lambda x: x * 1, name="output_rois")(rois)
- # Losses
- rpn_class_loss = KL.Lambda(lambda x: rpn_class_loss_graph(*x), name="rpn_class_loss")(
- [input_rpn_match, rpn_class_logits])
- rpn_bbox_loss = KL.Lambda(lambda x: rpn_bbox_loss_graph(config, *x), name="rpn_bbox_loss")(
- [input_rpn_bbox, input_rpn_match, rpn_bbox])
- class_loss = KL.Lambda(lambda x: mrcnn_class_loss_graph(*x), name="mrcnn_class_loss")(
- [target_class_ids, mrcnn_class_logits, active_class_ids])
- bbox_loss = KL.Lambda(lambda x: mrcnn_bbox_loss_graph(*x), name="mrcnn_bbox_loss")(
- [target_bbox, target_class_ids, mrcnn_bbox])
- # Model
- inputs = [input_image, input_image_meta,
- input_rpn_match, input_rpn_bbox,
- input_gt_class_ids, input_gt_boxes]
- if config.USE_MASK:
- inputs.append(input_gt_masks)
- if not config.USE_RPN_ROIS:
- inputs.append(input_rois)
- if config.USE_MASK:
- mask_loss = KL.Lambda(lambda x: mrcnn_mask_loss_graph(*x), name="mrcnn_mask_loss")(
- [target_mask, target_class_ids, mrcnn_mask])
- outputs = [rpn_class_logits, rpn_class, rpn_bbox,
- mrcnn_class_logits, mrcnn_class, mrcnn_bbox, mrcnn_mask,
- rpn_rois, output_rois,
- rpn_class_loss, rpn_bbox_loss, class_loss, bbox_loss, mask_loss]
- else:
- outputs = [rpn_class_logits, rpn_class, rpn_bbox,
- mrcnn_class_logits, mrcnn_class, mrcnn_bbox,
- rpn_rois, output_rois,
- rpn_class_loss, rpn_bbox_loss, class_loss, bbox_loss]
- model = KM.Model(inputs, outputs, name='faster_rcnn')
- else:
- # Network Heads
- # Proposal classifier and BBox regressor heads
- mrcnn_class_logits, mrcnn_class, mrcnn_bbox =\
- fpn_classifier_graph(rpn_rois, mrcnn_feature_maps, input_image_meta,
- config.POOL_SIZE, config.NUM_CLASSES,
- train_bn=config.TRAIN_BN,
- fc_layers_size=config.FPN_CLASSIF_FC_LAYERS_SIZE)
- # Detections
- # output is [batch, num_detections, (y1, x1, y2, x2, class_id, score)] in
- # normalized coordinates
- detections = DetectionLayer(config, name="mrcnn_detection")(
- [rpn_rois, mrcnn_class, mrcnn_bbox, input_image_meta])
- # Create masks for detections
- if config.USE_MASK:
- detection_boxes = KL.Lambda(lambda x: x[..., :4])(detections)
- mrcnn_mask = build_fpn_mask_graph(detection_boxes, mrcnn_feature_maps,
- input_image_meta,
- config.MASK_POOL_SIZE,
- config.NUM_CLASSES,
- train_bn=config.TRAIN_BN)
- model = KM.Model([input_image, input_image_meta, input_anchors],
- [detections, mrcnn_class, mrcnn_bbox,
- mrcnn_mask, rpn_rois, rpn_class, rpn_bbox],
- name='mask_rcnn')
- else:
- # Do not output the mask
- model = KM.Model([input_image, input_image_meta, input_anchors],
- [detections, mrcnn_class, mrcnn_bbox,
- rpn_rois, rpn_class, rpn_bbox],
- name='faster_rcnn')
- # Add multi-GPU support
- if config.GPU_COUNT > 1:
- from mrcnn.parallel_model import ParallelModel
- model = ParallelModel(model, config.GPU_COUNT)
- return model
- def find_last(self):
- """Finds the last checkpoint file of the last trained model in the
- model directory.
- Returns:
- The path of the last checkpoint file
- """
- # Get directory names. Each directory corresponds to a model
- dir_names = next(os.walk(self.model_dir))[1]
- key = self.config.NAME.lower()
- dir_names = filter(lambda f: f.startswith(key), dir_names)
- dir_names = sorted(dir_names)
- if not dir_names:
- import errno
- raise FileNotFoundError(
- errno.ENOENT,
- "Could not find model directory under {}".format(self.model_dir))
- # Pick last directory
- dir_name = os.path.join(self.model_dir, dir_names[-1])
- # Find the last checkpoint
- checkpoints = next(os.walk(dir_name))[2]
- checkpoints = filter(lambda f: f.startswith("2D_method"), checkpoints)
- checkpoints = sorted(checkpoints)
- if not checkpoints:
- import errno
- raise FileNotFoundError(
- errno.ENOENT, "Could not find weight files in {}".format(dir_name))
- checkpoint = os.path.join(dir_name, checkpoints[-1])
- return checkpoint
- def load_weights(self, filepath, by_name=False, exclude=None):
- import h5py
- try:
- from keras.engine import saving
- except ImportError:
- from keras.engine import topology as saving
- if exclude:
- by_name = True
- if h5py is None:
- raise ImportError('`load_weights` requires h5py.')
- f = h5py.File(filepath, mode='r')
- if 'layer_names' not in f.attrs and 'model_weights' in f:
- f = f['model_weights']
- # In multi-GPU training, we wrap the model.
- keras_model = self.keras_model
- layers = keras_model.inner_model.layers if hasattr(keras_model, "inner_model")\
- else keras_model.layers
- # Exclude some layers
- if exclude:
- layers = filter(lambda l: l.name not in exclude, layers)
- if by_name:
- saving.load_weights_from_hdf5_group_by_name(f, layers)
- else:
- saving.load_weights_from_hdf5_group(f, layers)
- if hasattr(f, 'close'):
- f.close()
- # Update the log directory
- self.set_log_dir(filepath)
- def get_imagenet_weights(self):
- from keras.utils.data_utils import get_file
- TF_WEIGHTS_PATH_NO_TOP = 'https://github.com/fchollet/deep-learning-models/'\
- 'releases/download/v0.2/'\
- 'resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5'
- weights_path = get_file('resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5',
- TF_WEIGHTS_PATH_NO_TOP,
- cache_subdir='models',
- md5_hash='a268eb855778b3df3c7506639542a6af')
- return weights_path
- def compile(self, learning_rate, momentum):
- # Optimizer object
- optimizer = keras.optimizers.SGD(
- lr=learning_rate, momentum=momentum,
- clipnorm=self.config.GRADIENT_CLIP_NORM)
- # Add Losses
- # First, clear previously set losses to avoid duplication
- self.keras_model._losses = []
- self.keras_model._per_input_losses = {}
- # loss_names = [
- # "rpn_class_loss", "rpn_bbox_loss",
- # "mrcnn_class_loss", "mrcnn_bbox_loss", "mrcnn_mask_loss"]
- if self.config.USE_MASK:
- loss_names = ["rpn_class_loss", "rpn_bbox_loss",
- "mrcnn_class_loss", "mrcnn_bbox_loss", "mrcnn_mask_loss"]
- else:
- loss_names = ["rpn_class_loss", "rpn_bbox_loss",
- "mrcnn_class_loss", "mrcnn_bbox_loss"]
- for name in loss_names:
- layer = self.keras_model.get_layer(name)
- if layer.output in self.keras_model.losses:
- continue
- loss = (
- tf.reduce_mean(layer.output, keepdims=True)
- * self.config.LOSS_WEIGHTS.get(name, 1.))
- self.keras_model.add_loss(loss)
- # Add L2 Regularization
- # Skip gamma and beta weights of batch normalization layers.
- reg_losses = [
- keras.regularizers.l2(self.config.WEIGHT_DECAY)(w) / tf.cast(tf.size(w), tf.float32)
- for w in self.keras_model.trainable_weights
- if 'gamma' not in w.name and 'beta' not in w.name]
- self.keras_model.add_loss(tf.add_n(reg_losses))
- # Compile
- self.keras_model.compile(
- optimizer=optimizer,
- loss=[None] * len(self.keras_model.outputs))
- # Add metrics for losses
- for name in loss_names:
- if name in self.keras_model.metrics_names:
- continue
- layer = self.keras_model.get_layer(name)
- self.keras_model.metrics_names.append(name)
- loss = (
- tf.reduce_mean(layer.output, keepdims=True)
- * self.config.LOSS_WEIGHTS.get(name, 1.))
- self.keras_model.metrics_tensors.append(loss)
- def set_trainable(self, layer_regex, keras_model=None, indent=0, verbose=1):
- # Print message on the first call (but not on recursive calls)
- if verbose > 0 and keras_model is None:
- log("Selecting layers to train")
- keras_model = keras_model or self.keras_model
- # In multi-GPU training, we wrap the model. Get layers
- # of the inner model because they have the weights.
- layers = keras_model.inner_model.layers if hasattr(keras_model, "inner_model")\
- else keras_model.layers
- for layer in layers:
- if layer.__class__.__name__ == 'Model':
- print("In model: ", layer.name)
- self.set_trainable(
- layer_regex, keras_model=layer, indent=indent + 4)
- continue
- if not layer.weights:
- continue
- trainable = bool(re.fullmatch(layer_regex, layer.name))
- # Update layer. If layer is a container, update inner layer.
- if layer.__class__.__name__ == 'TimeDistributed':
- layer.layer.trainable = trainable
- else:
- layer.trainable = trainable
- # Print trainable layer names
- if trainable and verbose > 0:
- log("{}{:20} ({})".format(" " * indent, layer.name,
- layer.__class__.__name__))
- def get_layer(self, layer_regex, keras_model=None, indent=0, verbose=1):
- # Print message on the first call (but not on recursive calls)
- if verbose > 0 and keras_model is None:
- log("Get layers output")
- keras_model = keras_model or self.keras_model
- # In multi-GPU training, we wrap the model. Get layers
- # of the inner model because they have the weights.
- layers = keras_model.inner_model.layers if hasattr(keras_model, "inner_model")\
- else keras_model.layers
- for layer in layers:
- if layer.__class__.__name__ == 'Model':
- print("In model: ", layer.name)
- return layers
- def set_log_dir(self, model_path=None):
- # Set date and epoch counter as if starting a new model
- self.epoch = 0
- now = datetime.datetime.now()
- # If we have a model path with date and epochs use them
- if model_path:
- # Continue from we left of. Get epoch and date from the file name
- # A sample model path might look like:
- # \path\to\logs\coco20171029T2315\mask_rcnn_coco_0001.h5 (Windows)
- regex = r".*[/\\][\w-]+(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})[/\\]2D\_method\_[\w-]+(\d{4})\.h5"
- m = re.match(regex, model_path)
- if m:
- now = datetime.datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)),
- int(m.group(4)), int(m.group(5)))
- # Epoch number in file is 1-based, and in Keras code it's 0-based.
- # So, adjust for that then increment by one to start from the next epoch
- self.epoch = int(m.group(6)) - 1 + 1
- print('Re-starting from epoch %d' % self.epoch)
- # Directory for training logs
- self.log_dir = os.path.join(self.model_dir, "{}{:%Y%m%dT%H%M}".format(
- self.config.NAME.lower(), now))
- # Path to save after each epoch. Include placeholders that get filled by Keras.
- self.checkpoint_path = os.path.join(self.log_dir, "2D_method_{}_*epoch*.h5".format(
- self.config.NAME.lower()))
- self.checkpoint_path = self.checkpoint_path.replace(
- "*epoch*", "{epoch:04d}")
- def train(self, train_dataset, val_dataset, learning_rate, epochs, layers,
- augmentation=None, custom_callbacks=None, no_augmentation_sources=None):
- """
- layers: Allows selecting wich layers to train. It can be:
- - A regular expression to match layer names to train
- - One of these predefined values:
- heads: The RPN, classifier and mask heads of the network
- all: All the layers
- 3+: Train Resnet stage 3 and up
- 4+: Train Resnet stage 4 and up
- 5+: Train Resnet stage 5 and up
- """
- assert self.mode == "training", "Create model in training mode."
- # Pre-defined layer regular expressions
- layer_regex = {
- # all layers but the backbone
- "heads": r"(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)|(conv1.*)",
- # From a specific Resnet stage and up
- "3+": r"(res3.*)|(bn3.*)|(res4.*)|(bn4.*)|(res5.*)|(bn5.*)|(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
- "4+": r"(res4.*)|(bn4.*)|(res5.*)|(bn5.*)|(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
- "5+": r"(res5.*)|(bn5.*)|(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
- # All layers
- "all": ".*",
- }
- if layers in layer_regex.keys():
- layers = layer_regex[layers]
- # Data generators
- train_generator = data_generator(train_dataset, self.config, shuffle=True,
- augmentation=augmentation,
- batch_size=self.config.BATCH_SIZE,
- no_augmentation_sources=no_augmentation_sources)
- val_generator = data_generator(val_dataset, self.config, shuffle=True,
- batch_size=self.config.BATCH_SIZE)
- # Create log_dir if it does not exist
- if not os.path.exists(self.log_dir):
- os.makedirs(self.log_dir)
- # Callbacks
- callbacks = [
- keras.callbacks.TensorBoard(log_dir=self.log_dir,
- histogram_freq=0, write_graph=True, write_images=False),
- keras.callbacks.ModelCheckpoint(self.checkpoint_path,
- verbose=0, save_weights_only=True),
- ]
- # Add custom callbacks to the list
- if custom_callbacks:
- callbacks += custom_callbacks
- # Train
- log("\nStarting at epoch {}. LR={}\n".format(self.epoch, learning_rate))
- log("Checkpoint Path: {}".format(self.checkpoint_path))
- self.set_trainable(layers)
- self.compile(learning_rate, self.config.LEARNING_MOMENTUM)
- if os.name is 'nt':
- workers = 0
- else:
- workers = multiprocessing.cpu_count()
- # fit_generator
- history = self.keras_model.fit_generator(
- train_generator,
- initial_epoch=self.epoch,
- epochs=epochs,
- steps_per_epoch=self.config.STEPS_PER_EPOCH,
- callbacks=callbacks,
- validation_data=val_generator,
- validation_steps=self.config.VALIDATION_STEPS,
- max_queue_size=100,
- workers=workers,
- use_multiprocessing=False,
- )
- self.epoch = max(self.epoch, epochs)
- try:
- a = history.epoch
- b = history.history['loss']
- b1 = history.history['rpn_class_loss']
- b2 = history.history['rpn_bbox_loss']
- b3 = history.history['mrcnn_class_loss']
- b4 = history.history['mrcnn_bbox_loss']
- c = history.history['val_loss']
- c1 = history.history['val_rpn_class_loss']
- c2 = history.history['val_rpn_bbox_loss']
- c3 = history.history['val_mrcnn_class_loss']
- c4 = history.history['val_mrcnn_bbox_loss']
- epoch_list.extend(a)
- tra_loss_list.extend(b)
- tra1_loss_list.extend(b1)
- tra2_loss_list.extend(b2)
- tra3_loss_list.extend(b3)
- tra4_loss_list.extend(b4)
- val_loss_list.extend(c)
- val1_loss_list.extend(c1)
- val2_loss_list.extend(c2)
- val3_loss_list.extend(c3)
- val4_loss_list.extend(c4)
- except Exception:
- pass
- def mold_inputs(self, images):
- molded_images = []
- image_metas = []
- windows = []
- for image in images:
- if image.ndim == 2:
- image = image[:, :, np.newaxis]
- # Resize image
- # TODO: move resizing to mold_image()
- molded_image, window, scale, padding, crop = utils.resize_image(
- image,
- min_dim=self.config.IMAGE_MIN_DIM,
- min_scale=self.config.IMAGE_MIN_SCALE,
- max_dim=self.config.IMAGE_MAX_DIM,
- mode=self.config.IMAGE_RESIZE_MODE)
- molded_image = mold_image(molded_image, self.config)
- # Build image_meta
- image_meta = compose_image_meta(
- 0, image.shape, molded_image.shape, window, scale,
- np.zeros([self.config.NUM_CLASSES], dtype=np.int32))
- # Append
- molded_images.append(molded_image)
- windows.append(window)
- image_metas.append(image_meta)
- # Pack into arrays
- molded_images = np.stack(molded_images)
- image_metas = np.stack(image_metas)
- windows = np.stack(windows)
- return molded_images, image_metas, windows
- def unmold_detections(self, detections, mrcnn_mask, original_image_shape,
- image_shape, window):
- # How many detections do we have?
- # Detections array is padded with zeros. Find the first class_id == 0.
- zero_ix = np.where(detections[:, 4] == 0)[0]
- N = zero_ix[0] if zero_ix.shape[0] > 0 else detections.shape[0]
- # Extract boxes, class_ids, scores, and class-specific masks
- boxes = detections[:N, :4]
- class_ids = detections[:N, 4].astype(np.int32)
- scores = detections[:N, 5]
- masks = mrcnn_mask[np.arange(N), :, :, class_ids]
- # Translate normalized coordinates in the resized image to pixel
- # coordinates in the original image before resizing
- window = utils.norm_boxes(window, image_shape[:2])
- wy1, wx1, wy2, wx2 = window
- shift = np.array([wy1, wx1, wy1, wx1])
- wh = wy2 - wy1 # window height
- ww = wx2 - wx1 # window width
- scale = np.array([wh, ww, wh, ww])
- # Convert boxes to normalized coordinates on the window
- boxes = np.divide(boxes - shift, scale)
- # Convert boxes to pixel coordinates on the original image
- boxes = utils.denorm_boxes(boxes, original_image_shape[:2])
- # Filter out detections with zero area. Happens in early training when
- # network weights are still random
- exclude_ix = np.where(
- (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) <= 0)[0]
- if exclude_ix.shape[0] > 0:
- boxes = np.delete(boxes, exclude_ix, axis=0)
- class_ids = np.delete(class_ids, exclude_ix, axis=0)
- scores = np.delete(scores, exclude_ix, axis=0)
- masks = np.delete(masks, exclude_ix, axis=0)
- N = class_ids.shape[0]
- # Resize masks to original image size and set boundary threshold.
- full_masks = []
- for i in range(N):
- # Convert neural network mask to full size mask
- full_mask = utils.unmold_mask(masks[i], boxes[i], original_image_shape)
- full_masks.append(full_mask)
- full_masks = np.stack(full_masks, axis=-1)\
- if full_masks else np.empty(original_image_shape[:2] + (0,))
- return boxes, class_ids, scores, full_masks
- def unmold_detections_no_mask(self, detections, original_image_shape,
- image_shape, window):
- """The version of unmold_detections without masks"""
- # How many detections do we have?
- zero_ix = np.where(detections[:, 4] == 0)[0]
- N = zero_ix[0] if zero_ix.shape[0] > 0 else detections.shape[0]
- # Extract boxes, class_ids, scores
- boxes = detections[:N, :4]
- class_ids = detections[:N, 4].astype(np.int32)
- scores = detections[:N, 5]
- # Translate normalized coordinates to pixel coordinates
- window = utils.norm_boxes(window, image_shape[:2])
- wy1, wx1, wy2, wx2 = window
- shift = np.array([wy1, wx1, wy1, wx1])
- wh = wy2 - wy1
- ww = wx2 - wx1
- scale = np.array([wh, ww, wh, ww])
- boxes = np.divide(boxes - shift, scale)
- boxes = utils.denorm_boxes(boxes, original_image_shape[:2])
- # Filter out detections with zero area
- exclude_ix = np.where(
- (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) <= 0)[0]
- if exclude_ix.shape[0] > 0:
- boxes = np.delete(boxes, exclude_ix, axis=0)
- class_ids = np.delete(class_ids, exclude_ix, axis=0)
- scores = np.delete(scores, exclude_ix, axis=0)
- return boxes, class_ids, scores
- def detect(self, images, verbose=0):
- assert self.mode == "inference", "Create model in inference mode."
- assert len(images) == self.config.BATCH_SIZE, "len(images) must be equal to BATCH_SIZE"
- # If the current number of inputs for keras_model is not 3
- # (indicating that it is a training model), then build an inference model
- if len(self.keras_model.inputs) != 3:
- if not hasattr(self, 'inference_model') or self.inference_model is None:
- original_mode = self.mode
- self.mode = "inference"
- self.inference_model = self.build(mode="inference", config=self.config)
- self.mode = original_mode
- predict_model = self.inference_model
- else:
- predict_model = self.keras_model
- if verbose:
- log("Processing {} images".format(len(images)))
- for image in images:
- log("image", image)
- # Mold inputs
- molded_images, image_metas, windows = self.mold_inputs(images)
- # Validate image sizes
- image_shape = molded_images[0].shape
- for g in molded_images[1:]:
- assert g.shape == image_shape, \
- "After resizing, all images must have the same size."
- # Anchors
- anchors = self.get_anchors(image_shape)
- anchors = np.broadcast_to(anchors, (self.config.BATCH_SIZE,) + anchors.shape)
- if verbose:
- log("molded_images", molded_images)
- log("image_metas", image_metas)
- log("anchors", anchors)
- # Run object detection (predict_model)
- if self.config.USE_MASK:
- detections, _, _, mrcnn_mask, _, _, _ = \
- predict_model.predict([molded_images, image_metas, anchors], verbose=0)
- else:
- outputs = predict_model.predict([molded_images, image_metas, anchors], verbose=0)
- if len(outputs) == 6:
- detections, _, _, _, _, _ = outputs
- mrcnn_mask = None
- elif len(outputs) == 5:
- detections, _, _, _, _ = outputs
- mrcnn_mask = None
- else:
- detections = outputs[0]
- mrcnn_mask = None
- # Process detections
- results = []
- for i, image in enumerate(images):
- if self.config.USE_MASK or mrcnn_mask is not None:
- final_rois, final_class_ids, final_scores, final_masks = \
- self.unmold_detections(detections[i], mrcnn_mask[i],
- image.shape, molded_images[i].shape,
- windows[i])
- else:
- final_rois, final_class_ids, final_scores = \
- self.unmold_detections_no_mask(detections[i],
- image.shape, molded_images[i].shape,
- windows[i])
- final_masks = None
- results.append({
- "rois": final_rois,
- "class_ids": final_class_ids,
- "scores": final_scores,
- "masks": final_masks,
- })
- return results
- def detect_molded(self, molded_images, image_metas, verbose=0):
- assert self.mode == "inference", "Create model in inference mode."
- assert len(molded_images) == self.config.BATCH_SIZE,\
- "Number of images must be equal to BATCH_SIZE"
- if verbose:
- log("Processing {} images".format(len(molded_images)))
- for image in molded_images:
- log("image", image)
- # Validate image sizes
- # All images in a batch MUST be of the same size
- image_shape = molded_images[0].shape
- for g in molded_images[1:]:
- assert g.shape == image_shape, "Images must have the same size"
- # Anchors
- anchors = self.get_anchors(image_shape)
- anchors = np.broadcast_to(anchors, (self.config.BATCH_SIZE,) + anchors.shape)
- if verbose:
- log("molded_images", molded_images)
- log("image_metas", image_metas)
- log("anchors", anchors)
- # Run object detection
- detections, _, _, mrcnn_mask, _, _, _ =\
- self.keras_model.predict([molded_images, image_metas, anchors], verbose=0)
- # Process detections
- results = []
- for i, image in enumerate(molded_images):
- window = [0, 0, image.shape[0], image.shape[1]]
- final_rois, final_class_ids, final_scores, final_masks =\
- self.unmold_detections(detections[i], mrcnn_mask[i],
- image.shape, molded_images[i].shape,
- window)
- results.append({
- "rois": final_rois,
- "class_ids": final_class_ids,
- "scores": final_scores,
- "masks": final_masks,
- })
- return results
- def get_anchors(self, image_shape):
- backbone_shapes = compute_backbone_shapes(self.config, image_shape)
- # Cache anchors and reuse if image shape is the same
- if not hasattr(self, "_anchor_cache"):
- self._anchor_cache = {}
- if not tuple(image_shape) in self._anchor_cache:
- # Generate Anchors
- a = utils.generate_pyramid_anchors(
- self.config.RPN_ANCHOR_SCALES,
- self.config.RPN_ANCHOR_RATIOS,
- backbone_shapes,
- self.config.BACKBONE_STRIDES,
- self.config.RPN_ANCHOR_STRIDE)
- self.anchors = a
- # Normalize coordinates
- self._anchor_cache[tuple(image_shape)] = utils.norm_boxes(a, image_shape[:2])
- return self._anchor_cache[tuple(image_shape)]
- def ancestor(self, tensor, name, checked=None):
- checked = checked if checked is not None else []
- # Put a limit on how deep we go to avoid very long loops
- if len(checked) > 500:
- return None
- # Convert name to a regex and allow matching a number prefix
- # because Keras adds them automatically
- if isinstance(name, str):
- name = re.compile(name.replace("/", r"(\_\d+)*/"))
- parents = tensor.op.inputs
- for p in parents:
- if p in checked:
- continue
- if bool(re.fullmatch(name, p.name)):
- return p
- checked.append(p)
- a = self.ancestor(p, name, checked)
- if a is not None:
- return a
- return None
- def find_trainable_layer(self, layer):
- if layer.__class__.__name__ == 'TimeDistributed':
- return self.find_trainable_layer(layer.layer)
- return layer
- def get_trainable_layers(self):
- layers = []
- # Loop through all layers
- for l in self.keras_model.layers:
- # If layer is a wrapper, find inner trainable layer
- l = self.find_trainable_layer(l)
- # Include layer if it has weights
- if l.get_weights():
- layers.append(l)
- return layers
- def run_graph(self, images, outputs, image_metas=None):
- model = self.keras_model
- # Organize desired outputs into an ordered dict
- outputs = OrderedDict(outputs)
- for o in outputs.values():
- assert o is not None
- # Build a Keras function to run parts of the computation graph
- inputs = model.inputs
- if model.uses_learning_phase and not isinstance(K.learning_phase(), int):
- inputs += [K.learning_phase()]
- kf = K.function(model.inputs, list(outputs.values()))
- # Prepare inputs
- if image_metas is None:
- molded_images, image_metas, _ = self.mold_inputs(images)
- else:
- molded_images = images
- image_shape = molded_images[0].shape
- # Anchors
- anchors = self.get_anchors(image_shape)
- anchors = np.broadcast_to(anchors, (self.config.BATCH_SIZE,) + anchors.shape)
- model_in = [molded_images, image_metas, anchors]
- # Run inference
- if model.uses_learning_phase and not isinstance(K.learning_phase(), int):
- model_in.append(0.)
- outputs_np = kf(model_in)
- # Pack the generated Numpy arrays into a a dict and log the results.
- outputs_np = OrderedDict([(k, v)
- for k, v in zip(outputs.keys(), outputs_np)])
- for k, v in outputs_np.items():
- log(k, v)
- return outputs_np
- # Data Formatting
- def compose_image_meta(image_id, original_image_shape, image_shape,
- window, scale, active_class_ids):
- meta = np.array(
- [image_id] + # size=1
- list(original_image_shape) + # size=3
- list(image_shape) + # size=3
- list(window) + # size=4 (y1, x1, y2, x2) in image cooredinates
- [scale] + # size=1
- list(active_class_ids) # size=num_classes
- )
- return meta
- def parse_image_meta(meta):
- image_id = meta[:, 0]
- original_image_shape = meta[:, 1:4]
- image_shape = meta[:, 4:7]
- window = meta[:, 7:11] # (y1, x1, y2, x2) window of image in in pixels
- scale = meta[:, 11]
- active_class_ids = meta[:, 12:]
- return {
- "image_id": image_id.astype(np.int32),
- "original_image_shape": original_image_shape.astype(np.int32),
- "image_shape": image_shape.astype(np.int32),
- "window": window.astype(np.int32),
- "scale": scale.astype(np.float32),
- "active_class_ids": active_class_ids.astype(np.int32),
- }
- def parse_image_meta_graph(meta):
- image_id = meta[:, 0]
- original_image_shape = meta[:, 1:4]
- image_shape = meta[:, 4:7]
- window = meta[:, 7:11] # (y1, x1, y2, x2) window of image in in pixels
- scale = meta[:, 11]
- active_class_ids = meta[:, 12:]
- return {
- "image_id": image_id,
- "original_image_shape": original_image_shape,
- "image_shape": image_shape,
- "window": window,
- "scale": scale,
- "active_class_ids": active_class_ids,
- }
- def mold_image(images, config):
- return images.astype(np.float32) - config.MEAN_PIXEL
- def unmold_image(normalized_images, config):
- return (normalized_images + config.MEAN_PIXEL).astype(np.uint8)
- # Miscellenous Graph Functions
- def trim_zeros_graph(boxes, name='trim_zeros'):
- non_zeros = tf.cast(tf.reduce_sum(tf.abs(boxes), axis=1), tf.bool)
- boxes = tf.boolean_mask(boxes, non_zeros, name=name)
- return boxes, non_zeros
- def batch_pack_graph(x, counts, num_rows):
- outputs = []
- for i in range(num_rows):
- outputs.append(x[i, :counts[i]])
- return tf.concat(outputs, axis=0)
- def norm_boxes_graph(boxes, shape):
- h, w = tf.split(tf.cast(shape, tf.float32), 2)
- scale = tf.concat([h, w, h, w], axis=-1) - tf.constant(1.0)
- shift = tf.constant([0., 0., 1., 1.])
- return tf.divide(boxes - shift, scale)
- def denorm_boxes_graph(boxes, shape):
- h, w = tf.split(tf.cast(shape, tf.float32), 2)
- scale = tf.concat([h, w, h, w], axis=-1) - tf.constant(1.0)
- shift = tf.constant([0., 0., 1., 1.])
- return tf.cast(tf.round(tf.multiply(boxes, scale) + shift), tf.int32)
- def return_value(epoch_loss, tra_loss, tra1_loss, tra2_loss, tra3_loss, tra4_loss,
- val_loss, val1_loss, val2_loss, val3_loss, val4_loss):
- return epoch_loss, tra_loss, tra1_loss, tra2_loss, tra3_loss, tra4_loss, val_loss, val1_loss, val2_loss, val3_loss, val4_loss
- def call_back():
- a, b, b1, b2, b3, b4, c, c1, c2, c3, c4 = return_value(epoch_list, tra_loss_list, tra1_loss_list, tra2_loss_list, tra3_loss_list,
- tra4_loss_list, val_loss_list, val1_loss_list,
- val2_loss_list, val3_loss_list, val4_loss_list)
- return a, b, b1, b2, b3, b4, c, c1, c2, c3, c4
model.py at commit cea2b38, no license · at the source
Overview
- MoE Key Laboratory for Biomedical Photonics, Wuhan National Laboratory for Optoelectronics, Huazhong University of Science and Technology, Wuhan, 430074, China
- HUST-Suzhou Institute for Brainsmatics, JITRI, Suzhou, 215123, China
- State Key Laboratory of Digital Medical Engineering, Key Laboratory of Biomedical Engineering of Hainan Province, School of Biomedical Engineering, Hainan University, Haikou, 570228, China
Abstract
The abstract is not reproduced here: the paper's license (none stated) does not allow it. Read it in the paper, at the publisher or on Europe PMC.
Repository
Its files are read in the Code ↔ Paper reader above, with 6 matches between paragraphs and lines of code.
Brainsmatics/GPDigit
cea2b38bfa09f29584a05a821771d2f25aaa8fa5, 7 May 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
41 files
- 2D_detection/
mrcnn/ , Python, 1 line__init__.py - 2D_detection/
mrcnn/ , Python, 182 lines, 1 matchconfig.py - 2D_detection/
mrcnn/ , Python, 2,888 lines, 2 matchesmodel.py - 2D_detection/
mrcnn/ , Python, 889 linesutils.py - 2D_detection/
mrcnn/ , Python, 528 linesvisualize.py - 2D_detection/
output_process/ , Python, 169 linesbbox_visual.py - 2D_detection/
output_process/ , Python, 126 linesfm_visual.py - 2D_detection/
output_process/ , Python, 156 linespostprocess_2D.py - 2D_detection/
output_process/ , Python, 452 linespostprocess_continue.py - 2D_detection/
predict_eval/ , Python, 1,377 linespred_eval_batch_models.p y - 2D_detection/
predict_eval/ , Python, 698 linespred_eval_model.py - 2D_detection/
samples/ , Python, 1,383 lines, 1 matchPlaques/ Plaques.py - 2D_detection/
samples/ , Python, 1,323 linesPlaques/ Plaques_mini_aug.py - 2D_detection/
samples/ , Python, 1 linePlaques/ __init__.py - 2D_detection/
samples/ , Python, 278 linesPlaques/ visualize_utils.py - 2D_detection/
samples/ , Python, 810 linesablation_batch.py - 2D_detection/
samples/ , Python, 162 linespred_demo.py - 3D_detection/
HBNet.py , Python, 86 lines - 3D_detection/
ablation_test.py , Python, 482 lines - 3D_detection/
darknet.py , Python, 585 lines - 3D_detection/
postprocess.py , Python, 154 lines - 3D_detection/
postprocess_demo.py , Python, 88 lines - 3D_detection/
predict_evaluation.py , Python, 630 lines - 3D_detection/
train.py , Python, 452 lines - 3D_detection/
train_mini_aug.py , Python, 374 lines - 3D_detection/
util.py , Python, 768 lines, 1 match - label_revise/
icon.py , Python, 1 line - label_revise/
labelrevise-note.ipynb , Jupyter, 606 lines - label_revise/
makeicon.ipynb , Jupyter, 24 lines - preprocessing/
aug_2D.py , Python, 198 lines - preprocessing/
aug_3D.py , Python, 303 lines - preprocessing/
aug_data_synthesis.py , Python, 268 lines - preprocessing/
data_chunk.py , Python, 105 lines - preprocessing/
json2txt.py , Python, 315 lines - preprocessing/
json_change.py , Python, 62 lines - preprocessing/
label_process.py , Python, 175 lines - preprocessing/
tif_3dto2d.py , Python, 64 lines - preprocessing/
txt_conver.py , Python, 205 lines - segmentation/
seg_block.py , Python, 685 lines, 1 match - segmentation/
seg_single.py , Python, 474 lines - README.md, Text, 72 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 40 scripts, each with its path and the digest of its content;
- 6 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Code and data availability statement
The paper has a code and data availability statement. Its license (none stated) does not allow reproducing it here; in short, from what the harvester recognized in it:
- it says that the data are available on request
Read it in the paper: doi.org/10.1364/boe.605322.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 27 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 12 authors, 3 funders, 31 references.
Cite
This paper
Gong, G., Liu, X., Jia, X., Long, B., Chen, S., Jiang, T., Luo, Y., Feng, Z., Li, X., Luo, Q., Gong, H., & Li, A. (2026). Generalized plaque digitization framework for multi-dimensional mesoscopic images. Biomedical optics express, 17(8), 4198-4215. https://
BibTeX
@article{gong2026general
author = {Gong, Guixuan and Liu, Xin and Jia, Xueyan and Long, Ben and Chen, Siqi and Jiang, Tao and Luo, Yue and Feng, Zhao and Li, Xiangning and Luo, Qingming and Gong, Hui and Li, Anan},
title = {{Generalized plaque digitization framework for multi-dimensional mesoscopic images}},
journal = {Biomedical optics express},
year = {2026},
month = jul,
volume = {17},
number = {8},
pages = {4198--4215},
publisher = {Optica Publishing Group},
issn = {2156-7085},
doi = {10.1364/
url = {https://
pmid = {42610137},
pmcid = {PMC13481076}
}
RIS
TY - JOUR
AU - Gong, Guixuan
AU - Liu, Xin
AU - Jia, Xueyan
AU - Long, Ben
AU - Chen, Siqi
AU - Jiang, Tao
AU - Luo, Yue
AU - Feng, Zhao
AU - Li, Xiangning
AU - Luo, Qingming
AU - Gong, Hui
AU - Li, Anan
TI - Generalized plaque digitization framework for multi-dimensional mesoscopic images
T2 - Biomedical optics express
J2 - Biomed Opt Express
PY - 2026
DA - 2026/
VL - 17
IS - 8
SP - 4198
EP - 4215
SN - 2156-7085
PB - Optica Publishing Group
DO - 10.1364/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1364/
"type": "article-journal",
"title": "Generalized plaque digitization framework for multi-dimensional mesoscopic images",
"container-title": "Biomedical optics express",
"author": [
{
"family": "Gong",
"given": "Guixuan"
},
{
"family": "Liu",
"given": "Xin"
},
{
"family": "Jia",
"given": "Xueyan"
},
{
"family": "Long",
"given": "Ben"
},
{
"family": "Chen",
"given": "Siqi"
},
{
"family": "Jiang",
"given": "Tao"
},
{
"family": "Luo",
"given": "Yue"
},
{
"family": "Feng",
"given": "Zhao"
},
{
"family": "Li",
"given": "Xiangning"
},
{
"family": "Luo",
"given": "Qingming"
},
{
"family": "Gong",
"given": "Hui"
},
{
"family": "Li",
"given": "Anan"
}
],
"container-title-short":
"volume": "17",
"issue": "8",
"page": "4198-4215",
"DOI": "10.1364/
"PMID": "42610137",
"PMCID": "PMC13481076",
"ISSN": "2156-7085",
"publisher": "Optica Publishing Group",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
20
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1038/s41593-026-02253-9 [code]
- Genoarchitecture and input-output organization of the mouse basal ganglia and thalamic parafascicular nucleus.Journal: Nature neuroscienceIn common: SimpleITK, scikit-image, h5py, 6 other tools, mouse, 1 reference, 2 authors
- [2] doi: [code]
- Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement controlJournal: eLifeIn common: imageio, tifffile, Keras, 11 other tools, mouse, 1 reference
- [3] doi:10.1016/j.xcrm.2026.102766 [code]
- A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.Journal: Cell reports. MedicineIn common: imageio, tifffile, Keras, 11 other tools
- [4] doi:10.1371/journal.pcbi.1014571 [code]
- SynAPSeg: A novel dataset and image analysis framework for deep learning-based synapse detection and quantification.Journal: PLoS computational biologyIn common: imageio, tifffile, Keras, 11 other tools
- [5] doi:10.1038/s41467-026-73373-w [code]
- Mapping neuro-vascular unit communications reveals distinct angiogenic programs across developing mouse brain regions.Journal: Nature communicationsIn common: imageio, SimpleITK, tifffile, 10 other tools, mouse
- [6] doi:10.3389/frai.2026.1771088 [code]
- Few-shot deployment of pretrained MRI transformers in brain imaging tasks.Journal: Frontiers in artificial intelligenceIn common: imageio, SimpleITK, OpenCV, 10 other tools
- [7] doi:10.1016/j.patter.2026.101538 [code]
- A multi-modal foundation model for brain disease diagnosis and medical imaging.Journal: Patterns (New York, N.Y.)In common: tifffile, TensorFlow, OpenCV, 9 other tools, 1 reference
- [8] doi:10.3389/fnsys.2026.1822122 [code]
- Convergence-divergence circuits for multimodal integration of innate and learned opponent valences.Journal: Frontiers in systems neuroscienceIn common: tifffile, Keras, TensorFlow, 10 other tools
- [9] doi:10.1080/07853890.2026.2685416 [code]
- Pulmonary and cerebral damage in COVID-19 survivors: is there any association?Journal: Annals of medicineIn common: imageio, SimpleITK, Keras, 9 other tools
- [10] doi:10.1038/s42003-026-10957-8 [code]
- Brain defence by the extracellular matrix protein Cochlin.Journal: Communications biologyIn common: Keras, TensorFlow, OpenCV, 9 other tools, mouse, 1 reference
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 40 scripts, and 6 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:b01f97be1f78b2bc…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
