# --------------------------------------------------------
# Fast R-CNN
# Copyright (c) 2015 Microsoft
# Licensed under The MIT License [see LICENSE for details]
# Written by Ross Girshick
# --------------------------------------------------------

"""Fast R-CNN config system.

This file specifies default config options for Fast R-CNN. You should not
change values in this file. Instead, you should write a config file (in yaml)
and use cfg_from_file(yaml_file) to load it and override the default options.

Most tools in $ROOT/tools take a --cfg option to specify an override file.
    - See tools/{train,test}_net.py for example code that uses cfg_from_file()
    - See experiments/cfgs/*.yml for example YAML config override files
"""

import os
import os.path as osp
import numpy as np
import math
# `pip install easydict` if you don't have it
from easydict import EasyDict as edict

__C = edict()
# Consumers can get config by:
#   from fast_rcnn_config import cfg
cfg = __C

# region proposal network (RPN) or not
__C.IS_RPN = False
__C.FLIP_X = False
__C.INPUT = 'COLOR'

# multiscale training and testing
__C.IS_MULTISCALE = True
__C.IS_EXTRAPOLATING = True

#
__C.REGION_PROPOSAL = 'RPN'

__C.NET_NAME = 'CaffeNet'
__C.SUBCLS_NAME = 'voxel_exemplars'

#
# Training options
#

__C.TRAIN = edict()

__C.TRAIN.VISUALIZE = False
__C.TRAIN.VERTEX_REG = False
__C.TRAIN.GRID_SIZE = 256
__C.TRAIN.CHROMATIC = False

# Scales to compute real features
__C.TRAIN.SCALES_BASE = (0.25, 0.5, 1.0, 2.0, 3.0)

# The number of scales per octave in the image pyramid 
# An octave is the set of scales up to half of the initial scale
__C.TRAIN.NUM_PER_OCTAVE = 4

# parameters for ROI generating
__C.TRAIN.SPATIAL_SCALE = 0.0625
__C.TRAIN.KERNEL_SIZE = 5

# Aspect ratio to use during training
__C.TRAIN.ASPECTS = (1, 0.75, 0.5, 0.25)

# Images to use per minibatch
__C.TRAIN.IMS_PER_BATCH = 2

# Minibatch size (number of regions of interest [ROIs])
__C.TRAIN.BATCH_SIZE = 128

# Fraction of minibatch that is labeled foreground (i.e. class > 0)
__C.TRAIN.FG_FRACTION = 0.25

# Overlap threshold for a ROI to be considered foreground (if >= FG_THRESH)
__C.TRAIN.FG_THRESH = (0.5,)

# Overlap threshold for a ROI to be considered background (class = 0 if
# overlap in [LO, HI))
__C.TRAIN.BG_THRESH_HI = (0.5,)
__C.TRAIN.BG_THRESH_LO = (0.1,)

# Use horizontally-flipped images during training?
__C.TRAIN.USE_FLIPPED = True

# Train bounding-box regressors
__C.TRAIN.BBOX_REG = True

# Overlap required between a ROI and ground-truth box in order for that ROI to
# be used as a bounding-box regression training example
__C.TRAIN.BBOX_THRESH = (0.5,)

# Iterations between snapshots
__C.TRAIN.SNAPSHOT_ITERS = 10000

# solver.prototxt specifies the snapshot path prefix, this adds an optional
# infix to yield the path: <prefix>[_<infix>]_iters_XYZ.caffemodel
__C.TRAIN.SNAPSHOT_INFIX = ''

# Use a prefetch thread in roi_data_layer.layer
# So far I haven't found this useful; likely more engineering work is required
__C.TRAIN.USE_PREFETCH = False

# Train using subclasses
__C.TRAIN.SUBCLS = True

# Train using viewpoint
__C.TRAIN.VIEWPOINT = False

# Threshold of ROIs in training RCNN
__C.TRAIN.ROI_THRESHOLD = 0.1


# IOU >= thresh: positive example
__C.TRAIN.RPN_POSITIVE_OVERLAP = 0.7
# IOU < thresh: negative example
__C.TRAIN.RPN_NEGATIVE_OVERLAP = 0.3
# If an anchor statisfied by positive and negative conditions set to negative
__C.TRAIN.RPN_CLOBBER_POSITIVES = False
# Max number of foreground examples
__C.TRAIN.RPN_FG_FRACTION = 0.5
# Total number of examples
__C.TRAIN.RPN_BATCHSIZE = 256
# NMS threshold used on RPN proposals
__C.TRAIN.RPN_NMS_THRESH = 0.7
# Number of top scoring boxes to keep before apply NMS to RPN proposals
__C.TRAIN.RPN_PRE_NMS_TOP_N = 12000
# Number of top scoring boxes to keep after applying NMS to RPN proposals
__C.TRAIN.RPN_POST_NMS_TOP_N = 2000
# Proposal height and width both need to be greater than RPN_MIN_SIZE (at orig image scale)
__C.TRAIN.RPN_MIN_SIZE = 16
# Deprecated (outside weights)
__C.TRAIN.RPN_BBOX_INSIDE_WEIGHTS = (1.0, 1.0, 1.0, 1.0)
# Give the positive RPN examples weight of p * 1 / {num positives}
# and give negatives a weight of (1 - p)
# Set to -1.0 to use uniform example weighting
__C.TRAIN.RPN_POSITIVE_WEIGHT = -1.0

__C.TRAIN.RPN_BASE_SIZE = 16
__C.TRAIN.RPN_ASPECTS = [0.25, 0.5, 0.75, 1, 1.5, 2, 3]  # 7 aspects
__C.TRAIN.RPN_SCALES = [2, 2.82842712, 4, 5.65685425, 8, 11.3137085, 16, 22.627417, 32, 45.254834] # 2**np.arange(1, 6, 0.5), 10 scales

#
# Testing options
#

__C.TEST = edict()

__C.TEST.IS_PATCH = False;
__C.TEST.VERTEX_REG = False
__C.TEST.VISUALIZE = False

# Scales to compute real features
__C.TEST.SCALES_BASE = (0.25, 0.5, 1.0, 2.0, 3.0)

# The number of scales per octave in the image pyramid 
# An octave is the set of scales up to half of the initial scale
__C.TEST.NUM_PER_OCTAVE = 4

# Aspect ratio to use during testing
__C.TEST.ASPECTS = (1, 0.75, 0.5, 0.25)

# parameters for ROI generating
__C.TEST.SPATIAL_SCALE = 0.0625
__C.TEST.KERNEL_SIZE = 5

# Overlap threshold used for non-maximum suppression (suppress boxes with
# IoU >= this threshold)
__C.TEST.NMS = 0.5

# Experimental: treat the (K+1) units in the cls_score layer as linear
# predictors (trained, eg, with one-vs-rest SVMs).
__C.TEST.SVM = False

# Test using bounding-box regressors
__C.TEST.BBOX_REG = True

# Test using subclass
__C.TEST.SUBCLS = True

# Train using viewpoint
__C.TEST.VIEWPOINT = False

# Threshold of ROIs in testing
__C.TEST.ROI_THRESHOLD = 0.1
__C.TEST.ROI_THRESHOLD_NUM = 80000
__C.TEST.ROI_NUM = 2000
__C.TEST.DET_THRESHOLD = 0.0001

## NMS threshold used on RPN proposals
__C.TEST.RPN_NMS_THRESH = 0.7
## Number of top scoring boxes to keep before apply NMS to RPN proposals
__C.TEST.RPN_PRE_NMS_TOP_N = 6000
## Number of top scoring boxes to keep after applying NMS to RPN proposals
__C.TEST.RPN_POST_NMS_TOP_N = 300
# Proposal height and width both need to be greater than RPN_MIN_SIZE (at orig image scale)
__C.TEST.RPN_MIN_SIZE = 16

#
# MISC
#

# The mapping from image coordinates to feature map coordinates might cause
# some boxes that are distinct in image space to become identical in feature
# coordinates. If DEDUP_BOXES > 0, then DEDUP_BOXES is used as the scale factor
# for identifying duplicate boxes.
# 1/16 is correct for {Alex,Caffe}Net, VGG_CNN_M_1024, and VGG16
__C.DEDUP_BOXES = 1./16.

# Pixel mean values (BGR order) as a (1, 1, 3) array
# These are the values originally used for training VGG16
__C.PIXEL_MEANS = np.array([[[102.9801, 115.9465, 122.7717]]])

# For reproducibility
__C.RNG_SEED = 3

# A small number that's used many times
__C.EPS = 1e-14

# Root directory of project
__C.ROOT_DIR = osp.abspath(osp.join(osp.dirname(__file__), '..', '..'))

# Place outputs under an experiments directory
__C.EXP_DIR = 'default'

# Use GPU implementation of non-maximum suppression
__C.USE_GPU_NMS = True

# Default GPU device id
__C.GPU_ID = 0

def 
(imdb, net):
    """Return the directory where experimental artifacts are placed.

    A canonical path is built using the name from an imdb and a network
    (if not None).
    """
    path = osp.abspath(osp.join(__C.ROOT_DIR, 'output', __C.EXP_DIR, imdb.name))
    if net is None:
        return path
    else:
        return osp.join(path, net.name)

def _add_more_info(is_train):
    # compute all the scales
    if is_train:
        scales_base = __C.TRAIN.SCALES_BASE
        num_per_octave = __C.TRAIN.NUM_PER_OCTAVE
    else:
        scales_base = __C.TEST.SCALES_BASE
        num_per_octave = __C.TEST.NUM_PER_OCTAVE

    num_scale_base = len(scales_base)
    num = (num_scale_base - 1) * num_per_octave + 1
    scales = []
    for i in xrange(num):
        index_scale_base = i / num_per_octave
        sbase = scales_base[index_scale_base]
        j = i % num_per_octave
        if j == 0:
            scales.append(sbase)
        else:
            sbase_next = scales_base[index_scale_base+1]
            step = (sbase_next - sbase) / num_per_octave
            scales.append(sbase + j * step)

    if is_train:
        __C.TRAIN.SCALES = scales
    else:
        __C.TEST.SCALES = scales
    print scales


    # map the scales to scales for RoI pooling of classification
    if is_train:
        kernel_size = __C.TRAIN.KERNEL_SIZE / __C.TRAIN.SPATIAL_SCALE
    else:
        kernel_size = __C.TEST.KERNEL_SIZE / __C.TEST.SPATIAL_SCALE

    area = kernel_size * kernel_size
    scales = np.array(scales)
    areas = np.repeat(area, num) / (scales ** 2)
    scaled_areas = areas[:, np.newaxis] * (scales[np.newaxis, :] ** 2)
    diff_areas = np.abs(scaled_areas - 224 * 224)
    levels = diff_areas.argmin(axis=1)

    if is_train:
        __C.TRAIN.SCALE_MAPPING = levels
    else:
        __C.TEST.SCALE_MAPPING = levels

    # compute width and height of grid box
    if is_train:
        area = __C.TRAIN.KERNEL_SIZE * __C.TRAIN.KERNEL_SIZE
        aspect = __C.TRAIN.ASPECTS  # height / width
    else:
        area = __C.TEST.KERNEL_SIZE * __C.TEST.KERNEL_SIZE
        aspect = __C.TEST.ASPECTS  # height / width

    num_aspect = len(aspect)
    widths = np.zeros((num_aspect), dtype=np.float32)
    heights = np.zeros((num_aspect), dtype=np.float32)
    for i in xrange(num_aspect):
        widths[i] = math.sqrt(area / aspect[i])
        heights[i] = widths[i] * aspect[i]

    if is_train:
        __C.TRAIN.ASPECT_WIDTHS = widths
        __C.TRAIN.ASPECT_HEIGHTS = heights
        __C.TRAIN.RPN_SCALES = np.array(__C.TRAIN.RPN_SCALES)
    else:
        __C.TEST.ASPECT_WIDTHS = widths
        __C.TEST.ASPECT_HEIGHTS = heights

def _merge_a_into_b(a, b):
    """Merge config dictionary a into config dictionary b, clobbering the
    options in b whenever they are also specified in a.
    """
    if type(a) is not edict:
        return

    for k, v in a.iteritems():
        # a must specify keys that are in b
        if not b.has_key(k):
            raise KeyError('{} is not a valid config key'.format(k))

        # the types must match, too
        if type(b[k]) is not type(v):
            raise ValueError(('Type mismatch ({} vs. {}) '
                              'for config key: {}').format(type(b[k]),
                                                           type(v), k))

        # recursively merge dicts
        if type(v) is edict:
            try:
                _merge_a_into_b(a[k], b[k])
            except:
                print('Error under config key: {}'.format(k))
                raise
        else:
            b[k] = v

def cfg_from_file(filename):
    """Load a config file and merge it into the default options."""
    import yaml
    with open(filename, 'r') as f:
        yaml_cfg = edict(yaml.load(f))

    _merge_a_into_b(yaml_cfg, __C)
    _add_more_info(1)
    _add_more_info(0)

get_output_dir