import itertools from omegaconf import OmegaConf def bifpn_config(min_level, max_level, weight_method=None): """BiFPN config. Adapted from https://github.com/google/automl/blob/56815c9986ffd4b508fe1d68508e268d129715c1/efficientdet/keras/fpn_configs.py """ p = OmegaConf.create() weight_method = weight_method or 'fastattn' num_levels = max_level - min_level + 1 node_ids = {min_level + i: [i] for i in range(num_levels)} level_last_id = lambda level: node_ids[level][-1] level_all_ids = lambda level: node_ids[level] id_cnt = itertools.count(num_levels) p.nodes = [] for i in range(max_level - 1, min_level - 1, -1): # top-down path. p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': [level_last_id(i), level_last_id(i + 1)], 'weight_method': weight_method, }) node_ids[i].append(next(id_cnt)) for i in range(min_level + 1, max_level + 1): # bottom-up path. p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': level_all_ids(i) + [level_last_id(i - 1)], 'weight_method': weight_method, }) node_ids[i].append(next(id_cnt)) return p def panfpn_config(min_level, max_level, weight_method=None): """PAN FPN config. This defines FPN layout from Path Aggregation Networks as an alternate to BiFPN, it does not implement the full PAN spec. Paper: https://arxiv.org/abs/1803.01534 """ p = OmegaConf.create() weight_method = weight_method or 'fastattn' num_levels = max_level - min_level + 1 node_ids = {min_level + i: [i] for i in range(num_levels)} level_last_id = lambda level: node_ids[level][-1] id_cnt = itertools.count(num_levels) p.nodes = [] for i in range(max_level, min_level - 1, -1): # top-down path. offsets = [level_last_id(i), level_last_id(i + 1)] if i != max_level else [level_last_id(i)] p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': offsets, 'weight_method': weight_method, }) node_ids[i].append(next(id_cnt)) for i in range(min_level, max_level + 1): # bottom-up path. offsets = [level_last_id(i), level_last_id(i - 1)] if i != min_level else [level_last_id(i)] p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': offsets, 'weight_method': weight_method, }) node_ids[i].append(next(id_cnt)) return p def qufpn_config(min_level, max_level, weight_method=None): """A dynamic quad fpn config that can adapt to different min/max levels. It extends the idea of BiFPN, and has four paths: (up_down -> bottom_up) + (bottom_up -> up_down). Paper: https://ieeexplore.ieee.org/document/9225379 Ref code: From contribution to TF EfficientDet https://github.com/google/automl/blob/eb74c6739382e9444817d2ad97c4582dbe9a9020/efficientdet/keras/fpn_configs.py """ p = OmegaConf.create() weight_method = weight_method or 'fastattn' quad_method = 'fastattn' num_levels = max_level - min_level + 1 node_ids = {min_level + i: [i] for i in range(num_levels)} level_last_id = lambda level: node_ids[level][-1] level_all_ids = lambda level: node_ids[level] level_first_id = lambda level: node_ids[level][0] id_cnt = itertools.count(num_levels) p.nodes = [] for i in range(max_level - 1, min_level - 1, -1): # top-down path 1. p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': [level_last_id(i), level_last_id(i + 1)], 'weight_method': weight_method }) node_ids[i].append(next(id_cnt)) node_ids[max_level].append(node_ids[max_level][-1]) for i in range(min_level + 1, max_level): # bottom-up path 2. p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': level_all_ids(i) + [level_last_id(i - 1)], 'weight_method': weight_method }) node_ids[i].append(next(id_cnt)) i = max_level p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': [level_first_id(i)] + [level_last_id(i - 1)], 'weight_method': weight_method }) node_ids[i].append(next(id_cnt)) node_ids[min_level].append(node_ids[min_level][-1]) for i in range(min_level + 1, max_level + 1, 1): # bottom-up path 3. p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': [ level_first_id(i), level_last_id(i - 1) if i != min_level + 1 else level_first_id(i - 1)], 'weight_method': weight_method }) node_ids[i].append(next(id_cnt)) node_ids[min_level].append(node_ids[min_level][-1]) for i in range(max_level - 1, min_level, -1): # top-down path 4. p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': [node_ids[i][0]] + [node_ids[i][-1]] + [level_last_id(i + 1)], 'weight_method': weight_method }) node_ids[i].append(next(id_cnt)) i = min_level p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': [node_ids[i][0]] + [level_last_id(i + 1)], 'weight_method': weight_method }) node_ids[i].append(next(id_cnt)) node_ids[max_level].append(node_ids[max_level][-1]) # NOTE: the order of the quad path is reversed from the original, my code expects the output of # each FPN repeat to be same as input from backbone, in order of increasing reductions for i in range(min_level, max_level + 1): # quad-add path. p.nodes.append({ 'reduction': 1 << i, 'inputs_offsets': [node_ids[i][2], node_ids[i][4]], 'weight_method': quad_method }) node_ids[i].append(next(id_cnt)) return p def get_fpn_config(fpn_name, min_level=3, max_level=7): if not fpn_name: fpn_name = 'bifpn_fa' name_to_config = { 'bifpn_sum': bifpn_config(min_level=min_level, max_level=max_level, weight_method='sum'), 'bifpn_attn': bifpn_config(min_level=min_level, max_level=max_level, weight_method='attn'), 'bifpn_fa': bifpn_config(min_level=min_level, max_level=max_level, weight_method='fastattn'), 'pan_sum': panfpn_config(min_level=min_level, max_level=max_level, weight_method='sum'), 'pan_fa': panfpn_config(min_level=min_level, max_level=max_level, weight_method='fastattn'), 'qufpn_sum': qufpn_config(min_level=min_level, max_level=max_level, weight_method='sum'), 'qufpn_fa': qufpn_config(min_level=min_level, max_level=max_level, weight_method='fastattn'), } return name_to_config[fpn_name]