# Adapted FROM: https://github.com/PacktPublishing/Hands-On-Computer-Vision-with-Detectron2/blob/main/Chapter07/Detectron2_Chapter07_Anchors.ipynb
import numpy as np
import matplotlib.pyplot as plt
from scipy.cluster.vq import kmeans
import math
from tqdm import tqdm
from pandas import json_normalize
import json
from typing import List, Union
plt.rcParams["figure.dpi"] = 150
[docs]
def boxes2wh(boxes):
x1y1 = boxes[:, :2]
x2y2 = boxes[:, 2:]
return x2y2 - x1y1
[docs]
def wh2ratio(wh):
return wh[:, 1] / wh[:, 0]
[docs]
def best_ratio(ac_wh, gt_wh):
all_ratios = gt_wh[:, None] / ac_wh[None]
inverse_ratios = 1 / all_ratios
ratios = np.minimum(all_ratios, inverse_ratios)
worst = np.min(ratios, axis=-1)
best = np.max(worst, axis=-1)
return best
[docs]
def fitness(ac_wh, gt_wh, EDGE_RATIO_THRESHOLD = 0.25):
ratio = best_ratio(ac_wh, gt_wh)
return (ratio * (ratio > EDGE_RATIO_THRESHOLD)).mean()
[docs]
def best_recall(ac_wh, gt_wh, EDGE_RATIO_THRESHOLD=0.25):
ratio = best_ratio(ac_wh, gt_wh)
best = (ratio > EDGE_RATIO_THRESHOLD).float().mean()
return best
[docs]
def estimate_clusters(values, num_clusters, iter=100):
std = values.std(0).item()
k, _ = kmeans(values / std, num_clusters, iter=iter)
k *= std
return k
[docs]
def visualize_clusters(values, centers):
plt.hist(values, histtype="step")
plt.scatter(centers, [0]*len(centers), c="red")
plt.show()
[docs]
def evolve(sizes, ratios, gt_wh,
iterations=10_000,
probability=0.9,
muy=1, sigma=0.05,
fit_fn=fitness,
verbose=False
):
anchors = generate_cell_anchors(tuple(sizes), tuple(ratios))
ac_wh = boxes2wh(anchors)
best_fit = fit_fn(ac_wh, gt_wh)
anchor_shape = len(sizes) + len(ratios)
pbar = tqdm(range(iterations), desc="Evolving ratios and sizes:")
for i, _ in enumerate(pbar):
mutation = np.ones(anchor_shape)
mutate = np.random.random(anchor_shape) < probability
mutation = np.random.normal(muy, sigma, anchor_shape)*mutate
mutation = mutation.clip(0.3, 3.0)
# mutated
mutated_sizes = sizes.copy()*mutation[:len(sizes)]
mutated_ratios = ratios.copy()*mutation[-len(ratios):]
mutated_anchors = generate_cell_anchors(tuple(mutated_sizes), tuple(mutated_ratios))
mutated_ac_wh = boxes2wh(mutated_anchors)
mutated_fit = fit_fn(mutated_ac_wh, gt_wh)
if mutated_fit > best_fit:
sizes = mutated_sizes.copy()
ratios = mutated_ratios.copy()
best_fit = mutated_fit
pbar.desc = (f"Evolving {ratios} and {sizes}, Fitness = {best_fit: .4f}")
return sizes, ratios
# COPIED FROM: Detectron2 source code detectron2/modeling/anchor_generator.py
[docs]
def generate_cell_anchors(sizes=(32, 64, 128, 256, 512),
aspect_ratios=(0.5, 1, 2)
):
"""
Generate a tensor storing canonical anchor boxes, which are all anchor
boxes of different sizes and aspect_ratios centered at (0, 0).
We can later build the set of anchors for a full feature map by
shifting and tiling these tensors (see `meth:_grid_anchors`).
Args:
sizes (tuple[float]):
aspect_ratios (tuple[float]]):
Returns:
Array of shape (len(sizes) * len(aspect_ratios), 4) storing anchor boxes
in XYXY format.
"""
anchors = []
for size in sizes:
area = size**2.0
for aspect_ratio in aspect_ratios:
w = math.sqrt(area / aspect_ratio)
h = aspect_ratio * w
x0, y0, x1, y1 = -w / 2.0, -h / 2.0, w / 2.0, h / 2.0
anchors.append([x0, y0, x1, y1])
return np.array(anchors)
[docs]
def _wh2size(gt_wh):
return np.sqrt(gt_wh[:,0] * gt_wh[:,1])
[docs]
def _boxes2wh(boxes):
wh = boxes[:, 2:]
return wh
[docs]
def get_size_ratio_fitness_score(sizes, ratios, gt_wh):
anchors = generate_cell_anchors(sizes=tuple(sizes),
aspect_ratios=tuple(ratios)
)
anchor_wh = boxes2wh(anchors)
fit_score = fitness(anchor_wh, gt_wh)
return fit_score
[docs]
def coco_annotation_to_df(coco_annotation_file):
with open(coco_annotation_file, "r") as annot_file:
annotation = json.load(annot_file)
annotations_df = json_normalize(annotation, "annotations")
annot_imgs_df = json_normalize(annotation, "images")
annot_cat_df = json_normalize(annotation, "categories")
annotations_images_merge_df = annotations_df.merge(annot_imgs_df, left_on='image_id',
right_on='id',
suffixes=("_annotation", "_image"),
how="outer"
)
annotations_imgs_cat_merge = annotations_images_merge_df.merge(annot_cat_df, left_on="category_id", right_on="id",
suffixes=(None, '_categories'),
how="outer"
)
all_merged_df = annotations_imgs_cat_merge[['id_annotation', 'image_id','category_id', 'bbox', 'area', 'segmentation', 'iscrowd',
'file_name', 'height', 'width', 'name', 'supercategory'
]]
all_merged_df.rename(columns={"name": "category_name",
"height": "image_height",
"width": "image_width"},
inplace=True
)
all_merged_df.dropna(subset=["file_name"], inplace=True)
return all_merged_df
[docs]
class AnchorMiner(object):
def __init__(self,
coco_annotation_file: str
) -> None:
self.coco_annotation_file = coco_annotation_file
self.annotations_df = coco_annotation_to_df(coco_annotation_file)
[docs]
def get_sizes_ratios(self, num_sizes=5, num_ratios=3):
self.gt_boxes = np.concatenate(self.annotations_df.bbox.dropna().values).reshape(-1,4)
self.gt_wh = _boxes2wh(self.gt_boxes)
gt_sizes = _wh2size(self.gt_wh)
gt_ratios = wh2ratio(self.gt_wh)
self.sizes = estimate_clusters(gt_sizes, num_sizes)
self.ratios = estimate_clusters(gt_ratios, num_ratios)
return self.sizes, self.ratios
[docs]
def tune_sizes_ratios(self, sizes: Union[None, List]=None,
ratios: Union[None, List]=None,
iterations=10_000,
include_fitness_score=True,
):
if not sizes and not ratios:
if hasattr(self, "sizes"):
sizes = self.sizes
ratios = self.ratios
else:
sizes, ratios = self.get_sizes_ratios()
elif not sizes:
if hasattr(self, "sizes"):
sizes = self.sizes
else:
sizes, _ = self.get_sizes_ratios()
elif not ratios:
if hasattr(self, "ratios"):
ratios = self.ratios
else:
_, ratios = self.get_sizes_ratios()
if hasattr(self, "gt_boxes"):
gt_boxes = self.gt_boxes
else:
gt_boxes = np.concatenate(self.annotations_df.bbox.dropna().values).reshape(-1,4)
if hasattr(self, "gt_wh"):
gt_wh = self.gt_wh
else:
gt_wh = _boxes2wh(gt_boxes)
e_sizes, e_ratios = evolve(sizes, ratios, gt_wh,
iterations=iterations
)
self.tuned_sizes = e_sizes
self.tuned_ratios = e_ratios
if include_fitness_score:
self.fitness_score = get_size_ratio_fitness_score(e_sizes, e_ratios, gt_wh)
return e_sizes, e_ratios, self.fitness_score if include_fitness_score else (e_sizes, e_ratios, None)