first commit
This commit is contained in:
commit
54ccfb11fe
218 files changed
+19525
No files matched your search
Executable
Whitespace-only changes.
Executable
+61
@@ -0,0 +1,61 @@
|
||||
import torch
|
||||
from torch.utils.data import DataLoader
|
||||
from torch.utils.data.dataloader import default_collate
|
||||
|
||||
from ssd.data import samplers
|
||||
from ssd.data.datasets import build_dataset
|
||||
from ssd.data.transforms import build_transforms, build_target_transform
|
||||
from ssd.structures.container import Container
|
||||
|
||||
|
||||
class BatchCollator:
|
||||
def __init__(self, is_train=True):
|
||||
self.is_train = is_train
|
||||
|
||||
def __call__(self, batch):
|
||||
transposed_batch = list(zip(*batch))
|
||||
images = default_collate(transposed_batch[0])
|
||||
img_ids = default_collate(transposed_batch[2])
|
||||
|
||||
if self.is_train:
|
||||
list_targets = transposed_batch[1]
|
||||
targets = Container(
|
||||
{key: default_collate([d[key] for d in list_targets]) for key in list_targets[0]}
|
||||
)
|
||||
else:
|
||||
targets = None
|
||||
return images, targets, img_ids
|
||||
|
||||
|
||||
def make_data_loader(cfg, is_train=True, distributed=False, max_iter=None, start_iter=0):
|
||||
train_transform = build_transforms(cfg, is_train=is_train)
|
||||
target_transform = build_target_transform(cfg) if is_train else None
|
||||
dataset_list = cfg.DATASETS.TRAIN if is_train else cfg.DATASETS.TEST
|
||||
datasets = build_dataset(dataset_list, transform=train_transform, target_transform=target_transform, is_train=is_train)
|
||||
|
||||
shuffle = is_train or distributed
|
||||
|
||||
data_loaders = []
|
||||
|
||||
for dataset in datasets:
|
||||
if distributed:
|
||||
sampler = samplers.DistributedSampler(dataset, shuffle=shuffle)
|
||||
elif shuffle:
|
||||
sampler = torch.utils.data.RandomSampler(dataset)
|
||||
else:
|
||||
sampler = torch.utils.data.sampler.SequentialSampler(dataset)
|
||||
|
||||
batch_size = cfg.SOLVER.BATCH_SIZE if is_train else cfg.TEST.BATCH_SIZE
|
||||
batch_sampler = torch.utils.data.sampler.BatchSampler(sampler=sampler, batch_size=batch_size, drop_last=False)
|
||||
if max_iter is not None:
|
||||
batch_sampler = samplers.IterationBasedBatchSampler(batch_sampler, num_iterations=max_iter, start_iter=start_iter)
|
||||
|
||||
data_loader = DataLoader(dataset, num_workers=cfg.DATA_LOADER.NUM_WORKERS, batch_sampler=batch_sampler,
|
||||
pin_memory=cfg.DATA_LOADER.PIN_MEMORY, collate_fn=BatchCollator(is_train))
|
||||
data_loaders.append(data_loader)
|
||||
|
||||
if is_train:
|
||||
# during training, a single (possibly concatenated) data_loader is returned
|
||||
assert len(data_loaders) == 1
|
||||
return data_loaders[0]
|
||||
return data_loaders
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
from torch.utils.data import ConcatDataset
|
||||
|
||||
from ssd.config.path_catlog import DatasetCatalog
|
||||
from .voc import VOCDataset
|
||||
from .coco import COCODataset
|
||||
|
||||
_DATASETS = {
|
||||
'VOCDataset': VOCDataset,
|
||||
'COCODataset': COCODataset,
|
||||
}
|
||||
|
||||
|
||||
def build_dataset(dataset_list, transform=None, target_transform=None, is_train=True):
|
||||
assert len(dataset_list) > 0
|
||||
datasets = []
|
||||
for dataset_name in dataset_list:
|
||||
data = DatasetCatalog.get(dataset_name)
|
||||
args = data['args']
|
||||
factory = _DATASETS[data['factory']]
|
||||
args['transform'] = transform
|
||||
args['target_transform'] = target_transform
|
||||
if factory == VOCDataset:
|
||||
args['keep_difficult'] = not is_train
|
||||
elif factory == COCODataset:
|
||||
args['remove_empty'] = is_train
|
||||
dataset = factory(**args)
|
||||
datasets.append(dataset)
|
||||
# for testing, return a list of datasets
|
||||
if not is_train:
|
||||
return datasets
|
||||
dataset = datasets[0]
|
||||
if len(datasets) > 1:
|
||||
dataset = ConcatDataset(datasets)
|
||||
|
||||
return [dataset]
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
import os
|
||||
import torch.utils.data
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from ssd.structures.container import Container
|
||||
|
||||
|
||||
class COCODataset(torch.utils.data.Dataset):
|
||||
class_names = ('__background__',
|
||||
'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus',
|
||||
'train', 'truck', 'boat', 'traffic light', 'fire hydrant',
|
||||
'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog',
|
||||
'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra',
|
||||
'giraffe', 'backpack', 'umbrella', 'handbag', 'tie',
|
||||
'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
|
||||
'kite', 'baseball bat', 'baseball glove', 'skateboard',
|
||||
'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup',
|
||||
'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
|
||||
'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
|
||||
'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed',
|
||||
'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote',
|
||||
'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink',
|
||||
'refrigerator', 'book', 'clock', 'vase', 'scissors',
|
||||
'teddy bear', 'hair drier', 'toothbrush')
|
||||
|
||||
def __init__(self, data_dir, ann_file, transform=None, target_transform=None, remove_empty=False):
|
||||
from pycocotools.coco import COCO
|
||||
self.coco = COCO(ann_file)
|
||||
self.data_dir = data_dir
|
||||
self.transform = transform
|
||||
self.target_transform = target_transform
|
||||
self.remove_empty = remove_empty
|
||||
if self.remove_empty:
|
||||
# when training, images without annotations are removed.
|
||||
self.ids = list(self.coco.imgToAnns.keys())
|
||||
else:
|
||||
# when testing, all images used.
|
||||
self.ids = list(self.coco.imgs.keys())
|
||||
coco_categories = sorted(self.coco.getCatIds())
|
||||
self.coco_id_to_contiguous_id = {coco_id: i + 1 for i, coco_id in enumerate(coco_categories)}
|
||||
self.contiguous_id_to_coco_id = {v: k for k, v in self.coco_id_to_contiguous_id.items()}
|
||||
|
||||
def __getitem__(self, index):
|
||||
image_id = self.ids[index]
|
||||
boxes, labels = self._get_annotation(image_id)
|
||||
image = self._read_image(image_id)
|
||||
if self.transform:
|
||||
image, boxes, labels = self.transform(image, boxes, labels)
|
||||
if self.target_transform:
|
||||
boxes, labels = self.target_transform(boxes, labels)
|
||||
targets = Container(
|
||||
boxes=boxes,
|
||||
labels=labels,
|
||||
)
|
||||
return image, targets, index
|
||||
|
||||
def get_annotation(self, index):
|
||||
image_id = self.ids[index]
|
||||
return image_id, self._get_annotation(image_id)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.ids)
|
||||
|
||||
def _get_annotation(self, image_id):
|
||||
ann_ids = self.coco.getAnnIds(imgIds=image_id)
|
||||
ann = self.coco.loadAnns(ann_ids)
|
||||
# filter crowd annotations
|
||||
ann = [obj for obj in ann if obj["iscrowd"] == 0]
|
||||
boxes = np.array([self._xywh2xyxy(obj["bbox"]) for obj in ann], np.float32).reshape((-1, 4))
|
||||
labels = np.array([self.coco_id_to_contiguous_id[obj["category_id"]] for obj in ann], np.int64).reshape((-1,))
|
||||
# remove invalid boxes
|
||||
keep = (boxes[:, 3] > boxes[:, 1]) & (boxes[:, 2] > boxes[:, 0])
|
||||
boxes = boxes[keep]
|
||||
labels = labels[keep]
|
||||
return boxes, labels
|
||||
|
||||
def _xywh2xyxy(self, box):
|
||||
x1, y1, w, h = box
|
||||
return [x1, y1, x1 + w, y1 + h]
|
||||
|
||||
def get_img_info(self, index):
|
||||
image_id = self.ids[index]
|
||||
img_data = self.coco.imgs[image_id]
|
||||
return img_data
|
||||
|
||||
def _read_image(self, image_id):
|
||||
file_name = self.coco.loadImgs(image_id)[0]['file_name']
|
||||
image_file = os.path.join(self.data_dir, file_name)
|
||||
image = Image.open(image_file).convert("RGB")
|
||||
image = np.array(image)
|
||||
return image
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
from ssd.data.datasets import VOCDataset, COCODataset
|
||||
from .coco import coco_evaluation
|
||||
from .voc import voc_evaluation
|
||||
|
||||
|
||||
def evaluate(dataset, predictions, output_dir, **kwargs):
|
||||
"""evaluate dataset using different methods based on dataset type.
|
||||
Args:
|
||||
dataset: Dataset object
|
||||
predictions(list[(boxes, labels, scores)]): Each item in the list represents the
|
||||
prediction results for one image. And the index should match the dataset index.
|
||||
output_dir: output folder, to save evaluation files or results.
|
||||
Returns:
|
||||
evaluation result
|
||||
"""
|
||||
args = dict(
|
||||
dataset=dataset, predictions=predictions, output_dir=output_dir, **kwargs,
|
||||
)
|
||||
if isinstance(dataset, VOCDataset):
|
||||
return voc_evaluation(**args)
|
||||
elif isinstance(dataset, COCODataset):
|
||||
return coco_evaluation(**args)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def coco_evaluation(dataset, predictions, output_dir, iteration=None):
|
||||
coco_results = []
|
||||
for i, prediction in enumerate(predictions):
|
||||
img_info = dataset.get_img_info(i)
|
||||
prediction = prediction.resize((img_info['width'], img_info['height'])).numpy()
|
||||
boxes, labels, scores = prediction['boxes'], prediction['labels'], prediction['scores']
|
||||
|
||||
image_id, annotation = dataset.get_annotation(i)
|
||||
class_mapper = dataset.contiguous_id_to_coco_id
|
||||
if labels.shape[0] == 0:
|
||||
continue
|
||||
|
||||
boxes = boxes.tolist()
|
||||
labels = labels.tolist()
|
||||
scores = scores.tolist()
|
||||
coco_results.extend(
|
||||
[
|
||||
{
|
||||
"image_id": image_id,
|
||||
"category_id": class_mapper[labels[k]],
|
||||
"bbox": [box[0], box[1], box[2] - box[0], box[3] - box[1]], # to xywh format
|
||||
"score": scores[k],
|
||||
}
|
||||
for k, box in enumerate(boxes)
|
||||
]
|
||||
)
|
||||
iou_type = 'bbox'
|
||||
json_result_file = os.path.join(output_dir, iou_type + ".json")
|
||||
logger = logging.getLogger("SSD.inference")
|
||||
logger.info('Writing results to {}...'.format(json_result_file))
|
||||
with open(json_result_file, "w") as f:
|
||||
json.dump(coco_results, f)
|
||||
from pycocotools.cocoeval import COCOeval
|
||||
coco_gt = dataset.coco
|
||||
coco_dt = coco_gt.loadRes(json_result_file)
|
||||
coco_eval = COCOeval(coco_gt, coco_dt, iou_type)
|
||||
coco_eval.evaluate()
|
||||
coco_eval.accumulate()
|
||||
coco_eval.summarize()
|
||||
|
||||
result_strings = []
|
||||
keys = ["AP", "AP50", "AP75", "APs", "APm", "APl"]
|
||||
metrics = {}
|
||||
for i, key in enumerate(keys):
|
||||
metrics[key] = coco_eval.stats[i]
|
||||
logger.info('{:<10}: {}'.format(key, round(coco_eval.stats[i], 3)))
|
||||
result_strings.append('{:<10}: {}'.format(key, round(coco_eval.stats[i], 3)))
|
||||
|
||||
if iteration is not None:
|
||||
result_path = os.path.join(output_dir, 'result_{:07d}.txt'.format(iteration))
|
||||
else:
|
||||
result_path = os.path.join(output_dir, 'result_{}.txt'.format(datetime.now().strftime('%Y-%m-%d_%H-%M-%S')))
|
||||
with open(result_path, "w") as f:
|
||||
f.write('\n'.join(result_strings))
|
||||
|
||||
return dict(metrics=metrics)
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .eval_detection_voc import eval_detection_voc
|
||||
|
||||
|
||||
def voc_evaluation(dataset, predictions, output_dir, iteration=None):
|
||||
class_names = dataset.class_names
|
||||
|
||||
pred_boxes_list = []
|
||||
pred_labels_list = []
|
||||
pred_scores_list = []
|
||||
gt_boxes_list = []
|
||||
gt_labels_list = []
|
||||
gt_difficults = []
|
||||
|
||||
for i in range(len(dataset)):
|
||||
image_id, annotation = dataset.get_annotation(i)
|
||||
gt_boxes, gt_labels, is_difficult = annotation
|
||||
gt_boxes_list.append(gt_boxes)
|
||||
gt_labels_list.append(gt_labels)
|
||||
gt_difficults.append(is_difficult.astype(np.bool))
|
||||
|
||||
img_info = dataset.get_img_info(i)
|
||||
prediction = predictions[i]
|
||||
prediction = prediction.resize((img_info['width'], img_info['height'])).numpy()
|
||||
boxes, labels, scores = prediction['boxes'], prediction['labels'], prediction['scores']
|
||||
|
||||
pred_boxes_list.append(boxes)
|
||||
pred_labels_list.append(labels)
|
||||
pred_scores_list.append(scores)
|
||||
result = eval_detection_voc(pred_bboxes=pred_boxes_list,
|
||||
pred_labels=pred_labels_list,
|
||||
pred_scores=pred_scores_list,
|
||||
gt_bboxes=gt_boxes_list,
|
||||
gt_labels=gt_labels_list,
|
||||
gt_difficults=gt_difficults,
|
||||
iou_thresh=0.5,
|
||||
use_07_metric=True)
|
||||
logger = logging.getLogger("SSD.inference")
|
||||
result_str = "mAP: {:.4f}\n".format(result["map"])
|
||||
metrics = {'mAP': result["map"]}
|
||||
for i, ap in enumerate(result["ap"]):
|
||||
if i == 0: # skip background
|
||||
continue
|
||||
metrics[class_names[i]] = ap
|
||||
result_str += "{:<16}: {:.4f}\n".format(class_names[i], ap)
|
||||
logger.info(result_str)
|
||||
|
||||
if iteration is not None:
|
||||
result_path = os.path.join(output_dir, 'result_{:07d}.txt'.format(iteration))
|
||||
else:
|
||||
result_path = os.path.join(output_dir, 'result_{}.txt'.format(datetime.now().strftime('%Y-%m-%d_%H-%M-%S')))
|
||||
with open(result_path, "w") as f:
|
||||
f.write(result_str)
|
||||
|
||||
return dict(metrics=metrics)
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
from __future__ import division
|
||||
|
||||
from collections import defaultdict
|
||||
import itertools
|
||||
import numpy as np
|
||||
import six
|
||||
|
||||
|
||||
def bbox_iou(bbox_a, bbox_b):
|
||||
"""Calculate the Intersection of Unions (IoUs) between bounding boxes.
|
||||
IoU is calculated as a ratio of area of the intersection
|
||||
and area of the union.
|
||||
This function accepts both :obj:`numpy.ndarray` and :obj:`cupy.ndarray` as
|
||||
inputs. Please note that both :obj:`bbox_a` and :obj:`bbox_b` need to be
|
||||
same type.
|
||||
The output is same type as the type of the inputs.
|
||||
Args:
|
||||
bbox_a (array): An array whose shape is :math:`(N, 4)`.
|
||||
:math:`N` is the number of bounding boxes.
|
||||
The dtype should be :obj:`numpy.float32`.
|
||||
bbox_b (array): An array similar to :obj:`bbox_a`,
|
||||
whose shape is :math:`(K, 4)`.
|
||||
The dtype should be :obj:`numpy.float32`.
|
||||
Returns:
|
||||
array:
|
||||
An array whose shape is :math:`(N, K)`. \
|
||||
An element at index :math:`(n, k)` contains IoUs between \
|
||||
:math:`n` th bounding box in :obj:`bbox_a` and :math:`k` th bounding \
|
||||
box in :obj:`bbox_b`.
|
||||
"""
|
||||
if bbox_a.shape[1] != 4 or bbox_b.shape[1] != 4:
|
||||
raise IndexError
|
||||
|
||||
# top left
|
||||
tl = np.maximum(bbox_a[:, None, :2], bbox_b[:, :2])
|
||||
# bottom right
|
||||
br = np.minimum(bbox_a[:, None, 2:], bbox_b[:, 2:])
|
||||
|
||||
area_i = np.prod(br - tl, axis=2) * (tl < br).all(axis=2)
|
||||
area_a = np.prod(bbox_a[:, 2:] - bbox_a[:, :2], axis=1)
|
||||
area_b = np.prod(bbox_b[:, 2:] - bbox_b[:, :2], axis=1)
|
||||
return area_i / (area_a[:, None] + area_b - area_i)
|
||||
|
||||
|
||||
def eval_detection_voc(
|
||||
pred_bboxes,
|
||||
pred_labels,
|
||||
pred_scores,
|
||||
gt_bboxes,
|
||||
gt_labels,
|
||||
gt_difficults=None,
|
||||
iou_thresh=0.5,
|
||||
use_07_metric=False):
|
||||
"""Calculate average precisions based on evaluation code of PASCAL VOC.
|
||||
|
||||
This function evaluates predicted bounding boxes obtained from a dataset
|
||||
which has :math:`N` images by using average precision for each class.
|
||||
The code is based on the evaluation code used in PASCAL VOC Challenge.
|
||||
|
||||
Args:
|
||||
pred_bboxes (iterable of numpy.ndarray): An iterable of :math:`N`
|
||||
sets of bounding boxes.
|
||||
Its index corresponds to an index for the base dataset.
|
||||
Each element of :obj:`pred_bboxes` is a set of coordinates
|
||||
of bounding boxes. This is an array whose shape is :math:`(R, 4)`,
|
||||
where :math:`R` corresponds
|
||||
to the number of bounding boxes, which may vary among boxes.
|
||||
The second axis corresponds to
|
||||
:math:`y_{min}, x_{min}, y_{max}, x_{max}` of a bounding box.
|
||||
pred_labels (iterable of numpy.ndarray): An iterable of labels.
|
||||
Similar to :obj:`pred_bboxes`, its index corresponds to an
|
||||
index for the base dataset. Its length is :math:`N`.
|
||||
pred_scores (iterable of numpy.ndarray): An iterable of confidence
|
||||
scores for predicted bounding boxes. Similar to :obj:`pred_bboxes`,
|
||||
its index corresponds to an index for the base dataset.
|
||||
Its length is :math:`N`.
|
||||
gt_bboxes (iterable of numpy.ndarray): An iterable of ground truth
|
||||
bounding boxes
|
||||
whose length is :math:`N`. An element of :obj:`gt_bboxes` is a
|
||||
bounding box whose shape is :math:`(R, 4)`. Note that the number of
|
||||
bounding boxes in each image does not need to be same as the number
|
||||
of corresponding predicted boxes.
|
||||
gt_labels (iterable of numpy.ndarray): An iterable of ground truth
|
||||
labels which are organized similarly to :obj:`gt_bboxes`.
|
||||
gt_difficults (iterable of numpy.ndarray): An iterable of boolean
|
||||
arrays which is organized similarly to :obj:`gt_bboxes`.
|
||||
This tells whether the
|
||||
corresponding ground truth bounding box is difficult or not.
|
||||
By default, this is :obj:`None`. In that case, this function
|
||||
considers all bounding boxes to be not difficult.
|
||||
iou_thresh (float): A prediction is correct if its Intersection over
|
||||
Union with the ground truth is above this value.
|
||||
use_07_metric (bool): Whether to use PASCAL VOC 2007 evaluation metric
|
||||
for calculating average precision. The default value is
|
||||
:obj:`False`.
|
||||
|
||||
Returns:
|
||||
dict:
|
||||
|
||||
The keys, value-types and the description of the values are listed
|
||||
below.
|
||||
|
||||
* **ap** (*numpy.ndarray*): An array of average precisions. \
|
||||
The :math:`l`-th value corresponds to the average precision \
|
||||
for class :math:`l`. If class :math:`l` does not exist in \
|
||||
either :obj:`pred_labels` or :obj:`gt_labels`, the corresponding \
|
||||
value is set to :obj:`numpy.nan`.
|
||||
* **map** (*float*): The average of Average Precisions over classes.
|
||||
|
||||
"""
|
||||
|
||||
prec, rec = calc_detection_voc_prec_rec(pred_bboxes,
|
||||
pred_labels,
|
||||
pred_scores,
|
||||
gt_bboxes,
|
||||
gt_labels,
|
||||
gt_difficults,
|
||||
iou_thresh=iou_thresh)
|
||||
|
||||
ap = calc_detection_voc_ap(prec, rec, use_07_metric=use_07_metric)
|
||||
|
||||
return {'ap': ap, 'map': np.nanmean(ap)}
|
||||
|
||||
|
||||
def calc_detection_voc_prec_rec(
|
||||
pred_bboxes, pred_labels, pred_scores, gt_bboxes, gt_labels,
|
||||
gt_difficults=None,
|
||||
iou_thresh=0.5):
|
||||
"""Calculate precision and recall based on evaluation code of PASCAL VOC.
|
||||
|
||||
This function calculates precision and recall of
|
||||
predicted bounding boxes obtained from a dataset which has :math:`N`
|
||||
images.
|
||||
The code is based on the evaluation code used in PASCAL VOC Challenge.
|
||||
|
||||
Args:
|
||||
pred_bboxes (iterable of numpy.ndarray): An iterable of :math:`N`
|
||||
sets of bounding boxes.
|
||||
Its index corresponds to an index for the base dataset.
|
||||
Each element of :obj:`pred_bboxes` is a set of coordinates
|
||||
of bounding boxes. This is an array whose shape is :math:`(R, 4)`,
|
||||
where :math:`R` corresponds
|
||||
to the number of bounding boxes, which may vary among boxes.
|
||||
The second axis corresponds to
|
||||
:math:`y_{min}, x_{min}, y_{max}, x_{max}` of a bounding box.
|
||||
pred_labels (iterable of numpy.ndarray): An iterable of labels.
|
||||
Similar to :obj:`pred_bboxes`, its index corresponds to an
|
||||
index for the base dataset. Its length is :math:`N`.
|
||||
pred_scores (iterable of numpy.ndarray): An iterable of confidence
|
||||
scores for predicted bounding boxes. Similar to :obj:`pred_bboxes`,
|
||||
its index corresponds to an index for the base dataset.
|
||||
Its length is :math:`N`.
|
||||
gt_bboxes (iterable of numpy.ndarray): An iterable of ground truth
|
||||
bounding boxes
|
||||
whose length is :math:`N`. An element of :obj:`gt_bboxes` is a
|
||||
bounding box whose shape is :math:`(R, 4)`. Note that the number of
|
||||
bounding boxes in each image does not need to be same as the number
|
||||
of corresponding predicted boxes.
|
||||
gt_labels (iterable of numpy.ndarray): An iterable of ground truth
|
||||
labels which are organized similarly to :obj:`gt_bboxes`.
|
||||
gt_difficults (iterable of numpy.ndarray): An iterable of boolean
|
||||
arrays which is organized similarly to :obj:`gt_bboxes`.
|
||||
This tells whether the
|
||||
corresponding ground truth bounding box is difficult or not.
|
||||
By default, this is :obj:`None`. In that case, this function
|
||||
considers all bounding boxes to be not difficult.
|
||||
iou_thresh (float): A prediction is correct if its Intersection over
|
||||
Union with the ground truth is above this value..
|
||||
|
||||
Returns:
|
||||
tuple of two lists:
|
||||
This function returns two lists: :obj:`prec` and :obj:`rec`.
|
||||
|
||||
* :obj:`prec`: A list of arrays. :obj:`prec[l]` is precision \
|
||||
for class :math:`l`. If class :math:`l` does not exist in \
|
||||
either :obj:`pred_labels` or :obj:`gt_labels`, :obj:`prec[l]` is \
|
||||
set to :obj:`None`.
|
||||
* :obj:`rec`: A list of arrays. :obj:`rec[l]` is recall \
|
||||
for class :math:`l`. If class :math:`l` that is not marked as \
|
||||
difficult does not exist in \
|
||||
:obj:`gt_labels`, :obj:`rec[l]` is \
|
||||
set to :obj:`None`.
|
||||
|
||||
"""
|
||||
|
||||
pred_bboxes = iter(pred_bboxes)
|
||||
pred_labels = iter(pred_labels)
|
||||
pred_scores = iter(pred_scores)
|
||||
gt_bboxes = iter(gt_bboxes)
|
||||
gt_labels = iter(gt_labels)
|
||||
if gt_difficults is None:
|
||||
gt_difficults = itertools.repeat(None)
|
||||
else:
|
||||
gt_difficults = iter(gt_difficults)
|
||||
|
||||
n_pos = defaultdict(int)
|
||||
score = defaultdict(list)
|
||||
match = defaultdict(list)
|
||||
|
||||
for pred_bbox, pred_label, pred_score, gt_bbox, gt_label, gt_difficult in \
|
||||
six.moves.zip(
|
||||
pred_bboxes, pred_labels, pred_scores,
|
||||
gt_bboxes, gt_labels, gt_difficults):
|
||||
|
||||
if gt_difficult is None:
|
||||
gt_difficult = np.zeros(gt_bbox.shape[0], dtype=bool)
|
||||
|
||||
for l in np.unique(np.concatenate((pred_label, gt_label)).astype(int)):
|
||||
pred_mask_l = pred_label == l
|
||||
pred_bbox_l = pred_bbox[pred_mask_l]
|
||||
pred_score_l = pred_score[pred_mask_l]
|
||||
# sort by score
|
||||
order = pred_score_l.argsort()[::-1]
|
||||
pred_bbox_l = pred_bbox_l[order]
|
||||
pred_score_l = pred_score_l[order]
|
||||
|
||||
gt_mask_l = gt_label == l
|
||||
gt_bbox_l = gt_bbox[gt_mask_l]
|
||||
gt_difficult_l = gt_difficult[gt_mask_l]
|
||||
|
||||
n_pos[l] += np.logical_not(gt_difficult_l).sum()
|
||||
score[l].extend(pred_score_l)
|
||||
|
||||
if len(pred_bbox_l) == 0:
|
||||
continue
|
||||
if len(gt_bbox_l) == 0:
|
||||
match[l].extend((0,) * pred_bbox_l.shape[0])
|
||||
continue
|
||||
|
||||
# VOC evaluation follows integer typed bounding boxes.
|
||||
pred_bbox_l = pred_bbox_l.copy()
|
||||
pred_bbox_l[:, 2:] += 1
|
||||
gt_bbox_l = gt_bbox_l.copy()
|
||||
gt_bbox_l[:, 2:] += 1
|
||||
|
||||
iou = bbox_iou(pred_bbox_l, gt_bbox_l)
|
||||
gt_index = iou.argmax(axis=1)
|
||||
# set -1 if there is no matching ground truth
|
||||
gt_index[iou.max(axis=1) < iou_thresh] = -1
|
||||
del iou
|
||||
|
||||
selec = np.zeros(gt_bbox_l.shape[0], dtype=bool)
|
||||
for gt_idx in gt_index:
|
||||
if gt_idx >= 0:
|
||||
if gt_difficult_l[gt_idx]:
|
||||
match[l].append(-1)
|
||||
else:
|
||||
if not selec[gt_idx]:
|
||||
match[l].append(1)
|
||||
else:
|
||||
match[l].append(0)
|
||||
selec[gt_idx] = True
|
||||
else:
|
||||
match[l].append(0)
|
||||
|
||||
for iter_ in (
|
||||
pred_bboxes, pred_labels, pred_scores,
|
||||
gt_bboxes, gt_labels, gt_difficults):
|
||||
if next(iter_, None) is not None:
|
||||
raise ValueError('Length of input iterables need to be same.')
|
||||
|
||||
n_fg_class = max(n_pos.keys()) + 1
|
||||
prec = [None] * n_fg_class
|
||||
rec = [None] * n_fg_class
|
||||
|
||||
for l in n_pos.keys():
|
||||
score_l = np.array(score[l])
|
||||
match_l = np.array(match[l], dtype=np.int8)
|
||||
|
||||
order = score_l.argsort()[::-1]
|
||||
match_l = match_l[order]
|
||||
|
||||
tp = np.cumsum(match_l == 1)
|
||||
fp = np.cumsum(match_l == 0)
|
||||
|
||||
# If an element of fp + tp is 0,
|
||||
# the corresponding element of prec[l] is nan.
|
||||
prec[l] = tp / (fp + tp)
|
||||
# If n_pos[l] is 0, rec[l] is None.
|
||||
if n_pos[l] > 0:
|
||||
rec[l] = tp / n_pos[l]
|
||||
|
||||
return prec, rec
|
||||
|
||||
|
||||
def calc_detection_voc_ap(prec, rec, use_07_metric=False):
|
||||
"""Calculate average precisions based on evaluation code of PASCAL VOC.
|
||||
|
||||
This function calculates average precisions
|
||||
from given precisions and recalls.
|
||||
The code is based on the evaluation code used in PASCAL VOC Challenge.
|
||||
|
||||
Args:
|
||||
prec (list of numpy.array): A list of arrays.
|
||||
:obj:`prec[l]` indicates precision for class :math:`l`.
|
||||
If :obj:`prec[l]` is :obj:`None`, this function returns
|
||||
:obj:`numpy.nan` for class :math:`l`.
|
||||
rec (list of numpy.array): A list of arrays.
|
||||
:obj:`rec[l]` indicates recall for class :math:`l`.
|
||||
If :obj:`rec[l]` is :obj:`None`, this function returns
|
||||
:obj:`numpy.nan` for class :math:`l`.
|
||||
use_07_metric (bool): Whether to use PASCAL VOC 2007 evaluation metric
|
||||
for calculating average precision. The default value is
|
||||
:obj:`False`.
|
||||
|
||||
Returns:
|
||||
~numpy.ndarray:
|
||||
This function returns an array of average precisions.
|
||||
The :math:`l`-th value corresponds to the average precision
|
||||
for class :math:`l`. If :obj:`prec[l]` or :obj:`rec[l]` is
|
||||
:obj:`None`, the corresponding value is set to :obj:`numpy.nan`.
|
||||
|
||||
"""
|
||||
|
||||
n_fg_class = len(prec)
|
||||
ap = np.empty(n_fg_class)
|
||||
for l in six.moves.range(n_fg_class):
|
||||
if prec[l] is None or rec[l] is None:
|
||||
ap[l] = np.nan
|
||||
continue
|
||||
|
||||
if use_07_metric:
|
||||
# 11 point metric
|
||||
ap[l] = 0
|
||||
for t in np.arange(0., 1.1, 0.1):
|
||||
if np.sum(rec[l] >= t) == 0:
|
||||
p = 0
|
||||
else:
|
||||
p = np.max(np.nan_to_num(prec[l])[rec[l] >= t])
|
||||
ap[l] += p / 11
|
||||
else:
|
||||
# correct AP calculation
|
||||
# first append sentinel values at the end
|
||||
mpre = np.concatenate(([0], np.nan_to_num(prec[l]), [0]))
|
||||
mrec = np.concatenate(([0], rec[l], [1]))
|
||||
|
||||
mpre = np.maximum.accumulate(mpre[::-1])[::-1]
|
||||
|
||||
# to calculate area under PR curve, look for points
|
||||
# where X axis (recall) changes value
|
||||
i = np.where(mrec[1:] != mrec[:-1])[0]
|
||||
|
||||
# and sum (\Delta recall) * prec
|
||||
ap[l] = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
|
||||
|
||||
return ap
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
import os
|
||||
import torch.utils.data
|
||||
import numpy as np
|
||||
import xml.etree.ElementTree as ET
|
||||
from PIL import Image
|
||||
|
||||
from ssd.structures.container import Container
|
||||
|
||||
root_dir = "/data1/Hatcher/model/data_MPKFNet/data/VOC/VOCdevkit"
|
||||
class VOCDataset(torch.utils.data.Dataset):
|
||||
|
||||
class_names = ('__background__',
|
||||
'aeroplane', 'bicycle', 'bird', 'boat',
|
||||
'bottle', 'bus', 'car', 'cat', 'chair',
|
||||
'cow', 'diningtable', 'dog', 'horse',
|
||||
'motorbike', 'person', 'pottedplant',
|
||||
'sheep', 'sofa', 'train', 'tvmonitor')
|
||||
|
||||
def __init__(self, data_dir, split, transform=None, target_transform=None, keep_difficult=False):
|
||||
"""Dataset for VOC data.
|
||||
Args:
|
||||
data_dir: the root of the VOC2007 or VOC2012 dataset, the directory contains the following sub-directories:
|
||||
Annotations, ImageSets, JPEGImages, SegmentationClass, SegmentationObject.
|
||||
"""
|
||||
self.data_dir = data_dir
|
||||
self.split = split
|
||||
self.transform = transform
|
||||
self.target_transform = target_transform
|
||||
image_sets_file = os.path.join(root_dir, self.data_dir, "ImageSets", "Main", "%s.txt" % self.split)
|
||||
self.ids = VOCDataset._read_image_ids(image_sets_file)
|
||||
self.keep_difficult = keep_difficult
|
||||
|
||||
self.class_dict = {class_name: i for i, class_name in enumerate(self.class_names)}
|
||||
|
||||
def __getitem__(self, index):
|
||||
image_id = self.ids[index]
|
||||
boxes, labels, is_difficult = self._get_annotation(image_id)
|
||||
if not self.keep_difficult:
|
||||
boxes = boxes[is_difficult == 0]
|
||||
labels = labels[is_difficult == 0]
|
||||
image = self._read_image(image_id)
|
||||
if self.transform:
|
||||
# print(image_id,"is dealing")
|
||||
# print(boxes)
|
||||
image, boxes, labels = self.transform(image, boxes, labels)
|
||||
if self.target_transform:
|
||||
boxes, labels = self.target_transform(boxes, labels)
|
||||
targets = Container(
|
||||
boxes=boxes,
|
||||
labels=labels,
|
||||
)
|
||||
return image, targets, index
|
||||
# def __getitem__(self, index):
|
||||
# image_id = self.ids[index]
|
||||
# #boxes, labels, is_difficult = self._get_annotation(image_id)
|
||||
# # if not self.keep_difficult:
|
||||
# # boxes = boxes[is_difficult == 0]
|
||||
# # labels = labels[is_difficult == 0]
|
||||
# image = self._read_image(image_id)
|
||||
# # if self.transform:
|
||||
# # image, boxes, labels = self.transform(image, boxes, labels)
|
||||
# if self.transform:
|
||||
# image, boxes, labels = self.transform(image)
|
||||
# # if self.target_transform:
|
||||
# # boxes, labels = self.target_transform(boxes, labels)
|
||||
# targets = Container(
|
||||
# boxes=boxes,
|
||||
# labels=labels,
|
||||
# )
|
||||
# return image, targets, index
|
||||
def get_annotation(self, index):
|
||||
image_id = self.ids[index]
|
||||
return image_id, self._get_annotation(image_id)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.ids)
|
||||
|
||||
@staticmethod
|
||||
def _read_image_ids(image_sets_file):
|
||||
ids = []
|
||||
with open(image_sets_file) as f:
|
||||
for line in f:
|
||||
ids.append(line.rstrip())
|
||||
return ids
|
||||
|
||||
def _get_annotation(self, image_id):
|
||||
annotation_file = os.path.join(root_dir, self.data_dir, "Annotations", "%s.xml" % image_id)
|
||||
objects = ET.parse(annotation_file).findall("object")
|
||||
boxes = []
|
||||
labels = []
|
||||
is_difficult = []
|
||||
for obj in objects:
|
||||
class_name = obj.find('name').text.lower().strip()
|
||||
bbox = obj.find('bndbox')
|
||||
# VOC dataset format follows Matlab, in which indexes start from 0
|
||||
x1 = float(bbox.find('xmin').text) - 1
|
||||
y1 = float(bbox.find('ymin').text) - 1
|
||||
x2 = float(bbox.find('xmax').text) - 1
|
||||
y2 = float(bbox.find('ymax').text) - 1
|
||||
boxes.append([x1, y1, x2, y2])
|
||||
labels.append(self.class_dict['bird'])
|
||||
# labels.append(self.class_dict[class_name])
|
||||
#labels.append('bird')
|
||||
#is_difficult_str = obj.find('difficult').text
|
||||
#is_difficult.append(int(is_difficult_str) if is_difficult_str else 0)
|
||||
is_difficult.append(0)
|
||||
return (np.array(boxes, dtype=np.float32),
|
||||
np.array(labels, dtype=np.int64),
|
||||
np.array(is_difficult, dtype=np.uint8))
|
||||
|
||||
def get_img_info(self, index):
|
||||
img_id = self.ids[index]
|
||||
annotation_file = os.path.join(root_dir, self.data_dir, "Annotations", "%s.xml" % img_id)
|
||||
anno = ET.parse(annotation_file).getroot()
|
||||
size = anno.find("size")
|
||||
im_info = tuple(map(int, (size.find("height").text, size.find("width").text)))
|
||||
return {"height": im_info[0], "width": im_info[1]}
|
||||
|
||||
def _read_image(self, image_id):
|
||||
image_file = os.path.join(root_dir, self.data_dir, "JPEGImages", "%s.jpg" % image_id)
|
||||
image = Image.open(image_file).convert("RGB")
|
||||
image = np.array(image)
|
||||
return image
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
from .iteration_based_batch_sampler import IterationBasedBatchSampler
|
||||
from .distributed import DistributedSampler
|
||||
|
||||
__all__ = ['IterationBasedBatchSampler', 'DistributedSampler']
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
|
||||
# Code is copy-pasted exactly as in torch.utils.data.distributed.
|
||||
# FIXME remove this once c10d fixes the bug it has
|
||||
import math
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from torch.utils.data.sampler import Sampler
|
||||
|
||||
|
||||
class DistributedSampler(Sampler):
|
||||
"""Sampler that restricts data loading to a subset of the dataset.
|
||||
It is especially useful in conjunction with
|
||||
:class:`torch.nn.parallel.DistributedDataParallel`. In such case, each
|
||||
process can pass a DistributedSampler instance as a DataLoader sampler,
|
||||
and load a subset of the original dataset that is exclusive to it.
|
||||
.. note::
|
||||
Dataset is assumed to be of constant size.
|
||||
Arguments:
|
||||
dataset: Dataset used for sampling.
|
||||
num_replicas (optional): Number of processes participating in
|
||||
distributed training.
|
||||
rank (optional): Rank of the current process within num_replicas.
|
||||
"""
|
||||
|
||||
def __init__(self, dataset, num_replicas=None, rank=None, shuffle=True):
|
||||
if num_replicas is None:
|
||||
if not dist.is_available():
|
||||
raise RuntimeError("Requires distributed package to be available")
|
||||
num_replicas = dist.get_world_size()
|
||||
if rank is None:
|
||||
if not dist.is_available():
|
||||
raise RuntimeError("Requires distributed package to be available")
|
||||
rank = dist.get_rank()
|
||||
self.dataset = dataset
|
||||
self.num_replicas = num_replicas
|
||||
self.rank = rank
|
||||
self.epoch = 0
|
||||
self.num_samples = int(math.ceil(len(self.dataset) * 1.0 / self.num_replicas))
|
||||
self.total_size = self.num_samples * self.num_replicas
|
||||
self.shuffle = shuffle
|
||||
|
||||
def __iter__(self):
|
||||
if self.shuffle:
|
||||
# deterministically shuffle based on epoch
|
||||
g = torch.Generator()
|
||||
g.manual_seed(self.epoch)
|
||||
indices = torch.randperm(len(self.dataset), generator=g).tolist()
|
||||
else:
|
||||
indices = torch.arange(len(self.dataset)).tolist()
|
||||
|
||||
# add extra samples to make it evenly divisible
|
||||
indices += indices[: (self.total_size - len(indices))]
|
||||
assert len(indices) == self.total_size
|
||||
|
||||
# subsample
|
||||
offset = self.num_samples * self.rank
|
||||
indices = indices[offset: offset + self.num_samples]
|
||||
assert len(indices) == self.num_samples
|
||||
|
||||
return iter(indices)
|
||||
|
||||
def __len__(self):
|
||||
return self.num_samples
|
||||
|
||||
def set_epoch(self, epoch):
|
||||
self.epoch = epoch
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
from torch.utils.data.sampler import BatchSampler
|
||||
|
||||
|
||||
class IterationBasedBatchSampler(BatchSampler):
|
||||
"""
|
||||
Wraps a BatchSampler, re-sampling from it until
|
||||
a specified number of iterations have been sampled
|
||||
"""
|
||||
|
||||
def __init__(self, batch_sampler, num_iterations, start_iter=0):
|
||||
self.batch_sampler = batch_sampler
|
||||
self.num_iterations = num_iterations
|
||||
self.start_iter = start_iter
|
||||
|
||||
def __iter__(self):
|
||||
iteration = self.start_iter
|
||||
while iteration <= self.num_iterations:
|
||||
# if the underlying sampler has a set_epoch method, like
|
||||
# DistributedSampler, used for making each process see
|
||||
# a different split of the dataset, then set it
|
||||
if hasattr(self.batch_sampler.sampler, "set_epoch"):
|
||||
self.batch_sampler.sampler.set_epoch(iteration)
|
||||
for batch in self.batch_sampler:
|
||||
iteration += 1
|
||||
if iteration > self.num_iterations:
|
||||
break
|
||||
yield batch
|
||||
|
||||
def __len__(self):
|
||||
return self.num_iterations
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
from ssd.modeling.anchors.prior_box import PriorBox
|
||||
from .target_transform import SSDTargetTransform
|
||||
from .transforms import *
|
||||
|
||||
|
||||
def build_transforms(cfg, is_train=True):
|
||||
if is_train:
|
||||
transform = [
|
||||
ConvertFromInts(),
|
||||
PhotometricDistort(),
|
||||
Expand(cfg.INPUT.PIXEL_MEAN),
|
||||
RandomSampleCrop(),
|
||||
RandomMirror(),
|
||||
ToPercentCoords(),
|
||||
Resize(cfg.INPUT.IMAGE_SIZE),
|
||||
SubtractMeans(cfg.INPUT.PIXEL_MEAN),
|
||||
ToTensor(),
|
||||
]
|
||||
else:
|
||||
transform = [
|
||||
Resize(cfg.INPUT.IMAGE_SIZE),
|
||||
SubtractMeans(cfg.INPUT.PIXEL_MEAN),
|
||||
ToTensor()
|
||||
]
|
||||
transform = Compose(transform)
|
||||
return transform
|
||||
|
||||
|
||||
def build_target_transform(cfg):
|
||||
transform = SSDTargetTransform(PriorBox(cfg)(),
|
||||
cfg.MODEL.CENTER_VARIANCE,
|
||||
cfg.MODEL.SIZE_VARIANCE,
|
||||
cfg.MODEL.THRESHOLD)
|
||||
return transform
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from ssd.utils import box_utils
|
||||
|
||||
|
||||
class SSDTargetTransform:
|
||||
def __init__(self, center_form_priors, center_variance, size_variance, iou_threshold):
|
||||
self.center_form_priors = center_form_priors
|
||||
self.corner_form_priors = box_utils.center_form_to_corner_form(center_form_priors)
|
||||
self.center_variance = center_variance
|
||||
self.size_variance = size_variance
|
||||
self.iou_threshold = iou_threshold
|
||||
|
||||
def __call__(self, gt_boxes, gt_labels):
|
||||
if type(gt_boxes) is np.ndarray:
|
||||
gt_boxes = torch.from_numpy(gt_boxes)
|
||||
if type(gt_labels) is np.ndarray:
|
||||
gt_labels = torch.from_numpy(gt_labels)
|
||||
boxes, labels = box_utils.assign_priors(gt_boxes, gt_labels,
|
||||
self.corner_form_priors, self.iou_threshold)
|
||||
boxes = box_utils.corner_form_to_center_form(boxes)
|
||||
locations = box_utils.convert_boxes_to_locations(boxes, self.center_form_priors, self.center_variance, self.size_variance)
|
||||
|
||||
return locations, labels
|
||||
Executable
+410
@@ -0,0 +1,410 @@
|
||||
# from https://github.com/amdegroot/ssd.pytorch
|
||||
|
||||
|
||||
import torch
|
||||
from torchvision import transforms
|
||||
import cv2
|
||||
import numpy as np
|
||||
import types
|
||||
from numpy import random
|
||||
|
||||
|
||||
def intersect(box_a, box_b):
|
||||
max_xy = np.minimum(box_a[:, 2:], box_b[2:])
|
||||
min_xy = np.maximum(box_a[:, :2], box_b[:2])
|
||||
inter = np.clip((max_xy - min_xy), a_min=0, a_max=np.inf)
|
||||
return inter[:, 0] * inter[:, 1]
|
||||
|
||||
|
||||
def jaccard_numpy(box_a, box_b):
|
||||
"""Compute the jaccard overlap of two sets of boxes. The jaccard overlap
|
||||
is simply the intersection over union of two boxes.
|
||||
E.g.:
|
||||
A ∩ B / A ∪ B = A ∩ B / (area(A) + area(B) - A ∩ B)
|
||||
Args:
|
||||
box_a: Multiple bounding boxes, Shape: [num_boxes,4]
|
||||
box_b: Single bounding box, Shape: [4]
|
||||
Return:
|
||||
jaccard overlap: Shape: [box_a.shape[0], box_a.shape[1]]
|
||||
"""
|
||||
inter = intersect(box_a, box_b)
|
||||
area_a = ((box_a[:, 2] - box_a[:, 0]) *
|
||||
(box_a[:, 3] - box_a[:, 1])) # [A,B]
|
||||
area_b = ((box_b[2] - box_b[0]) *
|
||||
(box_b[3] - box_b[1])) # [A,B]
|
||||
union = area_a + area_b - inter
|
||||
return inter / union # [A,B]
|
||||
|
||||
|
||||
class Compose(object):
|
||||
"""Composes several augmentations together.
|
||||
Args:
|
||||
transforms (List[Transform]): list of transforms to compose.
|
||||
Example:
|
||||
>>> augmentations.Compose([
|
||||
>>> transforms.CenterCrop(10),
|
||||
>>> transforms.ToTensor(),
|
||||
>>> ])
|
||||
"""
|
||||
|
||||
def __init__(self, transforms):
|
||||
self.transforms = transforms
|
||||
|
||||
def __call__(self, img, boxes=None, labels=None):
|
||||
for t in self.transforms:
|
||||
img, boxes, labels = t(img, boxes, labels)
|
||||
return img, boxes, labels
|
||||
|
||||
|
||||
class Lambda(object):
|
||||
"""Applies a lambda as a transform."""
|
||||
|
||||
def __init__(self, lambd):
|
||||
assert isinstance(lambd, types.LambdaType)
|
||||
self.lambd = lambd
|
||||
|
||||
def __call__(self, img, boxes=None, labels=None):
|
||||
return self.lambd(img, boxes, labels)
|
||||
|
||||
|
||||
class ConvertFromInts(object):
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
return image.astype(np.float32), boxes, labels
|
||||
|
||||
|
||||
class SubtractMeans(object):
|
||||
def __init__(self, mean):
|
||||
self.mean = np.array(mean, dtype=np.float32)
|
||||
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
image = image.astype(np.float32)
|
||||
image -= self.mean
|
||||
return image.astype(np.float32), boxes, labels
|
||||
|
||||
|
||||
class ToAbsoluteCoords(object):
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
height, width, channels = image.shape
|
||||
boxes[:, 0] *= width
|
||||
boxes[:, 2] *= width
|
||||
boxes[:, 1] *= height
|
||||
boxes[:, 3] *= height
|
||||
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class ToPercentCoords(object):
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
height, width, channels = image.shape
|
||||
boxes[:, 0] /= width
|
||||
boxes[:, 2] /= width
|
||||
boxes[:, 1] /= height
|
||||
boxes[:, 3] /= height
|
||||
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class Resize(object):
|
||||
def __init__(self, size=300):
|
||||
self.size = size
|
||||
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
image = cv2.resize(image, (self.size,
|
||||
self.size))
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class RandomSaturation(object):
|
||||
def __init__(self, lower=0.5, upper=1.5):
|
||||
self.lower = lower
|
||||
self.upper = upper
|
||||
assert self.upper >= self.lower, "contrast upper must be >= lower."
|
||||
assert self.lower >= 0, "contrast lower must be non-negative."
|
||||
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
if random.randint(2):
|
||||
image[:, :, 1] *= random.uniform(self.lower, self.upper)
|
||||
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class RandomHue(object):
|
||||
def __init__(self, delta=18.0):
|
||||
assert delta >= 0.0 and delta <= 360.0
|
||||
self.delta = delta
|
||||
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
if random.randint(2):
|
||||
image[:, :, 0] += random.uniform(-self.delta, self.delta)
|
||||
image[:, :, 0][image[:, :, 0] > 360.0] -= 360.0
|
||||
image[:, :, 0][image[:, :, 0] < 0.0] += 360.0
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class RandomLightingNoise(object):
|
||||
def __init__(self):
|
||||
self.perms = ((0, 1, 2), (0, 2, 1),
|
||||
(1, 0, 2), (1, 2, 0),
|
||||
(2, 0, 1), (2, 1, 0))
|
||||
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
if random.randint(2):
|
||||
swap = self.perms[random.randint(len(self.perms))]
|
||||
shuffle = SwapChannels(swap) # shuffle channels
|
||||
image = shuffle(image)
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class ConvertColor(object):
|
||||
def __init__(self, current, transform):
|
||||
self.transform = transform
|
||||
self.current = current
|
||||
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
if self.current == 'BGR' and self.transform == 'HSV':
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
|
||||
elif self.current == 'RGB' and self.transform == 'HSV':
|
||||
image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
|
||||
elif self.current == 'BGR' and self.transform == 'RGB':
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
elif self.current == 'HSV' and self.transform == 'BGR':
|
||||
image = cv2.cvtColor(image, cv2.COLOR_HSV2BGR)
|
||||
elif self.current == 'HSV' and self.transform == "RGB":
|
||||
image = cv2.cvtColor(image, cv2.COLOR_HSV2RGB)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class RandomContrast(object):
|
||||
def __init__(self, lower=0.5, upper=1.5):
|
||||
self.lower = lower
|
||||
self.upper = upper
|
||||
assert self.upper >= self.lower, "contrast upper must be >= lower."
|
||||
assert self.lower >= 0, "contrast lower must be non-negative."
|
||||
|
||||
# expects float image
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
if random.randint(2):
|
||||
alpha = random.uniform(self.lower, self.upper)
|
||||
image *= alpha
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class RandomBrightness(object):
|
||||
def __init__(self, delta=32):
|
||||
assert delta >= 0.0
|
||||
assert delta <= 255.0
|
||||
self.delta = delta
|
||||
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
if random.randint(2):
|
||||
delta = random.uniform(-self.delta, self.delta)
|
||||
image += delta
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class ToCV2Image(object):
|
||||
def __call__(self, tensor, boxes=None, labels=None):
|
||||
return tensor.cpu().numpy().astype(np.float32).transpose((1, 2, 0)), boxes, labels
|
||||
|
||||
|
||||
class ToTensor(object):
|
||||
def __call__(self, cvimage, boxes=None, labels=None):
|
||||
return torch.from_numpy(cvimage.astype(np.float32)).permute(2, 0, 1), boxes, labels
|
||||
|
||||
|
||||
class RandomSampleCrop(object):
|
||||
"""Crop
|
||||
Arguments:
|
||||
img (Image): the image being input during training
|
||||
boxes (Tensor): the original bounding boxes in pt form
|
||||
labels (Tensor): the class labels for each bbox
|
||||
mode (float tuple): the min and max jaccard overlaps
|
||||
Return:
|
||||
(img, boxes, classes)
|
||||
img (Image): the cropped image
|
||||
boxes (Tensor): the adjusted bounding boxes in pt form
|
||||
labels (Tensor): the class labels for each bbox
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.sample_options = (
|
||||
# using entire original input image
|
||||
None,
|
||||
# sample a patch s.t. MIN jaccard w/ obj in .1,.3,.4,.7,.9
|
||||
(0.1, None),
|
||||
(0.3, None),
|
||||
(0.7, None),
|
||||
(0.9, None),
|
||||
# randomly sample a patch
|
||||
(None, None),
|
||||
)
|
||||
|
||||
def __call__(self, image, boxes=None, labels=None):
|
||||
# guard against no boxes
|
||||
if boxes is not None and boxes.shape[0] == 0:
|
||||
return image, boxes, labels
|
||||
height, width, _ = image.shape
|
||||
while True:
|
||||
# randomly choose a mode
|
||||
mode = random.choice(self.sample_options)
|
||||
if mode is None:
|
||||
return image, boxes, labels
|
||||
|
||||
min_iou, max_iou = mode
|
||||
if min_iou is None:
|
||||
min_iou = float('-inf')
|
||||
if max_iou is None:
|
||||
max_iou = float('inf')
|
||||
|
||||
# max trails (50)
|
||||
for _ in range(50):
|
||||
current_image = image
|
||||
|
||||
w = random.uniform(0.3 * width, width)
|
||||
h = random.uniform(0.3 * height, height)
|
||||
|
||||
# aspect ratio constraint b/t .5 & 2
|
||||
if h / w < 0.5 or h / w > 2:
|
||||
continue
|
||||
|
||||
left = random.uniform(width - w)
|
||||
top = random.uniform(height - h)
|
||||
|
||||
# convert to integer rect x1,y1,x2,y2
|
||||
rect = np.array([int(left), int(top), int(left + w), int(top + h)])
|
||||
|
||||
# calculate IoU (jaccard overlap) b/t the cropped and gt boxes
|
||||
overlap = jaccard_numpy(boxes, rect)
|
||||
|
||||
# is min and max overlap constraint satisfied? if not try again
|
||||
if overlap.max() < min_iou or overlap.min() > max_iou:
|
||||
continue
|
||||
|
||||
# cut the crop from the image
|
||||
current_image = current_image[rect[1]:rect[3], rect[0]:rect[2],
|
||||
:]
|
||||
|
||||
# keep overlap with gt box IF center in sampled patch
|
||||
centers = (boxes[:, :2] + boxes[:, 2:]) / 2.0
|
||||
|
||||
# mask in all gt boxes that above and to the left of centers
|
||||
m1 = (rect[0] < centers[:, 0]) * (rect[1] < centers[:, 1])
|
||||
|
||||
# mask in all gt boxes that under and to the right of centers
|
||||
m2 = (rect[2] > centers[:, 0]) * (rect[3] > centers[:, 1])
|
||||
|
||||
# mask in that both m1 and m2 are true
|
||||
mask = m1 * m2
|
||||
|
||||
# have any valid boxes? try again if not
|
||||
if not mask.any():
|
||||
continue
|
||||
|
||||
# take only matching gt boxes
|
||||
current_boxes = boxes[mask, :].copy()
|
||||
|
||||
# take only matching gt labels
|
||||
current_labels = labels[mask]
|
||||
|
||||
# should we use the box left and top corner or the crop's
|
||||
current_boxes[:, :2] = np.maximum(current_boxes[:, :2],
|
||||
rect[:2])
|
||||
# adjust to crop (by substracting crop's left,top)
|
||||
current_boxes[:, :2] -= rect[:2]
|
||||
|
||||
current_boxes[:, 2:] = np.minimum(current_boxes[:, 2:],
|
||||
rect[2:])
|
||||
# adjust to crop (by substracting crop's left,top)
|
||||
current_boxes[:, 2:] -= rect[:2]
|
||||
|
||||
return current_image, current_boxes, current_labels
|
||||
|
||||
|
||||
class Expand(object):
|
||||
def __init__(self, mean):
|
||||
self.mean = mean
|
||||
|
||||
def __call__(self, image, boxes, labels):
|
||||
if random.randint(2):
|
||||
return image, boxes, labels
|
||||
|
||||
height, width, depth = image.shape
|
||||
ratio = random.uniform(1, 4)
|
||||
left = random.uniform(0, width * ratio - width)
|
||||
top = random.uniform(0, height * ratio - height)
|
||||
|
||||
expand_image = np.zeros(
|
||||
(int(height * ratio), int(width * ratio), depth),
|
||||
dtype=image.dtype)
|
||||
expand_image[:, :, :] = self.mean
|
||||
expand_image[int(top):int(top + height),
|
||||
int(left):int(left + width)] = image
|
||||
image = expand_image
|
||||
|
||||
boxes = boxes.copy()
|
||||
boxes[:, :2] += (int(left), int(top))
|
||||
boxes[:, 2:] += (int(left), int(top))
|
||||
|
||||
return image, boxes, labels
|
||||
|
||||
|
||||
class RandomMirror(object):
|
||||
def __call__(self, image, boxes, classes):
|
||||
_, width, _ = image.shape
|
||||
if random.randint(2):
|
||||
image = image[:, ::-1]
|
||||
boxes = boxes.copy()
|
||||
boxes[:, 0::2] = width - boxes[:, 2::-2]
|
||||
return image, boxes, classes
|
||||
|
||||
|
||||
class SwapChannels(object):
|
||||
"""Transforms a tensorized image by swapping the channels in the order
|
||||
specified in the swap tuple.
|
||||
Args:
|
||||
swaps (int triple): final order of channels
|
||||
eg: (2, 1, 0)
|
||||
"""
|
||||
|
||||
def __init__(self, swaps):
|
||||
self.swaps = swaps
|
||||
|
||||
def __call__(self, image):
|
||||
"""
|
||||
Args:
|
||||
image (Tensor): image tensor to be transformed
|
||||
Return:
|
||||
a tensor with channels swapped according to swap
|
||||
"""
|
||||
# if torch.is_tensor(image):
|
||||
# image = image.data.cpu().numpy()
|
||||
# else:
|
||||
# image = np.array(image)
|
||||
image = image[:, :, self.swaps]
|
||||
return image
|
||||
|
||||
|
||||
class PhotometricDistort(object):
|
||||
def __init__(self):
|
||||
self.pd = [
|
||||
RandomContrast(), # RGB
|
||||
ConvertColor(current="RGB", transform='HSV'), # HSV
|
||||
RandomSaturation(), # HSV
|
||||
RandomHue(), # HSV
|
||||
ConvertColor(current='HSV', transform='RGB'), # RGB
|
||||
RandomContrast() # RGB
|
||||
]
|
||||
self.rand_brightness = RandomBrightness()
|
||||
self.rand_light_noise = RandomLightingNoise()
|
||||
|
||||
def __call__(self, image, boxes, labels):
|
||||
im = image.copy()
|
||||
im, boxes, labels = self.rand_brightness(im, boxes, labels)
|
||||
if random.randint(2):
|
||||
distort = Compose(self.pd[:-1])
|
||||
else:
|
||||
distort = Compose(self.pd[1:])
|
||||
im, boxes, labels = distort(im, boxes, labels)
|
||||
return self.rand_light_noise(im, boxes, labels)
|
||||
Reference in new issue
Block a user