OSCR

Generalized plaque digitization framework for multi-dimensional mesoscopic images.

Code ↔ Paper

6 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 6 matches
  1. [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. [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. [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. [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. [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. [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

  1. '''
  2. Model network construction.
  3. '''
  4. import os
  5. import random
  6. import datetime
  7. import re
  8. import math
  9. import logging
  10. from collections import OrderedDict
  11. import multiprocessing
  12. import numpy as np
  13. import tensorflow as tf
  14. import keras
  15. import keras.backend as K
  16. import keras.layers as KL
  17. import keras.engine as KE
  18. import keras.models as KM
  19. import logging
  20. import matplotlib.pyplot as plt
  21. from mrcnn import utils
  22. # Requires TensorFlow 1.3+ and Keras 2.0.8+.
  23. from distutils.version import LooseVersion
  24. assert LooseVersion(tf.__version__) >= LooseVersion("1.3")
  25. assert LooseVersion(keras.__version__) >= LooseVersion('2.0.8')
  26. def log(text, array=None):
  27. if array is not None:
  28. text = text.ljust(25)
  29. text += ("shape: {:20} ".format(str(array.shape)))
  30. if array.size:
  31. text += ("min: {:10.5f} max: {:10.5f}".format(array.min(),array.max()))
  32. else:
  33. text += ("min: {:10} max: {:10}".format("",""))
  34. text += " {}".format(array.dtype)
  35. print(text)
  36. class BatchNorm(KL.BatchNormalization):
  37. def call(self, inputs, training=None):
  38. return super(self.__class__, self).call(inputs, training=training)
  39. def compute_backbone_shapes(config, image_shape):
  40. if callable(config.BACKBONE):
  41. return config.COMPUTE_BACKBONE_SHAPE(image_shape)
  42. # Currently supports ResNet only
  43. assert config.BACKBONE in ["resnet50", "resnet101"]
  44. return np.array(
  45. [[int(math.ceil(image_shape[0] / stride)),
  46. int(math.ceil(image_shape[1] / stride))]
  47. for stride in config.BACKBONE_STRIDES])
  48. # Resnet Graph
  49. def identity_block(input_tensor, kernel_size, filters, stage, block,
  50. use_bias=True, train_bn=True):
  51. nb_filter1, nb_filter2, nb_filter3 = filters
  52. conv_name_base = 'res' + str(stage) + block + '_branch'
  53. bn_name_base = 'bn' + str(stage) + block + '_branch'
  54. x = KL.Conv2D(nb_filter1, (1, 1), name=conv_name_base + '2a',
  55. use_bias=use_bias)(input_tensor)
  56. x = BatchNorm(name=bn_name_base + '2a')(x, training=train_bn)
  57. x = KL.Activation('relu')(x)
  58. x = KL.Conv2D(nb_filter2, (kernel_size, kernel_size), padding='same',
  59. name=conv_name_base + '2b', use_bias=use_bias)(x)
  60. x = BatchNorm(name=bn_name_base + '2b')(x, training=train_bn)
  61. x = KL.Activation('relu')(x)
  62. x = KL.Conv2D(nb_filter3, (1, 1), name=conv_name_base + '2c',
  63. use_bias=use_bias)(x)
  64. x = BatchNorm(name=bn_name_base + '2c')(x, training=train_bn)
  65. x = KL.Add()([x, input_tensor])
  66. x = KL.Activation('relu', name='res' + str(stage) + block + '_out')(x)
  67. return x
  68. def conv_block(input_tensor, kernel_size, filters, stage, block,
  69. strides=(2, 2), use_bias=True, train_bn=True):
  70. nb_filter1, nb_filter2, nb_filter3 = filters
  71. conv_name_base = 'res' + str(stage) + block + '_branch'
  72. bn_name_base = 'bn' + str(stage) + block + '_branch'
  73. x = KL.Conv2D(nb_filter1, (1, 1), strides=strides,
  74. name=conv_name_base + '2a', use_bias=use_bias)(input_tensor)
  75. x = BatchNorm(name=bn_name_base + '2a')(x, training=train_bn)
  76. x = KL.Activation('relu')(x)
  77. x = KL.Conv2D(nb_filter2, (kernel_size, kernel_size), padding='same',
  78. name=conv_name_base + '2b', use_bias=use_bias)(x)
  79. x = BatchNorm(name=bn_name_base + '2b')(x, training=train_bn)
  80. x = KL.Activation('relu')(x)
  81. x = KL.Conv2D(nb_filter3, (1, 1), name=conv_name_base +
  82. '2c', use_bias=use_bias)(x)
  83. x = BatchNorm(name=bn_name_base + '2c')(x, training=train_bn)
  84. shortcut = KL.Conv2D(nb_filter3, (1, 1), strides=strides,
  85. name=conv_name_base + '1', use_bias=use_bias)(input_tensor)
  86. shortcut = BatchNorm(name=bn_name_base + '1')(shortcut, training=train_bn)
  87. x = KL.Add()([x, shortcut])
  88. x = KL.Activation('relu', name='res' + str(stage) + block + '_out')(x)
  89. return x
  90. def resnet_graph(input_image, architecture, stage5=False, train_bn=True):
  91. assert architecture in ["resnet50", "resnet101"]
  92. # Stage 1
  93. x = KL.ZeroPadding2D((3, 3))(input_image)
  94. x = KL.Conv2D(64, (7, 7), strides=(2, 2), name='conv1', use_bias=True)(x)
  95. x = BatchNorm(name='bn_conv1')(x, training=train_bn)
  96. x = KL.Activation('relu')(x)
  97. C1 = x = KL.MaxPooling2D((3, 3), strides=(2, 2), padding="same")(x)
  98. # Stage 2
  99. x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1), train_bn=train_bn)
  100. x = identity_block(x, 3, [64, 64, 256], stage=2, block='b', train_bn=train_bn)
  101. C2 = x = identity_block(x, 3, [64, 64, 256], stage=2, block='c', train_bn=train_bn)
  102. # Stage 3
  103. x = conv_block(x, 3, [128, 128, 512], stage=3, block='a', train_bn=train_bn)
  104. x = identity_block(x, 3, [128, 128, 512], stage=3, block='b', train_bn=train_bn)
  105. x = identity_block(x, 3, [128, 128, 512], stage=3, block='c', train_bn=train_bn)
  106. C3 = x = identity_block(x, 3, [128, 128, 512], stage=3, block='d', train_bn=train_bn)
  107. # Stage 4
  108. x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a', train_bn=train_bn)
  109. block_count = {"resnet50": 5, "resnet101": 22}[architecture]
  110. for i in range(block_count):
  111. x = identity_block(x, 3, [256, 256, 1024], stage=4, block=chr(98 + i), train_bn=train_bn)
  112. C4 = x
  113. # Stage 5
  114. if stage5:
  115. x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a', train_bn=train_bn)
  116. x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b', train_bn=train_bn)
  117. C5 = x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c', train_bn=train_bn)
  118. else:
  119. C5 = None
  120. return [C1, C2, C3, C4, C5]
  121. # Proposal Layer
  122. def apply_box_deltas_graph(boxes, deltas):
  123. # Convert to y, x, h, w
  124. height = boxes[:, 2] - boxes[:, 0]
  125. width = boxes[:, 3] - boxes[:, 1]
  126. center_y = boxes[:, 0] + 0.5 * height
  127. center_x = boxes[:, 1] + 0.5 * width
  128. # Apply deltas
  129. center_y += deltas[:, 0] * height
  130. center_x += deltas[:, 1] * width
  131. height *= tf.exp(deltas[:, 2])
  132. width *= tf.exp(deltas[:, 3])
  133. # Convert back to y1, x1, y2, x2
  134. y1 = center_y - 0.5 * height
  135. x1 = center_x - 0.5 * width
  136. y2 = y1 + height
  137. x2 = x1 + width
  138. result = tf.stack([y1, x1, y2, x2], axis=1, name="apply_box_deltas_out")
  139. return result
  140. def clip_boxes_graph(boxes, window):
  141. # Split
  142. wy1, wx1, wy2, wx2 = tf.split(window, 4)
  143. y1, x1, y2, x2 = tf.split(boxes, 4, axis=1)
  144. # Clip
  145. y1 = tf.maximum(tf.minimum(y1, wy2), wy1)
  146. x1 = tf.maximum(tf.minimum(x1, wx2), wx1)
  147. y2 = tf.maximum(tf.minimum(y2, wy2), wy1)
  148. x2 = tf.maximum(tf.minimum(x2, wx2), wx1)
  149. clipped = tf.concat([y1, x1, y2, x2], axis=1, name="clipped_boxes")
  150. clipped.set_shape((clipped.shape[0], 4))
  151. return clipped
  152. class ProposalLayer(KE.Layer):
  153. def __init__(self, proposal_count, nms_threshold, config=None, **kwargs):
  154. super(ProposalLayer, self).__init__(**kwargs)
  155. self.config = config
  156. self.proposal_count = proposal_count
  157. self.nms_threshold = nms_threshold
  158. def call(self, inputs):
  159. # Box Scores. Use the foreground class confidence. [Batch, num_rois, 1]
  160. scores = inputs[0][:, :, 1]
  161. # Box deltas [batch, num_rois, 4]
  162. deltas = inputs[1]
  163. # [0.1 0.1 0.2 0.2]
  164. deltas = deltas * np.reshape(self.config.RPN_BBOX_STD_DEV, [1, 1, 4])
  165. # Anchors
  166. anchors = inputs[2]
  167. # Improve performance by trimming to top anchors by score
  168. # and doing the rest on the smaller subset.
  169. pre_nms_limit = tf.minimum(self.config.PRE_NMS_LIMIT, tf.shape(anchors)[1])
  170. ix = tf.nn.top_k(scores, pre_nms_limit, sorted=True,
  171. name="top_anchors").indices
  172. scores = utils.batch_slice([scores, ix], lambda x, y: tf.gather(x, y),
  173. self.config.IMAGES_PER_GPU)
  174. deltas = utils.batch_slice([deltas, ix], lambda x, y: tf.gather(x, y),
  175. self.config.IMAGES_PER_GPU)
  176. pre_nms_anchors = utils.batch_slice([anchors, ix], lambda a, x: tf.gather(a, x),
  177. self.config.IMAGES_PER_GPU,
  178. names=["pre_nms_anchors"])
  179. # Apply deltas to anchors to get refined anchors.
  180. # [batch, N, (y1, x1, y2, x2)]
  181. boxes = utils.batch_slice([pre_nms_anchors, deltas],
  182. lambda x, y: apply_box_deltas_graph(x, y),
  183. self.config.IMAGES_PER_GPU,
  184. names=["refined_anchors"])
  185. # Clip to image boundaries. Since we're in normalized coordinates,
  186. # clip to 0..1 range. [batch, N, (y1, x1, y2, x2)]
  187. window = np.array([0, 0, 1, 1], dtype=np.float32)
  188. boxes = utils.batch_slice(boxes,
  189. lambda x: clip_boxes_graph(x, window),
  190. self.config.IMAGES_PER_GPU,
  191. names=["refined_anchors_clipped"])
  192. # Filter out small boxes
  193. # According to Xinlei Chen's paper, this reduces detection accuracy
  194. # for small objects, so we're skipping it.
  195. # Non-max suppression
  196. def nms(boxes, scores):
  197. indices = tf.image.non_max_suppression(
  198. boxes, scores, self.proposal_count,
  199. self.nms_threshold, name="rpn_non_max_suppression")
  200. proposals = tf.gather(boxes, indices)
  201. # Pad if needed
  202. padding = tf.maximum(self.proposal_count - tf.shape(proposals)[0], 0)
  203. proposals = tf.pad(proposals, [(0, padding), (0, 0)])
  204. return proposals
  205. proposals = utils.batch_slice([boxes, scores], nms,
  206. self.config.IMAGES_PER_GPU)
  207. return proposals
  208. def compute_output_shape(self, input_shape):
  209. return (None, self.proposal_count, 4)
  210. # ROIAlign Layer
  211. def log2_graph(x):
  212. return tf.log(x) / tf.log(2.0)
  213. class PyramidROIAlign(KE.Layer):
  214. def __init__(self, pool_shape, **kwargs):
  215. super(PyramidROIAlign, self).__init__(**kwargs)
  216. self.pool_shape = tuple(pool_shape)
  217. def call(self, inputs):
  218. # Crop boxes [batch, num_boxes, (y1, x1, y2, x2)] in normalized coords
  219. boxes = inputs[0]
  220. # Image meta
  221. # Holds details about the image. See compose_image_meta()
  222. image_meta = inputs[1]
  223. # Feature Maps. List of feature maps from different level of the
  224. # feature pyramid. Each is [batch, height, width, channels]
  225. feature_maps = inputs[2:]
  226. # Assign each ROI to a level in the pyramid based on the ROI area.
  227. y1, x1, y2, x2 = tf.split(boxes, 4, axis=2)
  228. h = y2 - y1
  229. w = x2 - x1
  230. # Use shape of first image. Images in a batch must have the same size.
  231. image_shape = parse_image_meta_graph(image_meta)['image_shape'][0]
  232. # Equation 1 in the Feature Pyramid Networks paper. Account for
  233. # the fact that our coordinates are normalized here.
  234. image_area = tf.cast(image_shape[0] * image_shape[1], tf.float32)
  235. roi_level = log2_graph(tf.sqrt(h * w) / (224.0 / tf.sqrt(image_area)))
  236. roi_level = tf.minimum(5, tf.maximum(
  237. 2, 4 + tf.cast(tf.round(roi_level), tf.int32)))
  238. roi_level = tf.squeeze(roi_level, 2)
  239. # Loop through levels and apply ROI pooling to each. P2 to P5.
  240. pooled = []
  241. box_to_level = []
  242. for i, level in enumerate(range(2, 6)):
  243. ix = tf.where(tf.equal(roi_level, level))
  244. level_boxes = tf.gather_nd(boxes, ix)
  245. # Box indices for crop_and_resize.
  246. box_indices = tf.cast(ix[:, 0], tf.int32)
  247. # Keep track of which box is mapped to which level
  248. box_to_level.append(ix)
  249. # Stop gradient propogation to ROI proposals
  250. level_boxes = tf.stop_gradient(level_boxes)
  251. box_indices = tf.stop_gradient(box_indices)
  252. # Crop and Resize
  253. # Here we use the simplified approach of a single value per bin,
  254. # which is how it's done in tf.crop_and_resize()
  255. # Result: [batch * num_boxes, pool_height, pool_width, channels]
  256. pooled.append(tf.image.crop_and_resize(
  257. feature_maps[i], level_boxes, box_indices, self.pool_shape,
  258. method="bilinear"))
  259. # Pack pooled features into one tensor
  260. pooled = tf.concat(pooled, axis=0)
  261. # Pack box_to_level mapping into one array and add another
  262. # column representing the order of pooled boxes
  263. box_to_level = tf.concat(box_to_level, axis=0)
  264. box_range = tf.expand_dims(tf.range(tf.shape(box_to_level)[0]), 1)
  265. box_to_level = tf.concat([tf.cast(box_to_level, tf.int32), box_range],
  266. axis=1)
  267. # Rearrange pooled features to match the order of the original boxes
  268. # Sort box_to_level by batch then box index
  269. # TF doesn't have a way to sort by two columns, so merge them and sort.
  270. sorting_tensor = box_to_level[:, 0] * 100000 + box_to_level[:, 1]
  271. ix = tf.nn.top_k(sorting_tensor, k=tf.shape(
  272. box_to_level)[0]).indices[::-1]
  273. ix = tf.gather(box_to_level[:, 2], ix)
  274. pooled = tf.gather(pooled, ix)
  275. # Re-add the batch dimension reshape[batch, num_rois, POOL_SIZE, POOL_SIZE, channels]
  276. shape = tf.concat([tf.shape(boxes)[:2], tf.shape(pooled)[1:]], axis=0)
  277. pooled = tf.reshape(pooled, shape)
  278. return pooled
  279. def compute_output_shape(self, input_shape):
  280. return input_shape[0][:2] + self.pool_shape + (input_shape[2][-1], )
  281. # Detection Target Layer
  282. def overlaps_graph(boxes1, boxes2):
  283. # 1. Tile boxes2 and repeat boxes1.
  284. b1 = tf.reshape(tf.tile(tf.expand_dims(boxes1, 1),
  285. [1, 1, tf.shape(boxes2)[0]]), [-1, 4])
  286. b2 = tf.tile(boxes2, [tf.shape(boxes1)[0], 1])
  287. # 2. Compute intersections
  288. b1_y1, b1_x1, b1_y2, b1_x2 = tf.split(b1, 4, axis=1)
  289. b2_y1, b2_x1, b2_y2, b2_x2 = tf.split(b2, 4, axis=1)
  290. y1 = tf.maximum(b1_y1, b2_y1)
  291. x1 = tf.maximum(b1_x1, b2_x1)
  292. y2 = tf.minimum(b1_y2, b2_y2)
  293. x2 = tf.minimum(b1_x2, b2_x2)
  294. intersection = tf.maximum(x2 - x1, 0) * tf.maximum(y2 - y1, 0)
  295. # 3. Compute unions
  296. b1_area = (b1_y2 - b1_y1) * (b1_x2 - b1_x1)
  297. b2_area = (b2_y2 - b2_y1) * (b2_x2 - b2_x1)
  298. union = b1_area + b2_area - intersection
  299. # 4. Compute IoU and reshape to [boxes1, boxes2]
  300. iou = intersection / union
  301. overlaps = tf.reshape(iou, [tf.shape(boxes1)[0], tf.shape(boxes2)[0]])
  302. return overlaps
  303. def detection_targets_graph(proposals, gt_class_ids, gt_boxes, gt_masks, config):
  304. if tf.equal(tf.shape(proposals)[0], 0):
  305. rois = tf.zeros([config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
  306. target_class_ids = tf.zeros([config.TRAIN_ROIS_PER_IMAGE], dtype=tf.int32)
  307. target_bbox = tf.zeros([config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
  308. target_mask = tf.zeros([config.TRAIN_ROIS_PER_IMAGE, config.MASK_SHAPE[0],
  309. config.MASK_SHAPE[1]], dtype=tf.float32)
  310. return rois, target_class_ids, target_bbox, target_mask
  311. # Assertions
  312. # asserts = [
  313. # tf.Assert(tf.greater(tf.shape(proposals)[0], 0), [proposals],
  314. # name="roi_assertion"),
  315. # ]
  316. # with tf.control_dependencies(asserts):
  317. # proposals = tf.identity(proposals)
  318. # Remove zero padding
  319. proposals, _ = trim_zeros_graph(proposals, name="trim_proposals")
  320. gt_boxes, non_zeros = trim_zeros_graph(gt_boxes, name="trim_gt_boxes")
  321. gt_class_ids = tf.boolean_mask(gt_class_ids, non_zeros,
  322. name="trim_gt_class_ids")
  323. gt_masks = tf.gather(gt_masks, tf.where(non_zeros)[:, 0], axis=2,
  324. name="trim_gt_masks")
  325. # Handle COCO crowds
  326. crowd_ix = tf.where(gt_class_ids < 0)[:, 0]
  327. non_crowd_ix = tf.where(gt_class_ids > 0)[:, 0]
  328. crowd_boxes = tf.gather(gt_boxes, crowd_ix)
  329. gt_class_ids = tf.gather(gt_class_ids, non_crowd_ix)
  330. gt_boxes = tf.gather(gt_boxes, non_crowd_ix)
  331. gt_masks = tf.gather(gt_masks, non_crowd_ix, axis=2)
  332. # Compute overlaps matrix [proposals, gt_boxes]
  333. overlaps = overlaps_graph(proposals, gt_boxes)
  334. # Compute overlaps with crowd boxes [proposals, crowd_boxes]
  335. crowd_overlaps = overlaps_graph(proposals, crowd_boxes)
  336. crowd_iou_max = tf.reduce_max(crowd_overlaps, axis=1)
  337. no_crowd_bool = (crowd_iou_max < 0.001)
  338. # Determine positive and negative ROIs
  339. roi_iou_max = tf.reduce_max(overlaps, axis=1)
  340. # 1. Positive ROIs are those with >= 0.5 IoU with a GT box
  341. positive_roi_bool = (roi_iou_max >= 0.5)
  342. positive_indices = tf.where(positive_roi_bool)[:, 0]
  343. # 2. Negative ROIs are those with < 0.5 with every GT box. Skip crowds.
  344. negative_indices = tf.where(tf.logical_and(roi_iou_max < 0.5, no_crowd_bool))[:, 0]
  345. # Subsample ROIs. Aim for 33% positive
  346. positive_count = int(config.TRAIN_ROIS_PER_IMAGE *
  347. config.ROI_POSITIVE_RATIO)
  348. positive_indices = tf.random_shuffle(positive_indices)[:positive_count]
  349. positive_count = tf.shape(positive_indices)[0]
  350. # Negative ROIs. Add enough to maintain positive:negative ratio.
  351. r = 1.0 / config.ROI_POSITIVE_RATIO
  352. negative_count = tf.cast(r * tf.cast(positive_count, tf.float32), tf.int32) - positive_count
  353. negative_indices = tf.random_shuffle(negative_indices)[:negative_count]
  354. # Gather selected ROIs
  355. positive_rois = tf.gather(proposals, positive_indices)
  356. negative_rois = tf.gather(proposals, negative_indices)
  357. # Assign positive ROIs to GT boxes.
  358. positive_overlaps = tf.gather(overlaps, positive_indices)
  359. roi_gt_box_assignment = tf.cond(
  360. tf.greater(tf.shape(positive_overlaps)[1], 0),
  361. true_fn = lambda: tf.argmax(positive_overlaps, axis=1),
  362. false_fn = lambda: tf.cast(tf.constant([]),tf.int64)
  363. )
  364. roi_gt_boxes = tf.gather(gt_boxes, roi_gt_box_assignment)
  365. roi_gt_class_ids = tf.gather(gt_class_ids, roi_gt_box_assignment)
  366. # Compute bbox refinement for positive ROIs
  367. deltas = utils.box_refinement_graph(positive_rois, roi_gt_boxes)
  368. deltas /= config.BBOX_STD_DEV
  369. # Assign positive ROIs to GT masks
  370. transposed_masks = tf.expand_dims(tf.transpose(gt_masks, [2, 0, 1]), -1)
  371. roi_masks = tf.gather(transposed_masks, roi_gt_box_assignment)
  372. # Compute mask targets
  373. boxes = positive_rois
  374. if config.USE_MINI_MASK:
  375. # Transform ROI coordinates from normalized image space
  376. # to normalized mini-mask space.
  377. y1, x1, y2, x2 = tf.split(positive_rois, 4, axis=1)
  378. gt_y1, gt_x1, gt_y2, gt_x2 = tf.split(roi_gt_boxes, 4, axis=1)
  379. gt_h = gt_y2 - gt_y1
  380. gt_w = gt_x2 - gt_x1
  381. y1 = (y1 - gt_y1) / gt_h
  382. x1 = (x1 - gt_x1) / gt_w
  383. y2 = (y2 - gt_y1) / gt_h
  384. x2 = (x2 - gt_x1) / gt_w
  385. boxes = tf.concat([y1, x1, y2, x2], 1)
  386. box_ids = tf.range(0, tf.shape(roi_masks)[0])
  387. masks = tf.image.crop_and_resize(tf.cast(roi_masks, tf.float32), boxes,
  388. box_ids,
  389. config.MASK_SHAPE)
  390. # Remove the extra dimension from masks.
  391. masks = tf.squeeze(masks, axis=3)
  392. # Threshold mask pixels at 0.5 to have GT masks be 0 or 1 to use with
  393. # binary cross entropy loss.
  394. masks = tf.round(masks)
  395. # Append negative ROIs and pad bbox deltas and masks that
  396. # are not used for negative ROIs with zeros.
  397. rois = tf.concat([positive_rois, negative_rois], axis=0)
  398. N = tf.shape(negative_rois)[0]
  399. P = tf.maximum(config.TRAIN_ROIS_PER_IMAGE - tf.shape(rois)[0], 0)
  400. rois = tf.pad(rois, [(0, P), (0, 0)])
  401. roi_gt_boxes = tf.pad(roi_gt_boxes, [(0, N + P), (0, 0)])
  402. roi_gt_class_ids = tf.pad(roi_gt_class_ids, [(0, N + P)])
  403. deltas = tf.pad(deltas, [(0, N + P), (0, 0)])
  404. masks = tf.pad(masks, [[0, N + P], (0, 0), (0, 0)])
  405. return rois, roi_gt_class_ids, deltas, masks
  406. class DetectionTargetLayer(KE.Layer):
  407. def __init__(self, config, **kwargs):
  408. super(DetectionTargetLayer, self).__init__(**kwargs)
  409. self.config = config
  410. def call(self, inputs):
  411. proposals = inputs[0]
  412. gt_class_ids = inputs[1]
  413. gt_boxes = inputs[2]
  414. gt_masks = inputs[3]
  415. def empty_output():
  416. rois = tf.zeros([self.config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
  417. target_class_ids = tf.zeros([self.config.TRAIN_ROIS_PER_IMAGE], dtype=tf.int32)
  418. target_bbox = tf.zeros([self.config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
  419. target_mask = tf.zeros([self.config.TRAIN_ROIS_PER_IMAGE,
  420. self.config.MASK_SHAPE[0],
  421. self.config.MASK_SHAPE[1]], dtype=tf.float32)
  422. return rois, target_class_ids, target_bbox, target_mask
  423. def normal_output():
  424. # Slice the batch and run a graph for each slice
  425. names = ["rois", "target_class_ids", "target_bbox", "target_mask"]
  426. outputs = utils.batch_slice(
  427. [proposals, gt_class_ids, gt_boxes, gt_masks],
  428. lambda w, x, y, z: detection_targets_graph(
  429. w, x, y, z, self.config),
  430. self.config.IMAGES_PER_GPU, names=names)
  431. return outputs
  432. is_empty = tf.equal(tf.shape(proposals)[0], 0)
  433. return tf.cond(is_empty, empty_output, normal_output)
  434. def compute_output_shape(self, input_shape):
  435. return [
  436. (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # rois
  437. (None, self.config.TRAIN_ROIS_PER_IMAGE), # class_ids
  438. (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # deltas
  439. (None, self.config.TRAIN_ROIS_PER_IMAGE, self.config.MASK_SHAPE[0],
  440. self.config.MASK_SHAPE[1]) # masks
  441. ]
  442. def compute_mask(self, inputs, mask=None):
  443. return [None, None, None, None]
  444. class DetectionTargetLayerNoMask_old(KE.Layer):
  445. """DetectionTargetLayer without a mask"""
  446. def __init__(self, config, **kwargs):
  447. super(DetectionTargetLayerNoMask, self).__init__(**kwargs)
  448. self.config = config
  449. def call(self, inputs):
  450. proposals = inputs[0]
  451. gt_class_ids = inputs[1]
  452. gt_boxes = inputs[2]
  453. gt_class_ids = tf.where(
  454. gt_class_ids > 0,
  455. tf.ones_like(gt_class_ids),
  456. tf.zeros_like(gt_class_ids)
  457. )
  458. def get_normal_output():
  459. # Retrieve the batch size and num_rois for the proposals
  460. proposal_shape = tf.shape(proposals)
  461. batch_size = proposal_shape[0]
  462. num_rois = proposal_shape[1]
  463. # Create a dummy_masks shape that matches the proposals [batch, num_rois, 0, 0]
  464. # shape[0] = batch_size > 0
  465. dummy_masks = tf.zeros([batch_size, num_rois, 0, 0])
  466. names = ["rois", "target_class_ids", "target_bbox", "target_mask"]
  467. outputs = utils.batch_slice(
  468. [proposals, gt_class_ids, gt_boxes, dummy_masks],
  469. lambda w, x, y, z: detection_targets_graph(
  470. w, x, y, z, self.config),
  471. self.config.IMAGES_PER_GPU,
  472. names=names)
  473. return [outputs[0], outputs[1], outputs[2]]
  474. def get_empty_output():
  475. batch_size = self.config.IMAGES_PER_GPU
  476. rois = tf.zeros([batch_size, self.config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
  477. target_class_ids = tf.zeros([batch_size, self.config.TRAIN_ROIS_PER_IMAGE], dtype=tf.int32)
  478. target_bbox = tf.zeros([batch_size, self.config.TRAIN_ROIS_PER_IMAGE, 4], dtype=tf.float32)
  479. return [rois, target_class_ids, target_bbox]
  480. is_empty = tf.equal(tf.shape(proposals)[0], 0)
  481. return tf.cond(is_empty, get_empty_output, get_normal_output)
  482. def compute_output_shape(self, input_shape):
  483. return [
  484. (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # rois
  485. (None, self.config.TRAIN_ROIS_PER_IMAGE), # class_ids
  486. (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # deltas
  487. ]
  488. class DetectionTargetLayerNoMask(KE.Layer):
  489. """DetectionTargetLayer (without mask)"""
  490. def __init__(self, config, **kwargs):
  491. super(DetectionTargetLayerNoMask, self).__init__(**kwargs)
  492. self.config = config
  493. def call(self, inputs):
  494. proposals = inputs[0]
  495. gt_class_ids = inputs[1]
  496. gt_boxes = inputs[2]
  497. gt_class_ids = tf.where(
  498. gt_class_ids > 0,
  499. tf.ones_like(gt_class_ids),
  500. tf.zeros_like(gt_class_ids)
  501. )
  502. def process_single_sample(prop, gt_cls, gt_box):
  503. """Proposals for processing individual samples"""
  504. num_proposals = tf.shape(prop)[0]
  505. num_rois = self.config.TRAIN_ROIS_PER_IMAGE
  506. indices = tf.random_shuffle(tf.range(num_proposals))[:num_rois]
  507. selected_rois = tf.gather(prop, indices)
  508. # Create target classes (for simplicity: set all to 1, i.e. foreground)
  509. num_selected = tf.shape(selected_rois)[0]
  510. target_class_ids = tf.ones([num_selected], dtype=tf.int32)
  511. # Create bbox deltas (simplified: all zeros, i.e. no adjustment)
  512. target_bbox = tf.zeros([num_selected, 4], dtype=tf.float32)
  513. # Padding
  514. pad_size = num_rois - num_selected
  515. rois_padded = tf.pad(selected_rois, [[0, pad_size], [0, 0]])
  516. target_class_ids_padded = tf.pad(target_class_ids, [[0, pad_size]])
  517. target_bbox_padded = tf.pad(target_bbox, [[0, pad_size], [0, 0]])
  518. return rois_padded, target_class_ids_padded, target_bbox_padded
  519. # batch
  520. names = ["rois", "target_class_ids", "target_bbox"]
  521. outputs = utils.batch_slice(
  522. [proposals, gt_class_ids, gt_boxes],
  523. lambda p, c, b: process_single_sample(p, c, b),
  524. self.config.IMAGES_PER_GPU,
  525. names=names)
  526. return [outputs[0], outputs[1], outputs[2]]
  527. def compute_output_shape(self, input_shape):
  528. return [
  529. (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # rois
  530. (None, self.config.TRAIN_ROIS_PER_IMAGE), # class_ids
  531. (None, self.config.TRAIN_ROIS_PER_IMAGE, 4), # deltas
  532. ]
  533. def compute_mask(self, inputs, mask=None):
  534. return [None, None, None]
  535. # Detection Layer
  536. def refine_detections_graph(rois, probs, deltas, window, config):
  537. # Class IDs per ROI
  538. class_ids = tf.argmax(probs, axis=1, output_type=tf.int32)
  539. # Class probability of the top class of each ROI
  540. indices = tf.stack([tf.range(probs.shape[0]), class_ids], axis=1)
  541. class_scores = tf.gather_nd(probs, indices)
  542. # Class-specific bounding box deltas
  543. deltas_specific = tf.gather_nd(deltas, indices)
  544. # Apply bounding box deltas
  545. # Shape: [boxes, (y1, x1, y2, x2)] in normalized coordinates
  546. refined_rois = apply_box_deltas_graph(
  547. rois, deltas_specific * config.BBOX_STD_DEV)
  548. # Clip boxes to image window 防止超出0-1
  549. refined_rois = clip_boxes_graph(refined_rois, window)
  550. # Filter out background boxes
  551. keep = tf.where(class_ids > 0)[:, 0]
  552. # Filter out low confidence boxes
  553. if config.DETECTION_MIN_CONFIDENCE:
  554. conf_keep = tf.where(class_scores >= config.DETECTION_MIN_CONFIDENCE)[:, 0]
  555. keep = tf.sets.set_intersection(tf.expand_dims(keep, 0),
  556. tf.expand_dims(conf_keep, 0))
  557. keep = tf.sparse_tensor_to_dense(keep)[0]
  558. # Apply per-class NMS
  559. # 1. Prepare variables
  560. pre_nms_class_ids = tf.gather(class_ids, keep)
  561. pre_nms_scores = tf.gather(class_scores, keep)
  562. pre_nms_rois = tf.gather(refined_rois, keep)
  563. unique_pre_nms_class_ids = tf.unique(pre_nms_class_ids)[0]
  564. def nms_keep_map(class_id):
  565. """Apply Non-Maximum Suppression on ROIs of the given class."""
  566. # Indices of ROIs of the given class
  567. ixs = tf.where(tf.equal(pre_nms_class_ids, class_id))[:, 0]
  568. # Apply NMS
  569. class_keep = tf.image.non_max_suppression(
  570. tf.gather(pre_nms_rois, ixs),
  571. tf.gather(pre_nms_scores, ixs),
  572. max_output_size=config.DETECTION_MAX_INSTANCES,
  573. iou_threshold=config.DETECTION_NMS_THRESHOLD)
  574. # Map indices
  575. class_keep = tf.gather(keep, tf.gather(ixs, class_keep))
  576. # Pad with -1 so returned tensors have the same shape
  577. gap = config.DETECTION_MAX_INSTANCES - tf.shape(class_keep)[0]
  578. class_keep = tf.pad(class_keep, [(0, gap)],
  579. mode='CONSTANT', constant_values=-1)
  580. # Set shape so map_fn() can infer result shape
  581. class_keep.set_shape([config.DETECTION_MAX_INSTANCES])
  582. return class_keep
  583. # 2. Map over class IDs 进行nms
  584. nms_keep = tf.map_fn(nms_keep_map, unique_pre_nms_class_ids,
  585. dtype=tf.int64)
  586. # 3. Merge results into one list, and remove -1 padding
  587. nms_keep = tf.reshape(nms_keep, [-1])
  588. nms_keep = tf.gather(nms_keep, tf.where(nms_keep > -1)[:, 0])
  589. # 4. Compute intersection between keep and nms_keep
  590. keep = tf.sets.set_intersection(tf.expand_dims(keep, 0),
  591. tf.expand_dims(nms_keep, 0))
  592. keep = tf.sparse_tensor_to_dense(keep)[0]
  593. # Keep top detections
  594. roi_count = config.DETECTION_MAX_INSTANCES
  595. class_scores_keep = tf.gather(class_scores, keep)
  596. num_keep = tf.minimum(tf.shape(class_scores_keep)[0], roi_count)
  597. top_ids = tf.nn.top_k(class_scores_keep, k=num_keep, sorted=True)[1]
  598. keep = tf.gather(keep, top_ids)
  599. # Arrange output as [N, (y1, x1, y2, x2, class_id, score)]
  600. # Coordinates are normalized.
  601. detections = tf.concat([
  602. tf.gather(refined_rois, keep),
  603. tf.to_float(tf.gather(class_ids, keep))[..., tf.newaxis],
  604. tf.gather(class_scores, keep)[..., tf.newaxis]
  605. ], axis=1)
  606. # Pad with zeros if detections < DETECTION_MAX_INSTANCES
  607. gap = config.DETECTION_MAX_INSTANCES - tf.shape(detections)[0]
  608. detections = tf.pad(detections, [(0, gap), (0, 0)], "CONSTANT")
  609. return detections
  610. def refine_detections_graph_binary(rois, scores, deltas, window, config):
  611. """
  612. The binary classification version of refine_detections_graph
  613. rois: [num_rois, 4]
  614. scores: [num_rois]
  615. deltas: [num_rois, 4]
  616. """
  617. # bbox deltas
  618. refined_rois = apply_box_deltas_graph(rois, deltas * config.BBOX_STD_DEV)
  619. refined_rois = clip_boxes_graph(refined_rois, window)
  620. y1, x1, y2, x2 = tf.split(refined_rois, 4, axis=1)
  621. areas = (y2 - y1) * (x2 - x1)
  622. valid_mask = tf.squeeze(areas > 0, axis=1)
  623. confidence_mask = scores >= config.DETECTION_MIN_CONFIDENCE
  624. keep_mask = tf.logical_and(valid_mask, confidence_mask)
  625. keep = tf.where(keep_mask)[:, 0]
  626. def true_fn():
  627. # NMS
  628. nms_keep = tf.image.non_max_suppression(
  629. tf.gather(refined_rois, keep),
  630. tf.gather(scores, keep),
  631. max_output_size=config.DETECTION_MAX_INSTANCES,
  632. iou_threshold=config.DETECTION_NMS_THRESHOLD)
  633. keep_filtered = tf.gather(keep, nms_keep)
  634. # [N, (y1, x1, y2, x2, class_id, score)]
  635. # class_id = 1
  636. detections = tf.concat([
  637. tf.gather(refined_rois, keep_filtered),
  638. tf.ones([tf.shape(keep_filtered)[0], 1], dtype=tf.float32), # class_id=1
  639. tf.gather(scores, keep_filtered)[..., tf.newaxis]
  640. ], axis=1)
  641. return detections
  642. def false_fn():
  643. detections = tf.zeros([0, 6], dtype=tf.float32)
  644. return detections
  645. detections = tf.cond(
  646. tf.greater(tf.size(keep), 0),
  647. true_fn,
  648. false_fn
  649. )
  650. gap = config.DETECTION_MAX_INSTANCES - tf.shape(detections)[0]
  651. detections = tf.pad(detections, [(0, gap), (0, 0)], "CONSTANT")
  652. return detections
  653. class DetectionLayer(KE.Layer):
  654. def __init__(self, config=None, **kwargs):
  655. super(DetectionLayer, self).__init__(**kwargs)
  656. self.config = config
  657. def call(self, inputs):
  658. rois = inputs[0]
  659. mrcnn_class = inputs[1]
  660. mrcnn_bbox = inputs[2]
  661. image_meta = inputs[3]
  662. # Get windows of images in normalized coordinates. Windows are the area
  663. # in the image that excludes the padding.
  664. # Use the shape of the first image in the batch to normalize the window
  665. # because we know that all images get resized to the same size.
  666. m = parse_image_meta_graph(image_meta)
  667. image_shape = m['image_shape'][0]
  668. window = norm_boxes_graph(m['window'], image_shape[:2])
  669. foreground_scores = mrcnn_class[:, :, 1]
  670. foreground_bbox = mrcnn_bbox[:, :, 1, :]
  671. # Run detection refinement graph on each item in the batch
  672. # detections_batch = utils.batch_slice(
  673. # [rois, mrcnn_class, mrcnn_bbox, window],
  674. # lambda x, y, w, z: refine_detections_graph(x, y, w, z, self.config),
  675. # self.config.IMAGES_PER_GPU)
  676. detections_batch = utils.batch_slice(
  677. [rois, foreground_scores, foreground_bbox, window],
  678. lambda x, y, w, z: refine_detections_graph_binary(x, y, w, z, self.config),
  679. self.config.IMAGES_PER_GPU)
  680. # Reshape output
  681. # [batch, num_detections, (y1, x1, y2, x2, class_id, class_score)] in
  682. # normalized coordinates
  683. return tf.reshape(
  684. detections_batch,
  685. [self.config.BATCH_SIZE, self.config.DETECTION_MAX_INSTANCES, 6])
  686. def compute_output_shape(self, input_shape):
  687. return (None, self.config.DETECTION_MAX_INSTANCES, 6)
  688. # Region Proposal Network (RPN)
  689. def rpn_graph(feature_map, anchors_per_location, anchor_stride):
  690. # Shared convolutional base of the RPN
  691. shared = KL.Conv2D(512, (3, 3), padding='same', activation='relu',
  692. strides=anchor_stride,
  693. name='rpn_conv_shared')(feature_map)
  694. # Anchor Score. [batch, height, width, anchors per location * 2].
  695. x = KL.Conv2D(2 * anchors_per_location, (1, 1), padding='valid',
  696. activation='linear', name='rpn_class_raw')(shared)
  697. # Reshape to [batch, anchors, 2]
  698. rpn_class_logits = KL.Lambda(
  699. lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 2]))(x)
  700. # Softmax on last dimension of BG/FG.
  701. rpn_probs = KL.Activation(
  702. "softmax", name="rpn_class_xxx")(rpn_class_logits)
  703. # Bounding box refinement. [batch, H, W, anchors per location * depth]
  704. # where depth is [x, y, log(w), log(h)]
  705. x = KL.Conv2D(anchors_per_location * 4, (1, 1), padding="valid",
  706. activation='linear', name='rpn_bbox_pred')(shared)
  707. # Reshape to [batch, anchors, 4]
  708. rpn_bbox = KL.Lambda(lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 4]))(x)
  709. return [rpn_class_logits, rpn_probs, rpn_bbox]
  710. def build_rpn_model(anchor_stride, anchors_per_location, depth):
  711. input_feature_map = KL.Input(shape=[None, None, depth],
  712. name="input_rpn_feature_map")
  713. outputs = rpn_graph(input_feature_map, anchors_per_location, anchor_stride)
  714. return KM.Model([input_feature_map], outputs, name="rpn_model")
  715. # Feature Pyramid Network Heads
  716. def fpn_classifier_graph(rois, feature_maps, image_meta,
  717. pool_size, num_classes, train_bn=True,
  718. fc_layers_size=1024):
  719. """
  720. num_classes = 2
  721. Returns:
  722. logits: [batch, num_rois, NUM_CLASSES] classifier logits (before softmax)
  723. probs: [batch, num_rois, NUM_CLASSES] classifier probabilities
  724. bbox_deltas: [batch, num_rois, NUM_CLASSES, (dy, dx, log(dh), log(dw))] Deltas to apply to
  725. proposal boxes
  726. """
  727. # ROI Pooling
  728. # Shape: [batch, num_rois, POOL_SIZE, POOL_SIZE, channels]
  729. x = PyramidROIAlign([pool_size, pool_size],
  730. name="roi_align_classifier")([rois, image_meta] + feature_maps)
  731. # Two 1024 FC layers (implemented with Conv2D for consistency)
  732. # Shape:[batch, num_rois, 1, 1, fc_layers_size]
  733. x = KL.TimeDistributed(KL.Conv2D(fc_layers_size, (pool_size, pool_size), padding="valid"),
  734. name="mrcnn_class_conv1")(x)
  735. x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn1')(x, training=train_bn)
  736. x = KL.Activation('relu')(x)
  737. x = KL.TimeDistributed(KL.Conv2D(fc_layers_size, (1, 1)),
  738. name="mrcnn_class_conv2")(x)
  739. x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn2')(x, training=train_bn)
  740. x = KL.Activation('relu')(x)
  741. # Shape:[batch, num_rois, fc_layers_size]
  742. shared = KL.Lambda(lambda x: K.squeeze(K.squeeze(x, 3), 2),
  743. name="pool_squeeze")(x)
  744. # Classifier head
  745. mrcnn_class_logits = KL.TimeDistributed(KL.Dense(num_classes),
  746. name='mrcnn_class_logits')(shared)
  747. mrcnn_probs = KL.TimeDistributed(KL.Activation("softmax"),
  748. name="mrcnn_class")(mrcnn_class_logits)
  749. # BBox head
  750. # [batch, num_rois, NUM_CLASSES * (dy, dx, log(dh), log(dw))]
  751. x = KL.TimeDistributed(KL.Dense(num_classes * 4, activation='linear'),
  752. name='mrcnn_bbox_fc')(shared)
  753. # Reshape to [batch, num_rois, NUM_CLASSES, (dy, dx, log(dh), log(dw))]
  754. s = K.int_shape(x)
  755. mrcnn_bbox = KL.Reshape((s[1], num_classes, 4), name="mrcnn_bbox")(x)
  756. return mrcnn_class_logits, mrcnn_probs, mrcnn_bbox
  757. def build_fpn_mask_graph(rois, feature_maps, image_meta,
  758. pool_size, num_classes, train_bn=True):
  759. """
  760. Returns: Masks [batch, num_rois, MASK_POOL_SIZE, MASK_POOL_SIZE, NUM_CLASSES]
  761. """
  762. # ROI Pooling
  763. # Shape: [batch, num_rois, MASK_POOL_SIZE, MASK_POOL_SIZE, channels]
  764. x = PyramidROIAlign([pool_size, pool_size],
  765. name="roi_align_mask")([rois, image_meta] + feature_maps)
  766. # Conv layers
  767. x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
  768. name="mrcnn_mask_conv1")(x)
  769. x = KL.TimeDistributed(BatchNorm(),
  770. name='mrcnn_mask_bn1')(x, training=train_bn)
  771. x = KL.Activation('relu')(x)
  772. x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
  773. name="mrcnn_mask_conv2")(x)
  774. x = KL.TimeDistributed(BatchNorm(),
  775. name='mrcnn_mask_bn2')(x, training=train_bn)
  776. x = KL.Activation('relu')(x)
  777. x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
  778. name="mrcnn_mask_conv3")(x)
  779. x = KL.TimeDistributed(BatchNorm(),
  780. name='mrcnn_mask_bn3')(x, training=train_bn)
  781. x = KL.Activation('relu')(x)
  782. x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
  783. name="mrcnn_mask_conv4")(x)
  784. x = KL.TimeDistributed(BatchNorm(),
  785. name='mrcnn_mask_bn4')(x, training=train_bn)
  786. x = KL.Activation('relu')(x)
  787. # Shape: [batch, num_rois, 2*MASK_POOL_SIZE, MASK_POOL_SIZE, channels]
  788. x = KL.TimeDistributed(KL.Conv2DTranspose(256, (2, 2), strides=2, activation="relu"),
  789. name="mrcnn_mask_deconv")(x)
  790. x = KL.TimeDistributed(KL.Conv2D(num_classes, (1, 1), strides=1, activation="sigmoid"),
  791. name="mrcnn_mask")(x)
  792. return x
  793. # Loss Functions
  794. def smooth_l1_loss(y_true, y_pred):
  795. diff = K.abs(y_true - y_pred)
  796. less_than_one = K.cast(K.less(diff, 1.0), "float32")
  797. loss = (less_than_one * 0.5 * diff**2) + (1 - less_than_one) * (diff - 0.5)
  798. return loss
  799. def rpn_class_loss_graph(rpn_match, rpn_class_logits):
  800. # Squeeze last dim to simplify
  801. rpn_match = tf.squeeze(rpn_match, -1)
  802. # Get anchor classes. Convert the -1/+1 match to 0/1 values.
  803. anchor_class = K.cast(K.equal(rpn_match, 1), tf.int32)
  804. # Positive and Negative anchors contribute to the loss,
  805. # but neutral anchors (match value = 0) don't.
  806. indices = tf.where(K.not_equal(rpn_match, 0))
  807. # Pick rows that contribute to the loss and filter out the rest.
  808. rpn_class_logits = tf.gather_nd(rpn_class_logits, indices)
  809. anchor_class = tf.gather_nd(anchor_class, indices)
  810. # Cross entropy loss
  811. loss = K.sparse_categorical_crossentropy(target=anchor_class,
  812. output=rpn_class_logits,
  813. from_logits=True)
  814. loss = K.switch(tf.size(loss) > 0, K.mean(loss), tf.constant(0.0))
  815. return loss
  816. def rpn_bbox_loss_graph(config, target_bbox, rpn_match, rpn_bbox):
  817. """
  818. config: the model config object.
  819. target_bbox: [batch, max positive anchors, (dy, dx, log(dh), log(dw))].
  820. Uses 0 padding to fill in unsed bbox deltas.
  821. rpn_match: [batch, anchors, 1]. Anchor match type. 1=positive,
  822. -1=negative, 0=neutral anchor.
  823. rpn_bbox: [batch, anchors, (dy, dx, log(dh), log(dw))]
  824. """
  825. # Positive anchors contribute to the loss, but negative and
  826. # neutral anchors (match value of 0 or -1) don't.
  827. rpn_match = K.squeeze(rpn_match, -1)
  828. indices = tf.where(K.equal(rpn_match, 1))
  829. # Pick bbox deltas that contribute to the loss
  830. rpn_bbox = tf.gather_nd(rpn_bbox, indices)
  831. # Trim target bounding box deltas to the same length as rpn_bbox.
  832. batch_counts = K.sum(K.cast(K.equal(rpn_match, 1), tf.int32), axis=1)
  833. target_bbox = batch_pack_graph(target_bbox, batch_counts,
  834. config.IMAGES_PER_GPU)
  835. loss = smooth_l1_loss(target_bbox, rpn_bbox)
  836. loss = K.switch(tf.size(loss) > 0, K.mean(loss), tf.constant(0.0))
  837. return loss
  838. def mrcnn_class_loss_graph(target_class_ids, pred_class_logits,
  839. active_class_ids):
  840. """
  841. All predictions are included in the loss calculation
  842. """
  843. target_class_ids = tf.cast(target_class_ids, 'int64')
  844. # Standard cross-entropy loss
  845. loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
  846. labels=target_class_ids, logits=pred_class_logits)
  847. # Create a mask for valid ROIs (target_class_ids >= 0)
  848. valid_mask = tf.cast(tf.greater_equal(target_class_ids, 0), tf.float32)
  849. # use mask
  850. loss = loss * valid_mask
  851. # Calculate the average loss (calculate only for valid ROIs)
  852. total_loss = tf.reduce_sum(loss)
  853. num_valid = tf.reduce_sum(valid_mask)
  854. loss = tf.cond(tf.greater(num_valid, 0),
  855. lambda: total_loss / num_valid,
  856. lambda: tf.constant(0.0))
  857. return loss
  858. def mrcnn_bbox_loss_graph(target_bbox, target_class_ids, pred_bbox):
  859. """
  860. target_bbox: [batch, num_rois, (dy, dx, log(dh), log(dw))]
  861. target_class_ids: [batch, num_rois]. Integer class IDs.
  862. pred_bbox: [batch, num_rois, num_classes, (dy, dx, log(dh), log(dw))]
  863. """
  864. # Reshape to merge batch and roi dimensions for simplicity.
  865. target_class_ids = K.reshape(target_class_ids, (-1,))
  866. target_bbox = K.reshape(target_bbox, (-1, 4))
  867. pred_bbox = K.reshape(pred_bbox, (-1, K.int_shape(pred_bbox)[2], 4))
  868. # Only positive ROIs contribute to the loss. And only
  869. # the right class_id of each ROI. Get their indices.
  870. positive_roi_ix = tf.where(target_class_ids > 0)[:, 0]
  871. positive_roi_class_ids = tf.cast(
  872. tf.gather(target_class_ids, positive_roi_ix), tf.int64)
  873. indices = tf.stack([positive_roi_ix, positive_roi_class_ids], axis=1)
  874. # Gather the deltas (predicted and true) that contribute to loss
  875. target_bbox = tf.gather(target_bbox, positive_roi_ix)
  876. pred_bbox = tf.gather_nd(pred_bbox, indices)
  877. # Smooth-L1 Loss
  878. loss = K.switch(tf.size(target_bbox) > 0,
  879. smooth_l1_loss(y_true=target_bbox, y_pred=pred_bbox),
  880. tf.constant(0.0))
  881. loss = K.mean(loss)
  882. return loss
  883. def mrcnn_mask_loss_graph(target_masks, target_class_ids, pred_masks):
  884. """
  885. target_masks: [batch, num_rois, height, width].
  886. A float32 tensor of values 0 or 1. Uses zero padding to fill array.
  887. target_class_ids: [batch, num_rois]. Integer class IDs. Zero padded.
  888. pred_masks: [batch, proposals, height, width, num_classes] float32 tensor
  889. with values from 0 to 1.
  890. """
  891. # Reshape for simplicity. Merge first two dimensions into one.
  892. target_class_ids = K.reshape(target_class_ids, (-1,))
  893. mask_shape = tf.shape(target_masks)
  894. target_masks = K.reshape(target_masks, (-1, mask_shape[2], mask_shape[3]))
  895. pred_shape = tf.shape(pred_masks)
  896. pred_masks = K.reshape(pred_masks,
  897. (-1, pred_shape[2], pred_shape[3], pred_shape[4]))
  898. # Permute predicted masks to [N, num_classes, height, width]
  899. pred_masks = tf.transpose(pred_masks, [0, 3, 1, 2])
  900. # Only positive ROIs contribute to the loss. And only
  901. # the class specific mask of each ROI.
  902. positive_ix = tf.where(target_class_ids > 0)[:, 0]
  903. positive_class_ids = tf.cast(
  904. tf.gather(target_class_ids, positive_ix), tf.int64)
  905. indices = tf.stack([positive_ix, positive_class_ids], axis=1)
  906. # Gather the masks (predicted and true) that contribute to loss
  907. y_true = tf.gather(target_masks, positive_ix)
  908. y_pred = tf.gather_nd(pred_masks, indices)
  909. # Compute binary cross entropy. If no positive ROIs, then return 0.
  910. # shape: [batch, roi, num_classes]
  911. loss = K.switch(tf.size(y_true) > 0,
  912. K.binary_crossentropy(target=y_true, output=y_pred),
  913. tf.constant(0.0))
  914. loss = K.mean(loss)
  915. return loss
  916. # Data Generator
  917. def load_image_gt(dataset, config, image_id, augment=False, augmentation=None,
  918. use_mini_mask=False):
  919. # Load image and mask
  920. image = dataset.load_image(image_id)
  921. mask, class_ids = dataset.load_mask(image_id)
  922. box, class_ids = dataset.load_box(image_id)
  923. original_shape = image.shape
  924. image, window, scale, padding, crop = utils.resize_image(
  925. image,
  926. min_dim=config.IMAGE_MIN_DIM,
  927. min_scale=config.IMAGE_MIN_SCALE,
  928. max_dim=config.IMAGE_MAX_DIM,
  929. mode=config.IMAGE_RESIZE_MODE)
  930. mask = utils.resize_mask(mask, scale, padding, crop)
  931. # Random horizontal flips.
  932. if augment:
  933. logging.warning("'augment' is deprecated. Use 'augmentation' instead.")
  934. if random.randint(0, 1):
  935. image = np.fliplr(image)
  936. mask = np.fliplr(mask)
  937. # Augmentation
  938. if augmentation:
  939. import imgaug
  940. # Augmenters that are safe to apply to masks
  941. MASK_AUGMENTERS = ["Sequential", "SomeOf", "OneOf", "Sometimes",
  942. "Fliplr", "Flipud", "CropAndPad",
  943. "Affine", "PiecewiseAffine"]
  944. def hook(images, augmenter, parents, default):
  945. """Determines which augmenters to apply to masks."""
  946. return augmenter.__class__.__name__ in MASK_AUGMENTERS
  947. # Store shapes before augmentation to compare
  948. image_shape = image.shape
  949. mask_shape = mask.shape
  950. # Make augmenters deterministic to apply similarly to images and masks
  951. det = augmentation.to_deterministic()
  952. image = det.augment_image(image)
  953. # Change mask to np.uint8 because imgaug doesn't support np.bool
  954. mask = det.augment_image(mask.astype(np.uint8),
  955. hooks=imgaug.HooksImages(activator=hook))
  956. # Verify that shapes didn't change
  957. assert image.shape == image_shape, "Augmentation shouldn't change image size"
  958. assert mask.shape == mask_shape, "Augmentation shouldn't change mask size"
  959. # Change mask back to bool
  960. mask = mask.astype(np.bool)
  961. # Note that some boxes might be all zeros if the corresponding mask got cropped out.
  962. # and here is to filter them out
  963. _idx = np.sum(mask, axis=(0, 1)) > 0
  964. mask = mask[:, :, _idx]
  965. class_ids = class_ids[_idx]
  966. # Bounding boxes.
  967. # bbox: [num_instances, (y1, x1, y2, x2)]
  968. # bbox = utils.extract_bboxes(mask)
  969. bbox = utils.extract_bboxes_1(box)
  970. # Active classes
  971. # Different datasets have different classes, so track the
  972. # classes supported in the dataset of this image.
  973. active_class_ids = np.zeros([dataset.num_classes], dtype=np.int32)
  974. source_class_ids = dataset.source_class_ids[dataset.image_info[image_id]["source"]]
  975. active_class_ids[source_class_ids] = 1
  976. # Resize masks to smaller size to reduce memory usage
  977. if use_mini_mask:
  978. mask = utils.minimize_mask(bbox, mask, config.MINI_MASK_SHAPE)
  979. # Image meta data
  980. image_meta = compose_image_meta(image_id, original_shape, image.shape,
  981. window, scale, active_class_ids)
  982. return image, image_meta, class_ids, bbox, mask
  983. def load_image_gt_no_mask(dataset, config, image_id, augment=False, augmentation=None):
  984. """Load only the image and box, not the mask – for use with the optimised version of data_generator"""
  985. image = dataset.load_image(image_id)
  986. boxes, class_ids = dataset.load_box(image_id)
  987. if boxes is None or len(boxes) == 0 or (hasattr(boxes, 'shape') and boxes.shape[0] == 0):
  988. boxes = np.zeros((0, 4), dtype=np.int32)
  989. class_ids = np.zeros((0,), dtype=np.int32)
  990. elif hasattr(boxes, 'shape') and len(boxes.shape) == 2:
  991. if boxes.shape[0] == 4 and boxes.shape[1] > 0:
  992. boxes = boxes.T
  993. elif boxes.shape[1] != 4:
  994. if boxes.shape[0] == 4 and boxes.shape[1] == 0:
  995. boxes = np.zeros((0, 4), dtype=np.int32)
  996. class_ids = np.zeros((0,), dtype=np.int32)
  997. else:
  998. print(f"Warning: Unexpected boxes shape {boxes.shape} for image {image_id}")
  999. boxes = np.zeros((0, 4), dtype=np.int32)
  1000. class_ids = np.zeros((0,), dtype=np.int32)
  1001. else:
  1002. boxes = np.zeros((0, 4), dtype=np.int32)
  1003. class_ids = np.zeros((0,), dtype=np.int32)
  1004. original_shape = image.shape
  1005. # Resize the image
  1006. image, window, scale, padding, crop = utils.resize_image(
  1007. image,
  1008. min_dim=config.IMAGE_MIN_DIM,
  1009. min_scale=config.IMAGE_MIN_SCALE,
  1010. max_dim=config.IMAGE_MAX_DIM,
  1011. mode=config.IMAGE_RESIZE_MODE
  1012. )
  1013. if augmentation:
  1014. image = augmentation(image)
  1015. if len(boxes) > 0:
  1016. # Filter out invalid boxes (ensuring that the coordinates are valid and correctly formatted)
  1017. valid_boxes = []
  1018. valid_class_ids = []
  1019. for i in range(len(boxes)):
  1020. box = boxes[i]
  1021. if len(box) >= 4 and box[2] > box[0] and box[3] > box[1]:
  1022. valid_boxes.append(box)
  1023. valid_class_ids.append(class_ids[i])
  1024. if valid_boxes:
  1025. boxes = np.array(valid_boxes, dtype=np.int32)
  1026. class_ids = np.array(valid_class_ids, dtype=np.int32)
  1027. # padding : [(top, bottom), (left, right), (0, 0)]
  1028. top_pad, bottom_pad = padding[0]
  1029. left_pad, right_pad = padding[1]
  1030. boxes = boxes * scale + np.array([top_pad, left_pad, top_pad, left_pad])
  1031. boxes = boxes.astype(np.int32)
  1032. else:
  1033. boxes = np.zeros((0, 4), dtype=np.int32)
  1034. class_ids = np.zeros((0,), dtype=np.int32)
  1035. else:
  1036. boxes = np.zeros((0, 4), dtype=np.int32)
  1037. class_ids = np.zeros((0,), dtype=np.int32)
  1038. # Active classes
  1039. active_class_ids = np.zeros([dataset.num_classes], dtype=np.int32)
  1040. source_class_ids = dataset.source_class_ids[dataset.image_info[image_id]["source"]]
  1041. active_class_ids[source_class_ids] = 1
  1042. # Image meta data
  1043. image_meta = compose_image_meta(
  1044. image_id, original_shape, image.shape,
  1045. window, scale, active_class_ids
  1046. )
  1047. return image, image_meta, class_ids, boxes
  1048. def build_detection_targets(rpn_rois, gt_class_ids, gt_boxes, gt_masks, config):
  1049. assert rpn_rois.shape[0] > 0
  1050. assert gt_class_ids.dtype == np.int32, "Expected int but got {}".format(
  1051. gt_class_ids.dtype)
  1052. assert gt_boxes.dtype == np.int32, "Expected int but got {}".format(
  1053. gt_boxes.dtype)
  1054. assert gt_masks.dtype == np.bool_, "Expected bool but got {}".format(
  1055. gt_masks.dtype)
  1056. # It's common to add GT Boxes to ROIs but we don't do that here because
  1057. # according to XinLei Chen's paper, it doesn't help.
  1058. # Trim empty padding in gt_boxes and gt_masks parts
  1059. instance_ids = np.where(gt_class_ids > 0)[0]
  1060. assert instance_ids.shape[0] > 0, "Image must contain instances."
  1061. gt_class_ids = gt_class_ids[instance_ids]
  1062. gt_boxes = gt_boxes[instance_ids]
  1063. gt_masks = gt_masks[:, :, instance_ids]
  1064. # Compute areas of ROIs and ground truth boxes.
  1065. rpn_roi_area = (rpn_rois[:, 2] - rpn_rois[:, 0]) * \
  1066. (rpn_rois[:, 3] - rpn_rois[:, 1])
  1067. gt_box_area = (gt_boxes[:, 2] - gt_boxes[:, 0]) * \
  1068. (gt_boxes[:, 3] - gt_boxes[:, 1])
  1069. # Compute overlaps [rpn_rois, gt_boxes]
  1070. overlaps = np.zeros((rpn_rois.shape[0], gt_boxes.shape[0]))
  1071. for i in range(overlaps.shape[1]):
  1072. gt = gt_boxes[i]
  1073. overlaps[:, i] = utils.compute_iou(
  1074. gt, rpn_rois, gt_box_area[i], rpn_roi_area)
  1075. # Assign ROIs to GT boxes
  1076. rpn_roi_iou_argmax = np.argmax(overlaps, axis=1)
  1077. rpn_roi_iou_max = overlaps[np.arange(
  1078. overlaps.shape[0]), rpn_roi_iou_argmax]
  1079. # GT box assigned to each ROI
  1080. rpn_roi_gt_boxes = gt_boxes[rpn_roi_iou_argmax]
  1081. rpn_roi_gt_class_ids = gt_class_ids[rpn_roi_iou_argmax]
  1082. # Positive ROIs are those with >= 0.5 IoU with a GT box.
  1083. fg_ids = np.where(rpn_roi_iou_max > 0.5)[0]
  1084. # Negative ROIs are those with max IoU 0.1-0.5 (hard example mining)
  1085. # bg_ids = np.where((rpn_roi_iou_max >= 0.1) & (rpn_roi_iou_max < 0.5))[0]
  1086. bg_ids = np.where(rpn_roi_iou_max < 0.5)[0]
  1087. # Subsample ROIs. Aim for 33% foreground.
  1088. # FG
  1089. fg_roi_count = int(config.TRAIN_ROIS_PER_IMAGE * config.ROI_POSITIVE_RATIO)
  1090. if fg_ids.shape[0] > fg_roi_count:
  1091. keep_fg_ids = np.random.choice(fg_ids, fg_roi_count, replace=False)
  1092. else:
  1093. keep_fg_ids = fg_ids
  1094. # BG
  1095. remaining = config.TRAIN_ROIS_PER_IMAGE - keep_fg_ids.shape[0]
  1096. if bg_ids.shape[0] > remaining:
  1097. keep_bg_ids = np.random.choice(bg_ids, remaining, replace=False)
  1098. else:
  1099. keep_bg_ids = bg_ids
  1100. # Combine indices of ROIs to keep
  1101. keep = np.concatenate([keep_fg_ids, keep_bg_ids])
  1102. # Need more?
  1103. remaining = config.TRAIN_ROIS_PER_IMAGE - keep.shape[0]
  1104. if remaining > 0:
  1105. # There is a small chance we have neither fg nor bg samples.
  1106. if keep.shape[0] == 0:
  1107. # Pick bg regions with easier IoU threshold
  1108. bg_ids = np.where(rpn_roi_iou_max < 0.5)[0]
  1109. assert bg_ids.shape[0] >= remaining
  1110. keep_bg_ids = np.random.choice(bg_ids, remaining, replace=False)
  1111. assert keep_bg_ids.shape[0] == remaining
  1112. keep = np.concatenate([keep, keep_bg_ids])
  1113. else:
  1114. # Fill the rest with repeated bg rois.
  1115. keep_extra_ids = np.random.choice(
  1116. keep_bg_ids, remaining, replace=True)
  1117. keep = np.concatenate([keep, keep_extra_ids])
  1118. assert keep.shape[0] == config.TRAIN_ROIS_PER_IMAGE, \
  1119. "keep doesn't match ROI batch size {}, {}".format(
  1120. keep.shape[0], config.TRAIN_ROIS_PER_IMAGE)
  1121. # Reset the gt boxes assigned to BG ROIs.
  1122. rpn_roi_gt_boxes[keep_bg_ids, :] = 0
  1123. rpn_roi_gt_class_ids[keep_bg_ids] = 0
  1124. # For each kept ROI, assign a class_id, and for FG ROIs also add bbox refinement.
  1125. rois = rpn_rois[keep]
  1126. roi_gt_boxes = rpn_roi_gt_boxes[keep]
  1127. roi_gt_class_ids = rpn_roi_gt_class_ids[keep]
  1128. roi_gt_assignment = rpn_roi_iou_argmax[keep]
  1129. # Class-aware bbox deltas. [y, x, log(h), log(w)]
  1130. bboxes = np.zeros((config.TRAIN_ROIS_PER_IMAGE,
  1131. config.NUM_CLASSES, 4), dtype=np.float32)
  1132. pos_ids = np.where(roi_gt_class_ids > 0)[0]
  1133. bboxes[pos_ids, roi_gt_class_ids[pos_ids]] = utils.box_refinement(
  1134. rois[pos_ids], roi_gt_boxes[pos_ids, :4])
  1135. # Normalize bbox refinements
  1136. bboxes /= config.BBOX_STD_DEV
  1137. # Generate class-specific target masks
  1138. masks = np.zeros((config.TRAIN_ROIS_PER_IMAGE, config.MASK_SHAPE[0], config.MASK_SHAPE[1], config.NUM_CLASSES),
  1139. dtype=np.float32)
  1140. for i in pos_ids:
  1141. class_id = roi_gt_class_ids[i]
  1142. assert class_id > 0, "class id must be greater than 0"
  1143. gt_id = roi_gt_assignment[i]
  1144. class_mask = gt_masks[:, :, gt_id]
  1145. if config.USE_MINI_MASK:
  1146. # Create a mask placeholder, the size of the image
  1147. placeholder = np.zeros(config.IMAGE_SHAPE[:2], dtype=bool)
  1148. # GT box
  1149. gt_y1, gt_x1, gt_y2, gt_x2 = gt_boxes[gt_id]
  1150. gt_w = gt_x2 - gt_x1
  1151. gt_h = gt_y2 - gt_y1
  1152. # Resize mini mask to size of GT box
  1153. placeholder[gt_y1:gt_y2, gt_x1:gt_x2] = \
  1154. np.round(utils.resize(class_mask, (gt_h, gt_w))).astype(bool)
  1155. # Place the mini batch in the placeholder
  1156. class_mask = placeholder
  1157. # Pick part of the mask and resize it
  1158. y1, x1, y2, x2 = rois[i].astype(np.int32)
  1159. m = class_mask[y1:y2, x1:x2]
  1160. mask = utils.resize(m, config.MASK_SHAPE)
  1161. masks[i, :, :, class_id] = mask
  1162. return rois, roi_gt_class_ids, bboxes, masks
  1163. def build_rpn_targets(image_shape, anchors, gt_class_ids, gt_boxes, config):
  1164. # RPN Match: 1 = positive anchor, -1 = negative anchor, 0 = neutral
  1165. rpn_match = np.zeros([anchors.shape[0]], dtype=np.int32)
  1166. # RPN bounding boxes: [max anchors per image, (dy, dx, log(dh), log(dw))]
  1167. rpn_bbox = np.zeros((config.RPN_TRAIN_ANCHORS_PER_IMAGE, 4))
  1168. # Handle COCO crowds
  1169. crowd_ix = np.where(gt_class_ids < 0)[0]
  1170. if crowd_ix.shape[0] > 0:
  1171. # Filter out crowds from ground truth class IDs and boxes
  1172. non_crowd_ix = np.where(gt_class_ids > 0)[0]
  1173. crowd_boxes = gt_boxes[crowd_ix]
  1174. gt_class_ids = gt_class_ids[non_crowd_ix]
  1175. gt_boxes = gt_boxes[non_crowd_ix]
  1176. # Compute overlaps with crowd boxes [anchors, crowds]
  1177. crowd_overlaps = utils.compute_overlaps(anchors, crowd_boxes)
  1178. crowd_iou_max = np.amax(crowd_overlaps, axis=1)
  1179. no_crowd_bool = (crowd_iou_max < 0.001)
  1180. else:
  1181. # All anchors don't intersect a crowd
  1182. no_crowd_bool = np.ones([anchors.shape[0]], dtype=bool)
  1183. # Compute overlaps [num_anchors, num_gt_boxes] 计算先验框和gt_box的重合程度
  1184. overlaps = utils.compute_overlaps(anchors, gt_boxes)
  1185. # Match anchors to GT Boxes
  1186. # If an anchor overlaps a GT box with IoU >= 0.7 then it's positive.
  1187. # If an anchor overlaps a GT box with IoU < 0.3 then it's negative.
  1188. # 1. Set negative anchors first. They get overwritten below if a GT box is
  1189. # matched to them. Skip boxes in crowd areas.
  1190. anchor_iou_argmax = np.argmax(overlaps, axis=1)
  1191. anchor_iou_max = overlaps[np.arange(overlaps.shape[0]), anchor_iou_argmax]
  1192. rpn_match[(anchor_iou_max < 0.3) & (no_crowd_bool)] = -1
  1193. # 2. Set an anchor for each GT box (regardless of IoU value).
  1194. # If multiple anchors have the same IoU match all of them
  1195. gt_iou_argmax = np.argwhere(overlaps == np.max(overlaps, axis=0))[:,0]
  1196. rpn_match[gt_iou_argmax] = 1
  1197. # 3. Set anchors with high overlap as positive.
  1198. rpn_match[anchor_iou_max >= 0.7] = 1
  1199. # Subsample to balance positive and negative anchors
  1200. # Don't let positives be more than half the anchors
  1201. ids = np.where(rpn_match == 1)[0]
  1202. extra = len(ids) - (config.RPN_TRAIN_ANCHORS_PER_IMAGE // 2)
  1203. if extra > 0:
  1204. # Reset the extra ones to neutral
  1205. ids = np.random.choice(ids, extra, replace=False)
  1206. rpn_match[ids] = 0
  1207. # Same for negative proposals
  1208. ids = np.where(rpn_match == -1)[0]
  1209. extra = len(ids) - (config.RPN_TRAIN_ANCHORS_PER_IMAGE -
  1210. np.sum(rpn_match == 1))
  1211. if extra > 0:
  1212. # Rest the extra ones to neutral
  1213. ids = np.random.choice(ids, extra, replace=False)
  1214. rpn_match[ids] = 0
  1215. # For positive anchors, compute shift and scale needed to transform them
  1216. # to match the corresponding GT boxes.
  1217. ids = np.where(rpn_match == 1)[0]
  1218. ix = 0 # index into rpn_bbox
  1219. for i, a in zip(ids, anchors[ids]):
  1220. # Closest gt box (it might have IoU < 0.7)
  1221. gt = gt_boxes[anchor_iou_argmax[i]]
  1222. # Convert coordinates to center plus width/height.
  1223. # GT Box
  1224. gt_h = gt[2] - gt[0]
  1225. gt_w = gt[3] - gt[1]
  1226. gt_center_y = gt[0] + 0.5 * gt_h
  1227. gt_center_x = gt[1] + 0.5 * gt_w
  1228. # Anchor
  1229. a_h = a[2] - a[0]
  1230. a_w = a[3] - a[1]
  1231. a_center_y = a[0] + 0.5 * a_h
  1232. a_center_x = a[1] + 0.5 * a_w
  1233. rpn_bbox[ix] = [
  1234. (gt_center_y - a_center_y) / a_h,
  1235. (gt_center_x - a_center_x) / a_w,
  1236. np.log(gt_h / a_h),
  1237. np.log(gt_w / a_w),
  1238. ]
  1239. # Normalize
  1240. rpn_bbox[ix] /= config.RPN_BBOX_STD_DEV
  1241. ix += 1
  1242. return rpn_match, rpn_bbox
  1243. def generate_random_rois(image_shape, count, gt_class_ids, gt_boxes):
  1244. """
  1245. image_shape: [Height, Width, Depth]
  1246. count: Number of ROIs to generate
  1247. gt_class_ids: [N] Integer ground truth class IDs
  1248. gt_boxes: [N, (y1, x1, y2, x2)] Ground truth boxes in pixels.
  1249. Returns: [count, (y1, x1, y2, x2)] ROI boxes in pixels.
  1250. """
  1251. # placeholder
  1252. rois = np.zeros((count, 4), dtype=np.int32)
  1253. # Generate random ROIs around GT boxes (90% of count)
  1254. rois_per_box = int(0.9 * count / gt_boxes.shape[0])
  1255. for i in range(gt_boxes.shape[0]):
  1256. gt_y1, gt_x1, gt_y2, gt_x2 = gt_boxes[i]
  1257. h = gt_y2 - gt_y1
  1258. w = gt_x2 - gt_x1
  1259. # random boundaries
  1260. r_y1 = max(gt_y1 - h, 0)
  1261. r_y2 = min(gt_y2 + h, image_shape[0])
  1262. r_x1 = max(gt_x1 - w, 0)
  1263. r_x2 = min(gt_x2 + w, image_shape[1])
  1264. # To avoid generating boxes with zero area, we generate double what
  1265. # we need and filter out the extra. If we get fewer valid boxes
  1266. # than we need, we loop and try again.
  1267. while True:
  1268. y1y2 = np.random.randint(r_y1, r_y2, (rois_per_box * 2, 2))
  1269. x1x2 = np.random.randint(r_x1, r_x2, (rois_per_box * 2, 2))
  1270. # Filter out zero area boxes
  1271. threshold = 1
  1272. y1y2 = y1y2[np.abs(y1y2[:, 0] - y1y2[:, 1]) >=
  1273. threshold][:rois_per_box]
  1274. x1x2 = x1x2[np.abs(x1x2[:, 0] - x1x2[:, 1]) >=
  1275. threshold][:rois_per_box]
  1276. if y1y2.shape[0] == rois_per_box and x1x2.shape[0] == rois_per_box:
  1277. break
  1278. # Sort on axis 1 to ensure x1 <= x2 and y1 <= y2 and then reshape
  1279. # into x1, y1, x2, y2 order
  1280. x1, x2 = np.split(np.sort(x1x2, axis=1), 2, axis=1)
  1281. y1, y2 = np.split(np.sort(y1y2, axis=1), 2, axis=1)
  1282. box_rois = np.hstack([y1, x1, y2, x2])
  1283. rois[rois_per_box * i:rois_per_box * (i + 1)] = box_rois
  1284. # Generate random ROIs anywhere in the image (10% of count)
  1285. remaining_count = count - (rois_per_box * gt_boxes.shape[0])
  1286. # To avoid generating boxes with zero area, we generate double what
  1287. # we need and filter out the extra. If we get fewer valid boxes
  1288. # than we need, we loop and try again.
  1289. while True:
  1290. y1y2 = np.random.randint(0, image_shape[0], (remaining_count * 2, 2))
  1291. x1x2 = np.random.randint(0, image_shape[1], (remaining_count * 2, 2))
  1292. # Filter out zero area boxes
  1293. threshold = 1
  1294. y1y2 = y1y2[np.abs(y1y2[:, 0] - y1y2[:, 1]) >=
  1295. threshold][:remaining_count]
  1296. x1x2 = x1x2[np.abs(x1x2[:, 0] - x1x2[:, 1]) >=
  1297. threshold][:remaining_count]
  1298. if y1y2.shape[0] == remaining_count and x1x2.shape[0] == remaining_count:
  1299. break
  1300. # Sort on axis 1 to ensure x1 <= x2 and y1 <= y2 and then reshape
  1301. # into x1, y1, x2, y2 order
  1302. x1, x2 = np.split(np.sort(x1x2, axis=1), 2, axis=1)
  1303. y1, y2 = np.split(np.sort(y1y2, axis=1), 2, axis=1)
  1304. global_rois = np.hstack([y1, x1, y2, x2])
  1305. rois[-remaining_count:] = global_rois
  1306. return rois
  1307. def data_generator(dataset, config, shuffle=True, augment=False, augmentation=None,
  1308. random_rois=0, batch_size=1, detection_targets=False,
  1309. no_augmentation_sources=None):
  1310. b = 0 # batch item index
  1311. image_index = -1
  1312. image_ids = np.copy(dataset.image_ids)
  1313. error_count = 0
  1314. # Pre-compute anchors (calculate only once)
  1315. backbone_shapes = compute_backbone_shapes(config, config.IMAGE_SHAPE)
  1316. anchors = utils.generate_pyramid_anchors(
  1317. config.RPN_ANCHOR_SCALES,
  1318. config.RPN_ANCHOR_RATIOS,
  1319. backbone_shapes,
  1320. config.BACKBONE_STRIDES,
  1321. config.RPN_ANCHOR_STRIDE
  1322. )
  1323. anchor_count = anchors.shape[0]
  1324. # Pre-allocate the batch array (to avoid reallocation)
  1325. batch_arrays = None
  1326. # Keras requires a generator to run indefinitely.
  1327. while True:
  1328. try:
  1329. # Increment index to pick next image. Shuffle if at the start of an epoch.
  1330. image_index = (image_index + 1) % len(image_ids)
  1331. if shuffle and image_index == 0:
  1332. np.random.shuffle(image_ids)
  1333. # Get GT bounding boxes and masks for image.
  1334. image_id = image_ids[image_index]
  1335. try:
  1336. image, image_meta, gt_class_ids, gt_boxes = \
  1337. load_image_gt_no_mask(dataset, config, image_id)
  1338. except (IndexError, ValueError, TypeError) as e:
  1339. logging.warning("Skipping image {} due to error: {}".format(
  1340. dataset.image_info[image_id]['path'], e))
  1341. continue
  1342. # Skip images that have no instances
  1343. if gt_boxes.shape[0] == 0:
  1344. continue
  1345. # RPN Targets
  1346. rpn_match, rpn_bbox = build_rpn_targets(
  1347. image.shape, anchors, gt_class_ids, gt_boxes, config
  1348. )
  1349. # Allocate the batch array only on the first occasion or when the shape changes
  1350. if b == 0:
  1351. if batch_arrays is None or batch_arrays['image_shape'] != image.shape:
  1352. batch_image_meta = np.zeros(
  1353. (batch_size,) + image_meta.shape, dtype=image_meta.dtype)
  1354. batch_rpn_match = np.zeros(
  1355. [batch_size, anchor_count, 1], dtype=rpn_match.dtype)
  1356. batch_rpn_bbox = np.zeros(
  1357. [batch_size, config.RPN_TRAIN_ANCHORS_PER_IMAGE, 4],
  1358. dtype=rpn_bbox.dtype)
  1359. batch_images = np.zeros(
  1360. (batch_size,) + image.shape, dtype=np.float32)
  1361. batch_gt_class_ids = np.zeros(
  1362. (batch_size, config.MAX_GT_INSTANCES), dtype=np.int32)
  1363. batch_gt_boxes = np.zeros(
  1364. (batch_size, config.MAX_GT_INSTANCES, 4), dtype=np.int32)
  1365. batch_arrays = {
  1366. 'image_meta': batch_image_meta,
  1367. 'rpn_match': batch_rpn_match,
  1368. 'rpn_bbox': batch_rpn_bbox,
  1369. 'images': batch_images,
  1370. 'gt_class_ids': batch_gt_class_ids,
  1371. 'gt_boxes': batch_gt_boxes,
  1372. 'image_shape': image.shape
  1373. }
  1374. else:
  1375. batch_arrays['image_meta'].fill(0)
  1376. batch_arrays['rpn_match'].fill(0)
  1377. batch_arrays['rpn_bbox'].fill(0)
  1378. batch_arrays['images'].fill(0)
  1379. batch_arrays['gt_class_ids'].fill(0)
  1380. batch_arrays['gt_boxes'].fill(0)
  1381. # Limit the number of GTs to prevent them from going out of bounds
  1382. gt_count = min(gt_class_ids.shape[0], config.MAX_GT_INSTANCES)
  1383. # Add to batch
  1384. batch_arrays['image_meta'][b] = image_meta
  1385. batch_arrays['rpn_match'][b] = rpn_match[:, np.newaxis]
  1386. batch_arrays['rpn_bbox'][b] = rpn_bbox
  1387. batch_arrays['images'][b] = mold_image(image.astype(np.float32), config)
  1388. batch_arrays['gt_class_ids'][b, :gt_count] = gt_class_ids[:gt_count]
  1389. batch_arrays['gt_boxes'][b, :gt_count] = gt_boxes[:gt_count]
  1390. b += 1
  1391. # Batch full
  1392. if b >= batch_size:
  1393. inputs = [
  1394. batch_arrays['images'],
  1395. batch_arrays['image_meta'],
  1396. batch_arrays['rpn_match'],
  1397. batch_arrays['rpn_bbox'],
  1398. batch_arrays['gt_class_ids'],
  1399. batch_arrays['gt_boxes']
  1400. ]
  1401. outputs = []
  1402. yield inputs, outputs
  1403. # start a new batch
  1404. b = 0
  1405. except (GeneratorExit, KeyboardInterrupt):
  1406. raise
  1407. except Exception as e:
  1408. # Log it and skip the image
  1409. logging.exception("Error processing image {}".format(
  1410. dataset.image_info[image_id]))
  1411. error_count += 1
  1412. if error_count > 5:
  1413. raise
  1414. # Loss save 预先设置list
  1415. epoch_list = []
  1416. tra_loss_list = []
  1417. tra1_loss_list = []
  1418. tra2_loss_list = []
  1419. tra3_loss_list = []
  1420. tra4_loss_list = []
  1421. val_loss_list = []
  1422. val1_loss_list = []
  1423. val2_loss_list = []
  1424. val3_loss_list = []
  1425. val4_loss_list = []
  1426. def expand_dim_1(x):
  1427. x1 = K.expand_dims(x, axis=-1)
  1428. return x1
  1429. # MaskRCNN Class
  1430. def per_level_enhancement_block(C2, C3, C4, config):
  1431. """
  1432. Independent enhancement of each layer: Generate spatial attention to enhance each layer individually
  1433. Output: Enhanced C2, C3 and C4 (retaining original dimensions and number of channels)
  1434. """
  1435. c2_shape = tf.shape(C2)
  1436. c3_shape = tf.shape(C3)
  1437. c4_shape = tf.shape(C4)
  1438. # Merge them into a single C4 size
  1439. target_h = c4_shape[1]
  1440. target_w = c4_shape[2]
  1441. # Resize
  1442. c2_resized = KL.Lambda(
  1443. lambda x: tf.image.resize_images(x, (target_h, target_w), method=tf.image.ResizeMethod.BILINEAR),
  1444. name='per_resize_c2'
  1445. )(C2)
  1446. c3_resized = KL.Lambda(
  1447. lambda x: tf.image.resize_images(x, (target_h, target_w), method=tf.image.ResizeMethod.BILINEAR),
  1448. name='per_resize_c3'
  1449. )(C3)
  1450. c2_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='per_c2_proj')(c2_resized)
  1451. c3_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='per_c3_proj')(c3_resized)
  1452. c4_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='per_c4_proj')(C4)
  1453. # Fusion-based spatial attention
  1454. fused = KL.Concatenate(axis=-1, name='per_concat')([c2_proj, c3_proj, c4_proj])
  1455. fused = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='per_fusion')(fused)
  1456. # Generate single-channel spatial attention
  1457. spatial_attention = KL.Conv2D(
  1458. 1, (3, 3), padding='same', activation='sigmoid', name='per_spatial_att'
  1459. )(fused)
  1460. # Apply attention at each level
  1461. att_c2 = KL.Lambda(
  1462. lambda x: tf.image.resize_images(x, (c2_shape[1], c2_shape[2]), method=tf.image.ResizeMethod.BILINEAR),
  1463. name='per_att_to_c2'
  1464. )(spatial_attention)
  1465. att_c3 = KL.Lambda(
  1466. lambda x: tf.image.resize_images(x, (c3_shape[1], c3_shape[2]), method=tf.image.ResizeMethod.BILINEAR),
  1467. name='per_att_to_c3'
  1468. )(spatial_attention)
  1469. att_c4 = spatial_attention
  1470. # Residual Connection Enhancement
  1471. enhanced_C2 = KL.Add(name='per_enhanced_c2')([C2, KL.Multiply()([C2, att_c2])])
  1472. enhanced_C3 = KL.Add(name='per_enhanced_c3')([C3, KL.Multiply()([C3, att_c3])])
  1473. enhanced_C4 = KL.Add(name='per_enhanced_c4')([C4, KL.Multiply()([C4, att_c4])])
  1474. return enhanced_C2, enhanced_C3, enhanced_C4
  1475. def top_level_fusion_block(C2, C3, C4, C5, config):
  1476. """
  1477. Top-level fusion enhancement: After fusing the information from C2–C4, enhance P5
  1478. Output: The enhanced P5 (retaining the dimensions of P5)
  1479. """
  1480. # c2_shape = tf.shape(C2)
  1481. # c3_shape = tf.shape(C3)
  1482. c4_shape = tf.shape(C4)
  1483. target_h = c4_shape[1]
  1484. target_w = c4_shape[2]
  1485. # Adjust C2 and C3 to match the dimensions of C4
  1486. c2_down = KL.Lambda(
  1487. lambda x: tf.image.resize_images(x, (target_h, target_w), method=tf.image.ResizeMethod.BILINEAR),
  1488. name='top_resize_c2'
  1489. )(C2)
  1490. c3_down = KL.Lambda(
  1491. lambda x: tf.image.resize_images(x, (target_h, target_w), method=tf.image.ResizeMethod.BILINEAR),
  1492. name='top_resize_c3'
  1493. )(C3)
  1494. c4_current = C4
  1495. # Set the number of channels to TOP_DOWN_PYRAMID_SIZE
  1496. c2_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='top_c2_proj')(c2_down)
  1497. c3_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='top_c3_proj')(c3_down)
  1498. c4_proj = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='top_c4_proj')(c4_current)
  1499. # Feature fusion
  1500. fused = KL.Concatenate(axis=-1, name='top_concat')([c2_proj, c3_proj, c4_proj])
  1501. fused = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='top_fusion_1')(fused)
  1502. attention_3ch = KL.Conv2D(
  1503. 3, (3, 3), padding='same', activation='sigmoid', name='top_attention'
  1504. )(fused) # [B, H/16, W/16, 3]
  1505. # Separate the attention maps and apply them to the corresponding features
  1506. att_c2_raw = KL.Lambda(lambda x: x[:, :, :, 0:1], name='top_att_c2_slice')(attention_3ch)
  1507. att_c3_raw = KL.Lambda(lambda x: x[:, :, :, 1:2], name='top_att_c3_slice')(attention_3ch)
  1508. att_c4_raw = KL.Lambda(lambda x: x[:, :, :, 2:3], name='top_att_c4_slice')(attention_3ch)
  1509. # Apply attention to the corresponding projected features
  1510. attended_c2 = KL.Multiply(name='top_multiply_c2')([c2_proj, att_c2_raw])
  1511. attended_c3 = KL.Multiply(name='top_multiply_c3')([c3_proj, att_c3_raw])
  1512. attended_c4 = KL.Multiply(name='top_multiply_c4')([c4_proj, att_c4_raw])
  1513. # Combined to produce a fusion feature map FS5
  1514. FS5 = KL.Add(name='top_fs5')([attended_c2, attended_c3, attended_c4])
  1515. # Generate P5 and merge with FS5
  1516. # The original P5 (projected from C5)
  1517. P5_original = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c5p5')(C5)
  1518. # Resample the FS5 to P5 dimensions (H/32, W/32)
  1519. p5_shape = tf.shape(P5_original)
  1520. FS5_upsampled = KL.Lambda(
  1521. lambda x: tf.image.resize_images(x, (p5_shape[1], p5_shape[2]), method=tf.image.ResizeMethod.BILINEAR),
  1522. name='top_fs5_upsample'
  1523. )(FS5)
  1524. # The enhanced P5
  1525. enhanced_P5 = KL.Add(name='top_enhanced_p5')([P5_original, FS5_upsampled])
  1526. return enhanced_P5
  1527. class MaskRCNN():
  1528. def __init__(self, mode, config, model_dir):
  1529. assert mode in ['training', 'inference']
  1530. self.mode = mode
  1531. self.config = config
  1532. self.model_dir = model_dir
  1533. self.set_log_dir()
  1534. self.keras_model = self.build(mode=mode, config=config)
  1535. def build(self, mode, config):
  1536. assert mode in ['training', 'inference']
  1537. # Image size must be dividable by 2 multiple times
  1538. h, w = config.IMAGE_SHAPE[:2]
  1539. if h / 2**6 != int(h / 2**6) or w / 2**6 != int(w / 2**6):
  1540. raise Exception("Image size must be dividable by 2 at least 6 times "
  1541. "to avoid fractions when downscaling and upscaling."
  1542. "For example, use 256, 320, 384, 448, 512, ... etc. ")
  1543. # Inputs
  1544. input_image = KL.Input(
  1545. shape=[None, None, config.IMAGE_SHAPE[2]], name="input_image")
  1546. input_image_meta = KL.Input(shape=[config.IMAGE_META_SIZE],
  1547. name="input_image_meta")
  1548. if mode == "training":
  1549. # RPN GT
  1550. input_rpn_match = KL.Input(
  1551. shape=[None, 1], name="input_rpn_match", dtype=tf.int32)
  1552. input_rpn_bbox = KL.Input(
  1553. shape=[None, 4], name="input_rpn_bbox", dtype=tf.float32)
  1554. # Detection GT (class IDs, bounding boxes, and masks)
  1555. # 1. GT Class IDs (zero padded)
  1556. input_gt_class_ids = KL.Input(
  1557. shape=[None], name="input_gt_class_ids", dtype=tf.int32)
  1558. # 2. GT Boxes in pixels (zero padded)
  1559. # [batch, MAX_GT_INSTANCES, (y1, x1, y2, x2)] in image coordinates
  1560. input_gt_boxes = KL.Input(
  1561. shape=[None, 4], name="input_gt_boxes", dtype=tf.float32)
  1562. # Normalize coordinates
  1563. gt_boxes = KL.Lambda(lambda x: norm_boxes_graph(
  1564. x, K.shape(input_image)[1:3]))(input_gt_boxes)
  1565. # 3. GT Masks (zero padded)
  1566. # [batch, height, width, MAX_GT_INSTANCES]
  1567. if config.USE_MASK:
  1568. if config.USE_MINI_MASK:
  1569. input_gt_masks = KL.Input(
  1570. shape=[config.MINI_MASK_SHAPE[0],
  1571. config.MINI_MASK_SHAPE[1], None],
  1572. name="input_gt_masks", dtype=bool)
  1573. else:
  1574. input_gt_masks = KL.Input(
  1575. shape=[config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1], None],
  1576. name="input_gt_masks", dtype=bool)
  1577. # else:
  1578. # input_gt_masks = None
  1579. elif mode == "inference":
  1580. # Anchors in normalized coordinates
  1581. input_anchors = KL.Input(shape=[None, 4], name="input_anchors")
  1582. # Build the shared convolutional layers.
  1583. if callable(config.BACKBONE):
  1584. _, C2, C3, C4, C5 = config.BACKBONE(input_image, stage5=True,
  1585. train_bn=config.TRAIN_BN)
  1586. else:
  1587. _, C2, C3, C4, C5 = resnet_graph(input_image, config.BACKBONE,
  1588. stage5=True, train_bn=config.TRAIN_BN)
  1589. # feature reuse block(FR)
  1590. if config.USE_PER_LEVEL_ENHANCEMENT:
  1591. enhanced_C2, enhanced_C3, enhanced_C4 = per_level_enhancement_block(C2, C3, C4, config)
  1592. # Using enhanced features
  1593. feat_C2, feat_C3, feat_C4 = enhanced_C2, enhanced_C3, enhanced_C4
  1594. else:
  1595. # Use the original features
  1596. feat_C2, feat_C3, feat_C4 = C2, C3, C4
  1597. # FPN
  1598. P5_base = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c5p5_base')(C5)
  1599. # Top-level Fusion Enhancement
  1600. if config.USE_TOP_LEVEL_FUSION:
  1601. # Using enhanced C2-C4
  1602. fusion_input_C2 = enhanced_C2 if config.USE_PER_LEVEL_ENHANCEMENT else C2
  1603. fusion_input_C3 = enhanced_C3 if config.USE_PER_LEVEL_ENHANCEMENT else C3
  1604. fusion_input_C4 = enhanced_C4 if config.USE_PER_LEVEL_ENHANCEMENT else C4
  1605. P5 = top_level_fusion_block(fusion_input_C2, fusion_input_C3, fusion_input_C4, C5, config)
  1606. else:
  1607. P5 = P5_base
  1608. P4 = KL.Add(name="fpn_p4add")([
  1609. KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5),
  1610. KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c4p4')(feat_C4)
  1611. ])
  1612. P3 = KL.Add(name="fpn_p3add")([
  1613. KL.UpSampling2D(size=(2, 2), name="fpn_p4upsampled")(P4),
  1614. KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c3p3')(feat_C3)
  1615. ])
  1616. P2 = KL.Add(name="fpn_p2add")([
  1617. KL.UpSampling2D(size=(2, 2), name="fpn_p3upsampled")(P3),
  1618. KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c2p2')(feat_C2)
  1619. ])
  1620. # Attach 3x3 conv to all P layers to get the final feature maps.
  1621. P2 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p2")(P2)
  1622. P3 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p3")(P3)
  1623. P4 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p4")(P4)
  1624. P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p5")(P5)
  1625. # a, b, c, d = P5_test.shape
  1626. # P5_test = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p5_test")(P5_test)
  1627. # P6 is used for the 5th anchor scale in RPN. Generated by
  1628. # subsampling from P5 with stride of 2.
  1629. P6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2, name="fpn_p6")(P5)
  1630. # Note that P6 is used in RPN, but not in the classifier heads.
  1631. rpn_feature_maps = [P2, P3, P4, P5, P6]
  1632. mrcnn_feature_maps = [P2, P3, P4, P5]
  1633. # Anchors
  1634. if mode == "training":
  1635. anchors = self.get_anchors(config.IMAGE_SHAPE)
  1636. # Duplicate across the batch dimension because Keras requires it
  1637. # TODO: can this be optimized to avoid duplicating the anchors?
  1638. anchors = np.broadcast_to(anchors, (config.BATCH_SIZE,) + anchors.shape)
  1639. # A hack to get around Keras's bad support for constants
  1640. anchors = KL.Lambda(lambda x: tf.Variable(anchors), name="anchors")(input_image)
  1641. else:
  1642. anchors = input_anchors
  1643. # RPN Model
  1644. rpn = build_rpn_model(config.RPN_ANCHOR_STRIDE,
  1645. len(config.RPN_ANCHOR_RATIOS), config.TOP_DOWN_PYRAMID_SIZE)
  1646. # Loop through pyramid layers
  1647. layer_outputs = [] # list of lists
  1648. for p in rpn_feature_maps:
  1649. layer_outputs.append(rpn([p]))
  1650. print(p.shape)
  1651. # Concatenate layer outputs
  1652. # Convert from list of lists of level outputs to list of lists
  1653. # of outputs across levels.
  1654. # e.g. [[a1, b1, c1], [a2, b2, c2]] => [[a1, a2], [b1, b2], [c1, c2]]
  1655. output_names = ["rpn_class_logits", "rpn_class", "rpn_bbox"]
  1656. outputs = list(zip(*layer_outputs))
  1657. outputs = [KL.Concatenate(axis=1, name=n)(list(o))
  1658. for o, n in zip(outputs, output_names)]
  1659. rpn_class_logits, rpn_class, rpn_bbox = outputs
  1660. # Generate proposals
  1661. # Proposals are [batch, N, (y1, x1, y2, x2)] in normalized coordinates
  1662. # and zero padded.
  1663. proposal_count = config.POST_NMS_ROIS_TRAINING if mode == "training"\
  1664. else config.POST_NMS_ROIS_INFERENCE
  1665. rpn_rois = ProposalLayer(
  1666. proposal_count=proposal_count,
  1667. nms_threshold=config.RPN_NMS_THRESHOLD,
  1668. name="ROI",
  1669. config=config)([rpn_class, rpn_bbox, anchors])
  1670. if mode == "training":
  1671. # Class ID mask to mark class IDs supported by the dataset the image
  1672. # came from.
  1673. active_class_ids = KL.Lambda(
  1674. lambda x: parse_image_meta_graph(x)["active_class_ids"]
  1675. )(input_image_meta)
  1676. if not config.USE_RPN_ROIS:
  1677. # Ignore predicted ROIs and use ROIs provided as an input.
  1678. input_rois = KL.Input(shape=[config.POST_NMS_ROIS_TRAINING, 4],
  1679. name="input_roi", dtype=np.int32)
  1680. # Normalize coordinates
  1681. target_rois = KL.Lambda(lambda x: norm_boxes_graph(
  1682. x, K.shape(input_image)[1:3]))(input_rois)
  1683. else:
  1684. target_rois = rpn_rois
  1685. # Generate detection targets 其中target_rois是proposallayer输出结果
  1686. # Subsamples proposals and generates target outputs for training
  1687. # Note that proposal class IDs, gt_boxes, and gt_masks are zero
  1688. # padded. Equally, returned rois and targets are zero padded.
  1689. if config.USE_MASK:
  1690. rois, target_class_ids, target_bbox, target_mask =\
  1691. DetectionTargetLayer(config, name="proposal_targets")([
  1692. target_rois, input_gt_class_ids, gt_boxes, input_gt_masks])
  1693. else:
  1694. # NO MASK
  1695. rois, target_class_ids, target_bbox = \
  1696. DetectionTargetLayerNoMask(config, name="proposal_targets")([
  1697. target_rois, input_gt_class_ids, gt_boxes])
  1698. # target_mask = None
  1699. # Network Heads fpn_classifier_graph + build_fpn_mask_graph
  1700. mrcnn_class_logits, mrcnn_class, mrcnn_bbox =\
  1701. fpn_classifier_graph(rois, mrcnn_feature_maps, input_image_meta,
  1702. config.POOL_SIZE, config.NUM_CLASSES,
  1703. train_bn=config.TRAIN_BN,
  1704. fc_layers_size=config.FPN_CLASSIF_FC_LAYERS_SIZE)
  1705. if config.USE_MASK:
  1706. mrcnn_mask = build_fpn_mask_graph(rois, mrcnn_feature_maps,
  1707. input_image_meta,
  1708. config.MASK_POOL_SIZE,
  1709. config.NUM_CLASSES,
  1710. train_bn=config.TRAIN_BN)
  1711. # else:
  1712. # mrcnn_mask = KL.Lambda(lambda x: tf.constant(0.0), name="dummy_mask")(rois)
  1713. # TODO: clean up (use tf.identify if necessary)
  1714. output_rois = KL.Lambda(lambda x: x * 1, name="output_rois")(rois)
  1715. # Losses
  1716. rpn_class_loss = KL.Lambda(lambda x: rpn_class_loss_graph(*x), name="rpn_class_loss")(
  1717. [input_rpn_match, rpn_class_logits])
  1718. rpn_bbox_loss = KL.Lambda(lambda x: rpn_bbox_loss_graph(config, *x), name="rpn_bbox_loss")(
  1719. [input_rpn_bbox, input_rpn_match, rpn_bbox])
  1720. class_loss = KL.Lambda(lambda x: mrcnn_class_loss_graph(*x), name="mrcnn_class_loss")(
  1721. [target_class_ids, mrcnn_class_logits, active_class_ids])
  1722. bbox_loss = KL.Lambda(lambda x: mrcnn_bbox_loss_graph(*x), name="mrcnn_bbox_loss")(
  1723. [target_bbox, target_class_ids, mrcnn_bbox])
  1724. # Model
  1725. inputs = [input_image, input_image_meta,
  1726. input_rpn_match, input_rpn_bbox,
  1727. input_gt_class_ids, input_gt_boxes]
  1728. if config.USE_MASK:
  1729. inputs.append(input_gt_masks)
  1730. if not config.USE_RPN_ROIS:
  1731. inputs.append(input_rois)
  1732. if config.USE_MASK:
  1733. mask_loss = KL.Lambda(lambda x: mrcnn_mask_loss_graph(*x), name="mrcnn_mask_loss")(
  1734. [target_mask, target_class_ids, mrcnn_mask])
  1735. outputs = [rpn_class_logits, rpn_class, rpn_bbox,
  1736. mrcnn_class_logits, mrcnn_class, mrcnn_bbox, mrcnn_mask,
  1737. rpn_rois, output_rois,
  1738. rpn_class_loss, rpn_bbox_loss, class_loss, bbox_loss, mask_loss]
  1739. else:
  1740. outputs = [rpn_class_logits, rpn_class, rpn_bbox,
  1741. mrcnn_class_logits, mrcnn_class, mrcnn_bbox,
  1742. rpn_rois, output_rois,
  1743. rpn_class_loss, rpn_bbox_loss, class_loss, bbox_loss]
  1744. model = KM.Model(inputs, outputs, name='faster_rcnn')
  1745. else:
  1746. # Network Heads
  1747. # Proposal classifier and BBox regressor heads
  1748. mrcnn_class_logits, mrcnn_class, mrcnn_bbox =\
  1749. fpn_classifier_graph(rpn_rois, mrcnn_feature_maps, input_image_meta,
  1750. config.POOL_SIZE, config.NUM_CLASSES,
  1751. train_bn=config.TRAIN_BN,
  1752. fc_layers_size=config.FPN_CLASSIF_FC_LAYERS_SIZE)
  1753. # Detections
  1754. # output is [batch, num_detections, (y1, x1, y2, x2, class_id, score)] in
  1755. # normalized coordinates
  1756. detections = DetectionLayer(config, name="mrcnn_detection")(
  1757. [rpn_rois, mrcnn_class, mrcnn_bbox, input_image_meta])
  1758. # Create masks for detections
  1759. if config.USE_MASK:
  1760. detection_boxes = KL.Lambda(lambda x: x[..., :4])(detections)
  1761. mrcnn_mask = build_fpn_mask_graph(detection_boxes, mrcnn_feature_maps,
  1762. input_image_meta,
  1763. config.MASK_POOL_SIZE,
  1764. config.NUM_CLASSES,
  1765. train_bn=config.TRAIN_BN)
  1766. model = KM.Model([input_image, input_image_meta, input_anchors],
  1767. [detections, mrcnn_class, mrcnn_bbox,
  1768. mrcnn_mask, rpn_rois, rpn_class, rpn_bbox],
  1769. name='mask_rcnn')
  1770. else:
  1771. # Do not output the mask
  1772. model = KM.Model([input_image, input_image_meta, input_anchors],
  1773. [detections, mrcnn_class, mrcnn_bbox,
  1774. rpn_rois, rpn_class, rpn_bbox],
  1775. name='faster_rcnn')
  1776. # Add multi-GPU support
  1777. if config.GPU_COUNT > 1:
  1778. from mrcnn.parallel_model import ParallelModel
  1779. model = ParallelModel(model, config.GPU_COUNT)
  1780. return model
  1781. def find_last(self):
  1782. """Finds the last checkpoint file of the last trained model in the
  1783. model directory.
  1784. Returns:
  1785. The path of the last checkpoint file
  1786. """
  1787. # Get directory names. Each directory corresponds to a model
  1788. dir_names = next(os.walk(self.model_dir))[1]
  1789. key = self.config.NAME.lower()
  1790. dir_names = filter(lambda f: f.startswith(key), dir_names)
  1791. dir_names = sorted(dir_names)
  1792. if not dir_names:
  1793. import errno
  1794. raise FileNotFoundError(
  1795. errno.ENOENT,
  1796. "Could not find model directory under {}".format(self.model_dir))
  1797. # Pick last directory
  1798. dir_name = os.path.join(self.model_dir, dir_names[-1])
  1799. # Find the last checkpoint
  1800. checkpoints = next(os.walk(dir_name))[2]
  1801. checkpoints = filter(lambda f: f.startswith("2D_method"), checkpoints)
  1802. checkpoints = sorted(checkpoints)
  1803. if not checkpoints:
  1804. import errno
  1805. raise FileNotFoundError(
  1806. errno.ENOENT, "Could not find weight files in {}".format(dir_name))
  1807. checkpoint = os.path.join(dir_name, checkpoints[-1])
  1808. return checkpoint
  1809. def load_weights(self, filepath, by_name=False, exclude=None):
  1810. import h5py
  1811. try:
  1812. from keras.engine import saving
  1813. except ImportError:
  1814. from keras.engine import topology as saving
  1815. if exclude:
  1816. by_name = True
  1817. if h5py is None:
  1818. raise ImportError('`load_weights` requires h5py.')
  1819. f = h5py.File(filepath, mode='r')
  1820. if 'layer_names' not in f.attrs and 'model_weights' in f:
  1821. f = f['model_weights']
  1822. # In multi-GPU training, we wrap the model.
  1823. keras_model = self.keras_model
  1824. layers = keras_model.inner_model.layers if hasattr(keras_model, "inner_model")\
  1825. else keras_model.layers
  1826. # Exclude some layers
  1827. if exclude:
  1828. layers = filter(lambda l: l.name not in exclude, layers)
  1829. if by_name:
  1830. saving.load_weights_from_hdf5_group_by_name(f, layers)
  1831. else:
  1832. saving.load_weights_from_hdf5_group(f, layers)
  1833. if hasattr(f, 'close'):
  1834. f.close()
  1835. # Update the log directory
  1836. self.set_log_dir(filepath)
  1837. def get_imagenet_weights(self):
  1838. from keras.utils.data_utils import get_file
  1839. TF_WEIGHTS_PATH_NO_TOP = 'https://github.com/fchollet/deep-learning-models/'\
  1840. 'releases/download/v0.2/'\
  1841. 'resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5'
  1842. weights_path = get_file('resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5',
  1843. TF_WEIGHTS_PATH_NO_TOP,
  1844. cache_subdir='models',
  1845. md5_hash='a268eb855778b3df3c7506639542a6af')
  1846. return weights_path
  1847. def compile(self, learning_rate, momentum):
  1848. # Optimizer object
  1849. optimizer = keras.optimizers.SGD(
  1850. lr=learning_rate, momentum=momentum,
  1851. clipnorm=self.config.GRADIENT_CLIP_NORM)
  1852. # Add Losses
  1853. # First, clear previously set losses to avoid duplication
  1854. self.keras_model._losses = []
  1855. self.keras_model._per_input_losses = {}
  1856. # loss_names = [
  1857. # "rpn_class_loss", "rpn_bbox_loss",
  1858. # "mrcnn_class_loss", "mrcnn_bbox_loss", "mrcnn_mask_loss"]
  1859. if self.config.USE_MASK:
  1860. loss_names = ["rpn_class_loss", "rpn_bbox_loss",
  1861. "mrcnn_class_loss", "mrcnn_bbox_loss", "mrcnn_mask_loss"]
  1862. else:
  1863. loss_names = ["rpn_class_loss", "rpn_bbox_loss",
  1864. "mrcnn_class_loss", "mrcnn_bbox_loss"]
  1865. for name in loss_names:
  1866. layer = self.keras_model.get_layer(name)
  1867. if layer.output in self.keras_model.losses:
  1868. continue
  1869. loss = (
  1870. tf.reduce_mean(layer.output, keepdims=True)
  1871. * self.config.LOSS_WEIGHTS.get(name, 1.))
  1872. self.keras_model.add_loss(loss)
  1873. # Add L2 Regularization
  1874. # Skip gamma and beta weights of batch normalization layers.
  1875. reg_losses = [
  1876. keras.regularizers.l2(self.config.WEIGHT_DECAY)(w) / tf.cast(tf.size(w), tf.float32)
  1877. for w in self.keras_model.trainable_weights
  1878. if 'gamma' not in w.name and 'beta' not in w.name]
  1879. self.keras_model.add_loss(tf.add_n(reg_losses))
  1880. # Compile
  1881. self.keras_model.compile(
  1882. optimizer=optimizer,
  1883. loss=[None] * len(self.keras_model.outputs))
  1884. # Add metrics for losses
  1885. for name in loss_names:
  1886. if name in self.keras_model.metrics_names:
  1887. continue
  1888. layer = self.keras_model.get_layer(name)
  1889. self.keras_model.metrics_names.append(name)
  1890. loss = (
  1891. tf.reduce_mean(layer.output, keepdims=True)
  1892. * self.config.LOSS_WEIGHTS.get(name, 1.))
  1893. self.keras_model.metrics_tensors.append(loss)
  1894. def set_trainable(self, layer_regex, keras_model=None, indent=0, verbose=1):
  1895. # Print message on the first call (but not on recursive calls)
  1896. if verbose > 0 and keras_model is None:
  1897. log("Selecting layers to train")
  1898. keras_model = keras_model or self.keras_model
  1899. # In multi-GPU training, we wrap the model. Get layers
  1900. # of the inner model because they have the weights.
  1901. layers = keras_model.inner_model.layers if hasattr(keras_model, "inner_model")\
  1902. else keras_model.layers
  1903. for layer in layers:
  1904. if layer.__class__.__name__ == 'Model':
  1905. print("In model: ", layer.name)
  1906. self.set_trainable(
  1907. layer_regex, keras_model=layer, indent=indent + 4)
  1908. continue
  1909. if not layer.weights:
  1910. continue
  1911. trainable = bool(re.fullmatch(layer_regex, layer.name))
  1912. # Update layer. If layer is a container, update inner layer.
  1913. if layer.__class__.__name__ == 'TimeDistributed':
  1914. layer.layer.trainable = trainable
  1915. else:
  1916. layer.trainable = trainable
  1917. # Print trainable layer names
  1918. if trainable and verbose > 0:
  1919. log("{}{:20} ({})".format(" " * indent, layer.name,
  1920. layer.__class__.__name__))
  1921. def get_layer(self, layer_regex, keras_model=None, indent=0, verbose=1):
  1922. # Print message on the first call (but not on recursive calls)
  1923. if verbose > 0 and keras_model is None:
  1924. log("Get layers output")
  1925. keras_model = keras_model or self.keras_model
  1926. # In multi-GPU training, we wrap the model. Get layers
  1927. # of the inner model because they have the weights.
  1928. layers = keras_model.inner_model.layers if hasattr(keras_model, "inner_model")\
  1929. else keras_model.layers
  1930. for layer in layers:
  1931. if layer.__class__.__name__ == 'Model':
  1932. print("In model: ", layer.name)
  1933. return layers
  1934. def set_log_dir(self, model_path=None):
  1935. # Set date and epoch counter as if starting a new model
  1936. self.epoch = 0
  1937. now = datetime.datetime.now()
  1938. # If we have a model path with date and epochs use them
  1939. if model_path:
  1940. # Continue from we left of. Get epoch and date from the file name
  1941. # A sample model path might look like:
  1942. # \path\to\logs\coco20171029T2315\mask_rcnn_coco_0001.h5 (Windows)
  1943. regex = r".*[/\\][\w-]+(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})[/\\]2D\_method\_[\w-]+(\d{4})\.h5"
  1944. m = re.match(regex, model_path)
  1945. if m:
  1946. now = datetime.datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)),
  1947. int(m.group(4)), int(m.group(5)))
  1948. # Epoch number in file is 1-based, and in Keras code it's 0-based.
  1949. # So, adjust for that then increment by one to start from the next epoch
  1950. self.epoch = int(m.group(6)) - 1 + 1
  1951. print('Re-starting from epoch %d' % self.epoch)
  1952. # Directory for training logs
  1953. self.log_dir = os.path.join(self.model_dir, "{}{:%Y%m%dT%H%M}".format(
  1954. self.config.NAME.lower(), now))
  1955. # Path to save after each epoch. Include placeholders that get filled by Keras.
  1956. self.checkpoint_path = os.path.join(self.log_dir, "2D_method_{}_*epoch*.h5".format(
  1957. self.config.NAME.lower()))
  1958. self.checkpoint_path = self.checkpoint_path.replace(
  1959. "*epoch*", "{epoch:04d}")
  1960. def train(self, train_dataset, val_dataset, learning_rate, epochs, layers,
  1961. augmentation=None, custom_callbacks=None, no_augmentation_sources=None):
  1962. """
  1963. layers: Allows selecting wich layers to train. It can be:
  1964. - A regular expression to match layer names to train
  1965. - One of these predefined values:
  1966. heads: The RPN, classifier and mask heads of the network
  1967. all: All the layers
  1968. 3+: Train Resnet stage 3 and up
  1969. 4+: Train Resnet stage 4 and up
  1970. 5+: Train Resnet stage 5 and up
  1971. """
  1972. assert self.mode == "training", "Create model in training mode."
  1973. # Pre-defined layer regular expressions
  1974. layer_regex = {
  1975. # all layers but the backbone
  1976. "heads": r"(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)|(conv1.*)",
  1977. # From a specific Resnet stage and up
  1978. "3+": r"(res3.*)|(bn3.*)|(res4.*)|(bn4.*)|(res5.*)|(bn5.*)|(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
  1979. "4+": r"(res4.*)|(bn4.*)|(res5.*)|(bn5.*)|(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
  1980. "5+": r"(res5.*)|(bn5.*)|(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
  1981. # All layers
  1982. "all": ".*",
  1983. }
  1984. if layers in layer_regex.keys():
  1985. layers = layer_regex[layers]
  1986. # Data generators
  1987. train_generator = data_generator(train_dataset, self.config, shuffle=True,
  1988. augmentation=augmentation,
  1989. batch_size=self.config.BATCH_SIZE,
  1990. no_augmentation_sources=no_augmentation_sources)
  1991. val_generator = data_generator(val_dataset, self.config, shuffle=True,
  1992. batch_size=self.config.BATCH_SIZE)
  1993. # Create log_dir if it does not exist
  1994. if not os.path.exists(self.log_dir):
  1995. os.makedirs(self.log_dir)
  1996. # Callbacks
  1997. callbacks = [
  1998. keras.callbacks.TensorBoard(log_dir=self.log_dir,
  1999. histogram_freq=0, write_graph=True, write_images=False),
  2000. keras.callbacks.ModelCheckpoint(self.checkpoint_path,
  2001. verbose=0, save_weights_only=True),
  2002. ]
  2003. # Add custom callbacks to the list
  2004. if custom_callbacks:
  2005. callbacks += custom_callbacks
  2006. # Train
  2007. log("\nStarting at epoch {}. LR={}\n".format(self.epoch, learning_rate))
  2008. log("Checkpoint Path: {}".format(self.checkpoint_path))
  2009. self.set_trainable(layers)
  2010. self.compile(learning_rate, self.config.LEARNING_MOMENTUM)
  2011. if os.name is 'nt':
  2012. workers = 0
  2013. else:
  2014. workers = multiprocessing.cpu_count()
  2015. # fit_generator
  2016. history = self.keras_model.fit_generator(
  2017. train_generator,
  2018. initial_epoch=self.epoch,
  2019. epochs=epochs,
  2020. steps_per_epoch=self.config.STEPS_PER_EPOCH,
  2021. callbacks=callbacks,
  2022. validation_data=val_generator,
  2023. validation_steps=self.config.VALIDATION_STEPS,
  2024. max_queue_size=100,
  2025. workers=workers,
  2026. use_multiprocessing=False,
  2027. )
  2028. self.epoch = max(self.epoch, epochs)
  2029. try:
  2030. a = history.epoch
  2031. b = history.history['loss']
  2032. b1 = history.history['rpn_class_loss']
  2033. b2 = history.history['rpn_bbox_loss']
  2034. b3 = history.history['mrcnn_class_loss']
  2035. b4 = history.history['mrcnn_bbox_loss']
  2036. c = history.history['val_loss']
  2037. c1 = history.history['val_rpn_class_loss']
  2038. c2 = history.history['val_rpn_bbox_loss']
  2039. c3 = history.history['val_mrcnn_class_loss']
  2040. c4 = history.history['val_mrcnn_bbox_loss']
  2041. epoch_list.extend(a)
  2042. tra_loss_list.extend(b)
  2043. tra1_loss_list.extend(b1)
  2044. tra2_loss_list.extend(b2)
  2045. tra3_loss_list.extend(b3)
  2046. tra4_loss_list.extend(b4)
  2047. val_loss_list.extend(c)
  2048. val1_loss_list.extend(c1)
  2049. val2_loss_list.extend(c2)
  2050. val3_loss_list.extend(c3)
  2051. val4_loss_list.extend(c4)
  2052. except Exception:
  2053. pass
  2054. def mold_inputs(self, images):
  2055. molded_images = []
  2056. image_metas = []
  2057. windows = []
  2058. for image in images:
  2059. if image.ndim == 2:
  2060. image = image[:, :, np.newaxis]
  2061. # Resize image
  2062. # TODO: move resizing to mold_image()
  2063. molded_image, window, scale, padding, crop = utils.resize_image(
  2064. image,
  2065. min_dim=self.config.IMAGE_MIN_DIM,
  2066. min_scale=self.config.IMAGE_MIN_SCALE,
  2067. max_dim=self.config.IMAGE_MAX_DIM,
  2068. mode=self.config.IMAGE_RESIZE_MODE)
  2069. molded_image = mold_image(molded_image, self.config)
  2070. # Build image_meta
  2071. image_meta = compose_image_meta(
  2072. 0, image.shape, molded_image.shape, window, scale,
  2073. np.zeros([self.config.NUM_CLASSES], dtype=np.int32))
  2074. # Append
  2075. molded_images.append(molded_image)
  2076. windows.append(window)
  2077. image_metas.append(image_meta)
  2078. # Pack into arrays
  2079. molded_images = np.stack(molded_images)
  2080. image_metas = np.stack(image_metas)
  2081. windows = np.stack(windows)
  2082. return molded_images, image_metas, windows
  2083. def unmold_detections(self, detections, mrcnn_mask, original_image_shape,
  2084. image_shape, window):
  2085. # How many detections do we have?
  2086. # Detections array is padded with zeros. Find the first class_id == 0.
  2087. zero_ix = np.where(detections[:, 4] == 0)[0]
  2088. N = zero_ix[0] if zero_ix.shape[0] > 0 else detections.shape[0]
  2089. # Extract boxes, class_ids, scores, and class-specific masks
  2090. boxes = detections[:N, :4]
  2091. class_ids = detections[:N, 4].astype(np.int32)
  2092. scores = detections[:N, 5]
  2093. masks = mrcnn_mask[np.arange(N), :, :, class_ids]
  2094. # Translate normalized coordinates in the resized image to pixel
  2095. # coordinates in the original image before resizing
  2096. window = utils.norm_boxes(window, image_shape[:2])
  2097. wy1, wx1, wy2, wx2 = window
  2098. shift = np.array([wy1, wx1, wy1, wx1])
  2099. wh = wy2 - wy1 # window height
  2100. ww = wx2 - wx1 # window width
  2101. scale = np.array([wh, ww, wh, ww])
  2102. # Convert boxes to normalized coordinates on the window
  2103. boxes = np.divide(boxes - shift, scale)
  2104. # Convert boxes to pixel coordinates on the original image
  2105. boxes = utils.denorm_boxes(boxes, original_image_shape[:2])
  2106. # Filter out detections with zero area. Happens in early training when
  2107. # network weights are still random
  2108. exclude_ix = np.where(
  2109. (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) <= 0)[0]
  2110. if exclude_ix.shape[0] > 0:
  2111. boxes = np.delete(boxes, exclude_ix, axis=0)
  2112. class_ids = np.delete(class_ids, exclude_ix, axis=0)
  2113. scores = np.delete(scores, exclude_ix, axis=0)
  2114. masks = np.delete(masks, exclude_ix, axis=0)
  2115. N = class_ids.shape[0]
  2116. # Resize masks to original image size and set boundary threshold.
  2117. full_masks = []
  2118. for i in range(N):
  2119. # Convert neural network mask to full size mask
  2120. full_mask = utils.unmold_mask(masks[i], boxes[i], original_image_shape)
  2121. full_masks.append(full_mask)
  2122. full_masks = np.stack(full_masks, axis=-1)\
  2123. if full_masks else np.empty(original_image_shape[:2] + (0,))
  2124. return boxes, class_ids, scores, full_masks
  2125. def unmold_detections_no_mask(self, detections, original_image_shape,
  2126. image_shape, window):
  2127. """The version of unmold_detections without masks"""
  2128. # How many detections do we have?
  2129. zero_ix = np.where(detections[:, 4] == 0)[0]
  2130. N = zero_ix[0] if zero_ix.shape[0] > 0 else detections.shape[0]
  2131. # Extract boxes, class_ids, scores
  2132. boxes = detections[:N, :4]
  2133. class_ids = detections[:N, 4].astype(np.int32)
  2134. scores = detections[:N, 5]
  2135. # Translate normalized coordinates to pixel coordinates
  2136. window = utils.norm_boxes(window, image_shape[:2])
  2137. wy1, wx1, wy2, wx2 = window
  2138. shift = np.array([wy1, wx1, wy1, wx1])
  2139. wh = wy2 - wy1
  2140. ww = wx2 - wx1
  2141. scale = np.array([wh, ww, wh, ww])
  2142. boxes = np.divide(boxes - shift, scale)
  2143. boxes = utils.denorm_boxes(boxes, original_image_shape[:2])
  2144. # Filter out detections with zero area
  2145. exclude_ix = np.where(
  2146. (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) <= 0)[0]
  2147. if exclude_ix.shape[0] > 0:
  2148. boxes = np.delete(boxes, exclude_ix, axis=0)
  2149. class_ids = np.delete(class_ids, exclude_ix, axis=0)
  2150. scores = np.delete(scores, exclude_ix, axis=0)
  2151. return boxes, class_ids, scores
  2152. def detect(self, images, verbose=0):
  2153. assert self.mode == "inference", "Create model in inference mode."
  2154. assert len(images) == self.config.BATCH_SIZE, "len(images) must be equal to BATCH_SIZE"
  2155. # If the current number of inputs for keras_model is not 3
  2156. # (indicating that it is a training model), then build an inference model
  2157. if len(self.keras_model.inputs) != 3:
  2158. if not hasattr(self, 'inference_model') or self.inference_model is None:
  2159. original_mode = self.mode
  2160. self.mode = "inference"
  2161. self.inference_model = self.build(mode="inference", config=self.config)
  2162. self.mode = original_mode
  2163. predict_model = self.inference_model
  2164. else:
  2165. predict_model = self.keras_model
  2166. if verbose:
  2167. log("Processing {} images".format(len(images)))
  2168. for image in images:
  2169. log("image", image)
  2170. # Mold inputs
  2171. molded_images, image_metas, windows = self.mold_inputs(images)
  2172. # Validate image sizes
  2173. image_shape = molded_images[0].shape
  2174. for g in molded_images[1:]:
  2175. assert g.shape == image_shape, \
  2176. "After resizing, all images must have the same size."
  2177. # Anchors
  2178. anchors = self.get_anchors(image_shape)
  2179. anchors = np.broadcast_to(anchors, (self.config.BATCH_SIZE,) + anchors.shape)
  2180. if verbose:
  2181. log("molded_images", molded_images)
  2182. log("image_metas", image_metas)
  2183. log("anchors", anchors)
  2184. # Run object detection (predict_model)
  2185. if self.config.USE_MASK:
  2186. detections, _, _, mrcnn_mask, _, _, _ = \
  2187. predict_model.predict([molded_images, image_metas, anchors], verbose=0)
  2188. else:
  2189. outputs = predict_model.predict([molded_images, image_metas, anchors], verbose=0)
  2190. if len(outputs) == 6:
  2191. detections, _, _, _, _, _ = outputs
  2192. mrcnn_mask = None
  2193. elif len(outputs) == 5:
  2194. detections, _, _, _, _ = outputs
  2195. mrcnn_mask = None
  2196. else:
  2197. detections = outputs[0]
  2198. mrcnn_mask = None
  2199. # Process detections
  2200. results = []
  2201. for i, image in enumerate(images):
  2202. if self.config.USE_MASK or mrcnn_mask is not None:
  2203. final_rois, final_class_ids, final_scores, final_masks = \
  2204. self.unmold_detections(detections[i], mrcnn_mask[i],
  2205. image.shape, molded_images[i].shape,
  2206. windows[i])
  2207. else:
  2208. final_rois, final_class_ids, final_scores = \
  2209. self.unmold_detections_no_mask(detections[i],
  2210. image.shape, molded_images[i].shape,
  2211. windows[i])
  2212. final_masks = None
  2213. results.append({
  2214. "rois": final_rois,
  2215. "class_ids": final_class_ids,
  2216. "scores": final_scores,
  2217. "masks": final_masks,
  2218. })
  2219. return results
  2220. def detect_molded(self, molded_images, image_metas, verbose=0):
  2221. assert self.mode == "inference", "Create model in inference mode."
  2222. assert len(molded_images) == self.config.BATCH_SIZE,\
  2223. "Number of images must be equal to BATCH_SIZE"
  2224. if verbose:
  2225. log("Processing {} images".format(len(molded_images)))
  2226. for image in molded_images:
  2227. log("image", image)
  2228. # Validate image sizes
  2229. # All images in a batch MUST be of the same size
  2230. image_shape = molded_images[0].shape
  2231. for g in molded_images[1:]:
  2232. assert g.shape == image_shape, "Images must have the same size"
  2233. # Anchors
  2234. anchors = self.get_anchors(image_shape)
  2235. anchors = np.broadcast_to(anchors, (self.config.BATCH_SIZE,) + anchors.shape)
  2236. if verbose:
  2237. log("molded_images", molded_images)
  2238. log("image_metas", image_metas)
  2239. log("anchors", anchors)
  2240. # Run object detection
  2241. detections, _, _, mrcnn_mask, _, _, _ =\
  2242. self.keras_model.predict([molded_images, image_metas, anchors], verbose=0)
  2243. # Process detections
  2244. results = []
  2245. for i, image in enumerate(molded_images):
  2246. window = [0, 0, image.shape[0], image.shape[1]]
  2247. final_rois, final_class_ids, final_scores, final_masks =\
  2248. self.unmold_detections(detections[i], mrcnn_mask[i],
  2249. image.shape, molded_images[i].shape,
  2250. window)
  2251. results.append({
  2252. "rois": final_rois,
  2253. "class_ids": final_class_ids,
  2254. "scores": final_scores,
  2255. "masks": final_masks,
  2256. })
  2257. return results
  2258. def get_anchors(self, image_shape):
  2259. backbone_shapes = compute_backbone_shapes(self.config, image_shape)
  2260. # Cache anchors and reuse if image shape is the same
  2261. if not hasattr(self, "_anchor_cache"):
  2262. self._anchor_cache = {}
  2263. if not tuple(image_shape) in self._anchor_cache:
  2264. # Generate Anchors
  2265. a = utils.generate_pyramid_anchors(
  2266. self.config.RPN_ANCHOR_SCALES,
  2267. self.config.RPN_ANCHOR_RATIOS,
  2268. backbone_shapes,
  2269. self.config.BACKBONE_STRIDES,
  2270. self.config.RPN_ANCHOR_STRIDE)
  2271. self.anchors = a
  2272. # Normalize coordinates
  2273. self._anchor_cache[tuple(image_shape)] = utils.norm_boxes(a, image_shape[:2])
  2274. return self._anchor_cache[tuple(image_shape)]
  2275. def ancestor(self, tensor, name, checked=None):
  2276. checked = checked if checked is not None else []
  2277. # Put a limit on how deep we go to avoid very long loops
  2278. if len(checked) > 500:
  2279. return None
  2280. # Convert name to a regex and allow matching a number prefix
  2281. # because Keras adds them automatically
  2282. if isinstance(name, str):
  2283. name = re.compile(name.replace("/", r"(\_\d+)*/"))
  2284. parents = tensor.op.inputs
  2285. for p in parents:
  2286. if p in checked:
  2287. continue
  2288. if bool(re.fullmatch(name, p.name)):
  2289. return p
  2290. checked.append(p)
  2291. a = self.ancestor(p, name, checked)
  2292. if a is not None:
  2293. return a
  2294. return None
  2295. def find_trainable_layer(self, layer):
  2296. if layer.__class__.__name__ == 'TimeDistributed':
  2297. return self.find_trainable_layer(layer.layer)
  2298. return layer
  2299. def get_trainable_layers(self):
  2300. layers = []
  2301. # Loop through all layers
  2302. for l in self.keras_model.layers:
  2303. # If layer is a wrapper, find inner trainable layer
  2304. l = self.find_trainable_layer(l)
  2305. # Include layer if it has weights
  2306. if l.get_weights():
  2307. layers.append(l)
  2308. return layers
  2309. def run_graph(self, images, outputs, image_metas=None):
  2310. model = self.keras_model
  2311. # Organize desired outputs into an ordered dict
  2312. outputs = OrderedDict(outputs)
  2313. for o in outputs.values():
  2314. assert o is not None
  2315. # Build a Keras function to run parts of the computation graph
  2316. inputs = model.inputs
  2317. if model.uses_learning_phase and not isinstance(K.learning_phase(), int):
  2318. inputs += [K.learning_phase()]
  2319. kf = K.function(model.inputs, list(outputs.values()))
  2320. # Prepare inputs
  2321. if image_metas is None:
  2322. molded_images, image_metas, _ = self.mold_inputs(images)
  2323. else:
  2324. molded_images = images
  2325. image_shape = molded_images[0].shape
  2326. # Anchors
  2327. anchors = self.get_anchors(image_shape)
  2328. anchors = np.broadcast_to(anchors, (self.config.BATCH_SIZE,) + anchors.shape)
  2329. model_in = [molded_images, image_metas, anchors]
  2330. # Run inference
  2331. if model.uses_learning_phase and not isinstance(K.learning_phase(), int):
  2332. model_in.append(0.)
  2333. outputs_np = kf(model_in)
  2334. # Pack the generated Numpy arrays into a a dict and log the results.
  2335. outputs_np = OrderedDict([(k, v)
  2336. for k, v in zip(outputs.keys(), outputs_np)])
  2337. for k, v in outputs_np.items():
  2338. log(k, v)
  2339. return outputs_np
  2340. # Data Formatting
  2341. def compose_image_meta(image_id, original_image_shape, image_shape,
  2342. window, scale, active_class_ids):
  2343. meta = np.array(
  2344. [image_id] + # size=1
  2345. list(original_image_shape) + # size=3
  2346. list(image_shape) + # size=3
  2347. list(window) + # size=4 (y1, x1, y2, x2) in image cooredinates
  2348. [scale] + # size=1
  2349. list(active_class_ids) # size=num_classes
  2350. )
  2351. return meta
  2352. def parse_image_meta(meta):
  2353. image_id = meta[:, 0]
  2354. original_image_shape = meta[:, 1:4]
  2355. image_shape = meta[:, 4:7]
  2356. window = meta[:, 7:11] # (y1, x1, y2, x2) window of image in in pixels
  2357. scale = meta[:, 11]
  2358. active_class_ids = meta[:, 12:]
  2359. return {
  2360. "image_id": image_id.astype(np.int32),
  2361. "original_image_shape": original_image_shape.astype(np.int32),
  2362. "image_shape": image_shape.astype(np.int32),
  2363. "window": window.astype(np.int32),
  2364. "scale": scale.astype(np.float32),
  2365. "active_class_ids": active_class_ids.astype(np.int32),
  2366. }
  2367. def parse_image_meta_graph(meta):
  2368. image_id = meta[:, 0]
  2369. original_image_shape = meta[:, 1:4]
  2370. image_shape = meta[:, 4:7]
  2371. window = meta[:, 7:11] # (y1, x1, y2, x2) window of image in in pixels
  2372. scale = meta[:, 11]
  2373. active_class_ids = meta[:, 12:]
  2374. return {
  2375. "image_id": image_id,
  2376. "original_image_shape": original_image_shape,
  2377. "image_shape": image_shape,
  2378. "window": window,
  2379. "scale": scale,
  2380. "active_class_ids": active_class_ids,
  2381. }
  2382. def mold_image(images, config):
  2383. return images.astype(np.float32) - config.MEAN_PIXEL
  2384. def unmold_image(normalized_images, config):
  2385. return (normalized_images + config.MEAN_PIXEL).astype(np.uint8)
  2386. # Miscellenous Graph Functions
  2387. def trim_zeros_graph(boxes, name='trim_zeros'):
  2388. non_zeros = tf.cast(tf.reduce_sum(tf.abs(boxes), axis=1), tf.bool)
  2389. boxes = tf.boolean_mask(boxes, non_zeros, name=name)
  2390. return boxes, non_zeros
  2391. def batch_pack_graph(x, counts, num_rows):
  2392. outputs = []
  2393. for i in range(num_rows):
  2394. outputs.append(x[i, :counts[i]])
  2395. return tf.concat(outputs, axis=0)
  2396. def norm_boxes_graph(boxes, shape):
  2397. h, w = tf.split(tf.cast(shape, tf.float32), 2)
  2398. scale = tf.concat([h, w, h, w], axis=-1) - tf.constant(1.0)
  2399. shift = tf.constant([0., 0., 1., 1.])
  2400. return tf.divide(boxes - shift, scale)
  2401. def denorm_boxes_graph(boxes, shape):
  2402. h, w = tf.split(tf.cast(shape, tf.float32), 2)
  2403. scale = tf.concat([h, w, h, w], axis=-1) - tf.constant(1.0)
  2404. shift = tf.constant([0., 0., 1., 1.])
  2405. return tf.cast(tf.round(tf.multiply(boxes, scale) + shift), tf.int32)
  2406. def return_value(epoch_loss, tra_loss, tra1_loss, tra2_loss, tra3_loss, tra4_loss,
  2407. val_loss, val1_loss, val2_loss, val3_loss, val4_loss):
  2408. return epoch_loss, tra_loss, tra1_loss, tra2_loss, tra3_loss, tra4_loss, val_loss, val1_loss, val2_loss, val3_loss, val4_loss
  2409. def call_back():
  2410. 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,
  2411. tra4_loss_list, val_loss_list, val1_loss_list,
  2412. val2_loss_list, val3_loss_list, val4_loss_list)
  2413. return a, b, b1, b2, b3, b4, c, c1, c2, c3, c4

model.py at commit cea2b38, no license · at the source

Overview

Authors: Guixuan Gong1, Xin Liu1, Xueyan Jia2, Ben Long3, Siqi Chen1, Tao Jiang2, Yue Luo1, Zhao Feng2,3, Xiangning Li2,3, Qingming Luo3, Hui Gong1,2, Anan Li1,2,3
  1. MoE Key Laboratory for Biomedical Photonics, Wuhan National Laboratory for Optoelectronics, Huazhong University of Science and Technology, Wuhan, 430074, China
  2. HUST-Suzhou Institute for Brainsmatics, JITRI, Suzhou, 215123, China
  3. State Key Laboratory of Digital Medical Engineering, Key Laboratory of Biomedical Engineering of Hainan Province, School of Biomedical Engineering, Hainan University, Haikou, 570228, China
Journal: Biomedical optics express, volume 17, issue 8, pages 4198-4215
Dates: received 12 May 2026; accepted 6 July 2026; published online 20 July 2026
Type: Research article · Language: English
License: none stated
Identifiers: DOI 10.1364/boe.605322 · PMID 42610137 · PMCID PMC13481076 · OpenAlex W7168184877
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: mouse (organism)
Methods: Connectivity, Machine learning
Topic: Medical Image Segmentation Techniques (Computer Vision and Pattern Recognition, Computer Science), according to OpenAlex
Funding: Brain Science and Brain-like Intelligence Technology - National Science and Technology Major Project (2021ZD02010002, 2021ZD02010001); National Natural Science Foundation of China https://ror.org/01h0zpd94 (91749209); 111 Project (D23022)
Citations: cited by 1 paper (Europe PMC); 38 references in the paper

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

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: cea2b38bfa09f29584a05a821771d2f25aaa8fa5, 7 May 2026
Languages: Python (38), Jupyter (2)
Size: 99 files, 40 scripts
Software Heritage: not archived
Found in: the references
Holds: README, environment (2D_detection/requirements.txt, 3D_detection/requirements.txt, label_revise/requirements.txt), 2 notebooks
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (33 files), pandas (18 files), OpenCV (15 files), Matplotlib (12 files), scikit-image (10 files), PyTorch (7 files), SimpleITK (7 files), tifffile (7 files), TensorFlow (6 files), Pillow (3 files), Keras (2 files), SciPy (2 files), h5py (1 file), imageio (1 file), scikit-learn (1 file), seaborn (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
41 files

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://doi.org/10.1364/boe.605322

BibTeX

@article{gong2026generalized,
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/boe.605322},
url = {https://doi.org/10.1364/boe.605322},
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/07/20
VL - 17
IS - 8
SP - 4198
EP - 4215
SN - 2156-7085
PB - Optica Publishing Group
DO - 10.1364/boe.605322
UR - https://doi.org/10.1364/boe.605322
LA - en
ER -

CSL-JSON

{
"id": "10.1364/boe.605322",
"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": "Biomed Opt Express",
"volume": "17",
"issue": "8",
"page": "4198-4215",
"DOI": "10.1364/boe.605322",
"PMID": "42610137",
"PMCID": "PMC13481076",
"ISSN": "2156-7085",
"publisher": "Optica Publishing Group",
"URL": "https://doi.org/10.1364/boe.605322",
"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 neuroscience
In 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 control
Journal: eLife
In 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. Medicine
In 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 biology
In 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 communications
In 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 intelligence
In 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 neuroscience
In 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 medicine
In 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 biology
In 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.

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.