#!/usr/bin/env python3
# coding: utf-8

################################################################################
# Boost Software License - Version 1.0 - August 17th, 2003                     #
#                                                                              #
# Permission is hereby granted, free of charge, to any person or organization  #
# obtaining a copy of the software and accompanying documentation covered by   #
# this license (the "Software") to use, reproduce, display, distribute,        #
# execute, and transmit the Software, and to prepare derivative works of the   #
# Software, and to permit third-parties to whom the Software is furnished to   #
# do so, all subject to the following:                                         #
#                                                                              #
# The copyright notices in the Software and this entire statement, including   #
# the above license grant, this restriction and the following disclaimer,      #
# must be included in all copies of the Software, in whole or in part, and     #
# all derivative works of the Software, unless such copies or derivative       #
# works are solely in the form of machine-executable object code generated by  #
# a source language processor.                                                 #
#                                                                              #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR   #
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,     #
# FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT    #
# SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE    #
# FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,  #
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER  #
# DEALINGS IN THE SOFTWARE.                                                    #
################################################################################

################################################################################
# Last modified:   28.02.2019                                                  #
#                                                                              #
# Dieses Skript erlaubt es in .h5 Modelle nach Dlib (C++) zu exportieren.      #
#                                                                              #
# Usage: ./keras2dlib.py --h5 model.h5 --output dlib_net                       #
#   --> Lädt model.h5 und erstellt dlib_net.hpp + dlib_net.dat                 #
#                                                                              #
# Folgende Keras Layers werden unterstützt:                                    #
#   - keras.layers.Dense                                                       #
#   - bei Conv2D gibt es noch ein Problem in Dlib:                             #
# Es handelt sich um ein Problem im keras2dlib Serialisierungs-Code (Conv2D)   #
# Die Parameter der Conv2D Layer werden noch nicht richtig serialisiert        #
# Dlib_max_pool::output_shape() gibt falsches nr und nc                        #
#                                                                              #
################################################################################

import argparse
from keras.layers import Dense
from keras.utils.io_utils import H5Dict
import numpy as np
import h5py
import json
import struct
import math
import os
import sys
import errno
import copy

def mkdir(dir):
    try:
        os.makedirs(dir)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise

class SerializationError(Exception):
    pass

################################################################################
# Helper functions and classes to mimic recursive serialization of dlib.       #
# These mirror the functionality as implemented in dlib, so we can serialize   #
# in the correct native Dlib format, more or less without problems.            #
################################################################################
def serialize_int(num, out):
    anum = abs(num)
    format = '<'

    if anum < 2**8   :
        controlByte = 0x01
        format += 'B'
    elif anum < 2**16:
        controlByte = 0x02
        format += 'H'
    elif anum < 2**32:
        controlByte = 0x04
        format += 'I'
    else:
        controlByte = 0x08
        format += 'Q'

    if num < 0:
        controlByte |= 0x80

    out.write(struct.pack('B', controlByte))
    out.write(struct.pack(format, abs(num)))

# Implementation taken from dlib::float_details
# Sometimes mantissa might be slightly off but the value should be ok
def serialize_float(num, out):
    mantissa = 0
    exponent = 0
    is_inf   = 32000
    is_ninf  = 32001
    is_nan   = 32002

    # defined as std::numeric_limits<float>::digits on the hardware
    digits_HW_float = 24
    # number of digits in the mantissa
    digits = min(digits_HW_float, 63)

    if math.isinf(num):
        if num > 0:
            exponent = is_inf
        else:
            exponent = is_ninf
    elif math.isnan(num):
        exponent = is_nan;
    else:
        mantissa, exp = math.frexp(num)
        mantissa = int(mantissa * (1 << digits))
        exponent = exp - digits;

        # Compact the representation a bit by shifting off any low order bytes
        # which are zero in the mantissa.  This makes the numbers in mantissa and
        # exponent generally smaller which can make serialization and other things
        # more efficient in some cases.
        for i in range(8):
            if (mantissa & 0xFF) == 0:
                mantissa >>= 8
                exponent += 8
            else:
                break
    serialize_int(mantissa, out)
    serialize_int(exponent, out)

def serialize_str(str, out):
    serialize_int(len(str), out)
    out.write(str.encode())

def serialize_bool(bool, out):
    out.write(('1' if bool else '0').encode())

# not sure if this is needed
def serialize_tensor(ndarray, out):
    if ndarray.ndim != 4:
        raise SerializationError('Dlib tensors must have ndim=4')
    version = 2
    num_samples, k, nr, nc = ndarray.shape
    serialize_int(version, out)
    serialize_int(num_samples, out);
    serialize_int(k, out);
    serialize_int(nr, out);
    serialize_int(nc, out);
    if ndarray.size > 0:
        for x in np.nditer(ndarray):
            # write in 4 byte little endian float
            out.write(struct.pack('<f', x)) # bad idea to use struct.pack ?

def serialize_alias_tensor(num_samples, k, nr, nc, out):
    version = 1
    serialize_int(version, out)
    serialize_int(num_samples, out)
    serialize_int(k, out)
    serialize_int(nr, out)
    serialize_int(nc, out)

# This is for documentation purposes only (base class in theory)
class Dlib_Layer:
    # serialize the layer into .dat file (mimic dlib::serialize functions)
    def serialize(self, out):
        pass

    # generate code in .hpp for the dlib layer definition
    # this function returns the number of opening '<' it uses
    # (except for add_loss_layer, which writes the matching number of closing '>')
    def generate_header(self, out):
        pass

    # Mirror dlib setup() function. This may be used in some layers to set
    # parameters depending on the previous layer. This function is called by
    # add_layer objects
    def setup(self, subnet):
        pass

class Dlib_add_loss_layer:
    def __init__(self, loss_details, subnet):
        loss_details.setup(subnet)

        self.version = 1
        self.loss_details = loss_details
        self.subnet = subnet

    def serialize(self, out):
        serialize_int(self.version, out)
        self.loss_details.serialize(out)
        self.subnet.serialize(out)

    def generate_header(self, out):
        out.write('\nusing Net_t = \n')
        numGreaterThan = self.loss_details.generate_header(out)
        numGreaterThan += self.subnet.generate_header(out)
        out.write('>' * numGreaterThan + ';\n')

    def setup(self, subnet):
        pass

class Dlib_add_layer:
    def __init__(self, layer_details, subnet, subnetIsInputLayer=False):
        layer_details.setup(subnet)
        self.this_layer_setup_called = True

        self.layer_details = layer_details
        self.subnet = subnet
        self.x_grad = np.empty((0,0,0,0))
        self.cached_output = np.empty((0,0,0,0))
        self.params_grad = np.empty((0,0,0,0))
        self.gradient_input_is_stale = True
        self.get_output_and_gradient_input_disabled = False
        self.subnetIsInputLayer = subnetIsInputLayer
        if subnetIsInputLayer:
            self.version = 3
            self._sample_expansion_factor = 1
        else:
            self.version = 2

    def serialize(self, out):
        # We use an empty dummy tensor for training parameters dlib would store.
        # This is like calling net.clean() before serializing in dlib
        serialize_int(self.version, out)
        self.subnet.serialize(out)
        self.layer_details.serialize(out)
        serialize_bool(self.this_layer_setup_called, out)
        serialize_bool(self.gradient_input_is_stale, out)
        serialize_bool(self.get_output_and_gradient_input_disabled, out)
        serialize_tensor(self.x_grad, out)
        serialize_tensor(self.cached_output, out)
        serialize_tensor(self.params_grad, out)
        if self.subnetIsInputLayer:
            serialize_int(self._sample_expansion_factor, out)

    def generate_header(self, out):
        numGreaterThan = self.layer_details.generate_header(out)
        return numGreaterThan + self.subnet.generate_header(out)

    def setup(self, subnet):
        pass
        
    def output_shape(self):
        return self.layer_details.output_shape()

class Dlib_matrix_input:
    def __init__(self, kerasDataType, nr=-1, nc=-1):
        self.nr = nr
        self.nc = nc
        if kerasDataType == 'float32':
            self.dtype = 'float'
        else:
            raise SerializationError('Keras data type {} not implemented.'.format(kerasDataType))

    def serialize(self, out):
        serialize_str('input<matrix>', out)

    def generate_header(self, out):
        #if self.nr < 1 and self.nc < 1:
        out.write('dlib::input<dlib::matrix<{}>>\n'.format(self.dtype))
        #else:
        #    out.write('dlib::input<dlib::matrix<{},{},{}>>\n'.format(self.dtype, self.nr, self.nc))
        return 0

    def output_shape(self):
        num_samples = self.nr * self.nc
        return (num_samples, 1, self.nr, self.nc)

class Dlib_fc:
    def __init__(self, num_inputs, num_outputs, params, use_bias=True):
        # print("num_inputs_init: {}".format(num_inputs))
    
        self.version = 'fc_2'
        self.num_inputs = num_inputs
        self.num_outputs = num_outputs
        self.params = params
        self.use_bias = use_bias
        self.learning_rate_multiplier = 1.0
        self.weight_decay_multiplier = 1.0
        self.bias_learning_rate_multiplier = 1.0
        self.bias_weight_decay_multiplier = 0.0

    def serialize(self, out):
        serialize_str(self.version, out)
        serialize_int(self.num_outputs, out)
        serialize_int(self.num_inputs, out)
        serialize_tensor(self.params, out)
        serialize_alias_tensor(self.num_inputs, self.num_outputs, 1, 1, out)
        serialize_alias_tensor(1, self.num_outputs, 1, 1, out)
        serialize_int(0 if self.use_bias else 1, out) # FC_HAS_BIAS = 0, FC_NO_BIAS = 1
        serialize_float(self.learning_rate_multiplier, out)
        serialize_float(self.weight_decay_multiplier, out)
        serialize_float(self.bias_learning_rate_multiplier, out)
        serialize_float(self.bias_weight_decay_multiplier, out)

    def generate_header(self, out):
        out.write('dlib::fc<{},\n'.format(self.num_outputs))
        return 1

    def setup(self, subnet):
        self.IN_num_samples, self.IN_k, self.IN_nr, self.IN_nc = subnet.output_shape()
        # self.num_inputs = int(self.IN_k * self.IN_nr * self.IN_nc)
        # print("num_inputs setup: {}".format(self.num_inputs))
        
    def output_shape(self):
        OUT_num_samples = self.IN_num_samples
        OUT_k = self.num_outputs
        OUT_nr = 1
        OUT_nc = 1
        return (OUT_num_samples, OUT_k, OUT_nr, OUT_nc)

class Dlib_max_pool:
    def __init__(self, nr, nc, stride_y, stride_x, padding_y, padding_x):
        self.version = 'max_pool_2'
        self.nr = nr
        self.nc = nc
        self.stride_y = stride_y
        self.stride_x = stride_x
        self.padding_y = padding_y
        self.padding_x = padding_x

    def serialize(self, out):
        serialize_str(self.version, out);
        serialize_int(self.nr, out);
        serialize_int(self.nc, out);
        serialize_int(self.stride_y, out);
        serialize_int(self.stride_x, out);
        serialize_int(self.padding_y, out);
        serialize_int(self.padding_x, out);

    def generate_header(self, out):
        out.write('dlib::max_pool<{},{},{},{},\n' \
                  .format(self.nr, self.nc, self.stride_y, self.stride_x))#, self.padding_y, self.padding_x))
        return 1

    def setup(self, subnet):
        self.IN_num_samples, self.IN_k, self.IN_nr, self.IN_nc = subnet.output_shape()

    def output_shape(self):
        filt_nr = self.IN_nr if self.nr == 0 else self.nr
        filt_nc = self.IN_nc if self.nc == 0 else self.nc
        OUT_num_samples = self.IN_num_samples
        OUT_k = self.IN_k
        OUT_nr = 1+(self.IN_nr + 2*self.padding_y - filt_nr) / self.stride_y
        OUT_nc = 1+(self.IN_nc + 2*self.padding_x - filt_nc) / self.stride_x
        print("max_pool output k,nr,nc: {}, {}, {}".format(OUT_k, OUT_nr, OUT_nc))
        return (OUT_num_samples, OUT_k, OUT_nr, OUT_nc)
        
# Generates a random binary mask with drop_rate which is multiplied with input
# we will just serialize an empty tenor for mask, since dlib creates it
class Dlib_dropout:
    def __init__(self, drop_rate):
        self.version = 'dropout_'
        self.drop_rate = drop_rate
        self.mask = np.empty((0,0,0,0))

    def serialize(self, out):
        serialize_str(self.version, out)
        serialize_float(self.drop_rate, out)
        serialize_tensor(self.mask, out)

    def generate_header(self, out):
        out.write('dlib::dropout<')
        return 1

    def setup(self, subnet):
        self.IN_output_shape = subnet.output_shape()
        
    def output_shape(self):
        return self.IN_output_shape

class Dlib_con:
    def __init__(self, num_filters, nr, nc, stride_y, stride_x, padding_y, padding_x, params):
        self.version = 'con_4'
        self.num_filters = num_filters
        self.nr = nr
        self.nc = nc
        self.stride_y = stride_y
        self.stride_x = stride_x
        self.padding_y = padding_y
        self.padding_x = padding_x
        self.params = params
        self.learning_rate_multiplier = 1.0
        self.weight_decay_multiplier = 1.0
        self.bias_learning_rate_multiplier = 1.0
        self.bias_weight_decay_multiplier = 0.0

    def serialize(self, out):
        serialize_str(self.version, out)
        serialize_tensor(self.params, out)
        serialize_int(self.num_filters, out)
        serialize_int(self.nr, out)
        serialize_int(self.nc, out)
        serialize_int(self.stride_y, out)
        serialize_int(self.stride_x, out)
        serialize_int(self.padding_y, out)
        serialize_int(self.padding_x, out)
        serialize_alias_tensor(self.num_filters, self.filt_k, self.filt_nr, self.filt_nc, out) # filters
        serialize_alias_tensor(1, self.num_filters, 1, 1, out) # biases
        serialize_float(self.learning_rate_multiplier, out)
        serialize_float(self.weight_decay_multiplier, out)
        serialize_float(self.bias_learning_rate_multiplier, out)
        serialize_float(self.bias_weight_decay_multiplier, out)

    def generate_header(self, out):
        # TODO: Fill variables
        out.write('dlib::con<{},{},{},{},{},\n'.format( \
            self.num_filters, self.nr, self.nc, \
            self.stride_y, self.stride_x, self.padding_y, self.padding_x))
        return 1

    def setup(self, subnet):
        subnet_shape = subnet.output_shape()
        self.IN_num_samples, self.IN_k, self.IN_nr, self.IN_nc = subnet_shape
        self.filt_nr = self.nr if self.nr != 0 else subnet_shape[2]
        self.filt_nc = self.nc if self.nc != 0 else subnet_shape[3]
        self.filt_k = subnet_shape[1]

    # return [num_samples, k, nr, nc]
    def output_shape(self):
        # taken from dlib/layers_abstract.h
        OUT_num_samples = self.IN_num_samples
        OUT_k = self.num_filters
        OUT_nr = 1 if self.nr == 0 else 1+(self.IN_nr + 2*self.padding_y - self.nr) / self.stride_y
        OUT_nc = 1 if self.nc == 0 else 1+(self.IN_nc + 2*self.padding_x - self.nc) / self.stride_x
        return (OUT_num_samples, OUT_k, OUT_nr, OUT_nc)

class Dlib_loss:
    def __init__(self, kerasLoss):
        if kerasLoss == 'mse':
            self.loss_class_name = 'loss_mean_squared_multioutput_'
        elif kerasLoss == 'categorical_crossentropy':
            self.loss_class_name = 'loss_multiclass_log_'
        else:
            raise ValueError('Dlib_loss: Keras loss {} not implemented yet.'.format(kerasLoss))
        # loss_mean_squared_ --> dlib::loss_mean_squared etc.
        self.loss_layer = 'dlib::' + self.loss_class_name[:-1]

    def serialize(self, out):
        serialize_str(self.loss_class_name, out)

    def generate_header(self, out):
        out.write(self.loss_layer + '<\n')
        return 1

    def setup(self, subnet):
        pass

class Dlib_activation:
    def __init__(self, kerasActivation):
        if kerasActivation == 'relu':
            self.activation_class_name = 'relu_'
        elif kerasActivation == 'sigmoid':
            self.activation_class_name = 'sig_'
        elif kerasActivation == 'softmax':
            self.activation_class_name = 'softmax_'
        elif kerasActivation == 'tanh':
            self.activation_class_name = 'htan_'
        else:
            raise SerializationError('Keras activation {} not implemented yet.'.format(kerasActivation))
        # relu_ --> dlib::relu etc.
        self.activation_layer = 'dlib::' + self.activation_class_name[:-1]

    def serialize(self, out):
        serialize_str(self.activation_class_name, out)

    def generate_header(self, out):
        out.write(self.activation_layer + '<')
        return 1

    def setup(self, subnet):
        self.OUT_shape = subnet.output_shape()
        
    def output_shape(self):
        print("Dlib_activation output_shape: [{}, {}, {}, {}]".format(self.OUT_shape[0], self.OUT_shape[1], self.OUT_shape[2], self.OUT_shape[3]))
        return self.OUT_shape
        
# This activation function has params and not simply stores a string -> separate
# class
class Dlib_prelu:
    def __init__(self, initial_param):
        self.version = 'prelu_'
        self.initial_param = initial_param
        self.params = np.array([[[[ initial_param ]]]])

    def serialize(self, out):
        serialize_str(self.version, out)
        serialize_tensor(self.params, out)
        serialize_float(self.initial_param, out)

    def generate_header(self, out):
        out.write('dlib::prelu<')
        return 1

    def setup(self, subnet):
        pass
################################################################################
# The actual conversion logic                                                  #
################################################################################
def load_layer_params_Dense(h5dict, layerName, use_bias):
    model_weights_group = h5dict['model_weights']
    weights = model_weights_group[layerName]
    weight_names = weights['weight_names']
    weight_values = [weights[weight_name] for weight_name in weight_names]

    # load weights and biases
    # [num_inputs, num_outputs]
    params = np.array(weight_values[0])
    if use_bias:
        # [1, num_outputs]
        bias_values = np.expand_dims(np.array(weight_values[1]), axis=0)
        params = np.vstack((params, bias_values))
    # convert to dlib tensor shape (4D)
    params = params.reshape(params.shape[0], params.shape[1], 1, 1).astype(float)
    return params

def load_layer_params_Conv2D(h5dict, layerName, use_bias=True):
    print("load_layer_params_Conv2D")
    model_weights_group = h5dict['model_weights']
    weights = model_weights_group[layerName]
    weight_names = weights['weight_names']
    weight_values = [weights[weight_name] for weight_name in weight_names]

    # load kernels and biases
    # [nr, nc, k, num_filtes] (Keras) -> [num_filters, k, nr, nc] (Dlib)
    params = np.transpose(np.array(weight_values[0])).flatten()
    if use_bias:
        bias_values = np.array(weight_values[1])
        params = np.concatenate((params, bias_values))
    params = params.reshape(params.size, 1, 1, 1)
    print("params:")
    for i in np.nditer(params):
        print(i, end=', ')
    print("\n")
    return params

# This function loads the .h5 model into an internal representation from which
# various files are generated
def build_internal_rep(inputFile):
    h5dict = H5Dict(inputFile, mode='r')
    model_config = get_json_key(h5dict, 'model_config')
    # Dlib requires an add_loss_layer at the end of the network (I think)
    training_config = get_json_key(h5dict, 'training_config')
    model_layers = model_config['config']['layers']
    optimizer_config = training_config['optimizer_config']
    loss = training_config['loss']

    # This list stores all the Dlib_Layer objects used
    # This prevents problems with python shallow copying if you would pass the
    # layer objects themselves while building the subnetworks
    layers = []
    for layer in model_config['config']['layers']:
        layerClass = layer['class_name']
        print('-- Converting Keras.layers.{}'.format(layerClass))
        layer = layer['config'] # save some verbosity

        # Create additional dlib::input<> for the first Keras layer (input layer)
        isInputLayer = 'batch_input_shape' in layer
        if isInputLayer:
            if len(layers) > 0:
                raise SerializationError('Unexpected input layer at index {}'.format(len(layers)))
        elif len(layers) == 0:
            raise SerializationError('Expected an input layer as first layer')

        # If we leave out the input matrix size calculation (which is probably a good idea)
        # we can refactor adding the input layer out to here, instead of duplicating it
        # to every layer which could serve as the first layer
        if isInputLayer:
            layers.append(Dlib_matrix_input(layer['dtype']))

        ########################################################################
        # keras.layers.Dense
        ########################################################################
        if layerClass == 'Dense':
            num_outputs = layer['units']
            activation  = layer['activation']
            use_bias    = layer['use_bias']

            params = load_layer_params_Dense(h5dict, layer['name'], use_bias)
            num_inputs = params.shape[0]
            if use_bias: # subtract the additional space biases occupy in layer parameters
                num_inputs -= 1

            layers.append(Dlib_add_layer(Dlib_fc(num_inputs, num_outputs, params, use_bias), layers[-1], isInputLayer))
            if activation != 'linear':
                layers.append(Dlib_add_layer(Dlib_activation(activation), layers[-1]))
        ########################################################################
        # keras.layers.Conv2d
        ########################################################################
        elif layerClass == 'Conv2D':
            # "data_format": "channels_last"
            # "dilation_rate": [1, 1]
            num_filters        = layer['filters']
            nr, nc             = layer['kernel_size']
            stride_y, stride_x = layer['strides']
            padding            = layer['padding']
            activation         = layer['activation']
            use_bias           = layer['use_bias']

            # TODO: Figure out how to properly handle valid/same padding to dlib
            # until then we use the same as the default template parameters
            padding_y = 0 if stride_y != 1 else nr // 2
            padding_x = 0 if stride_x != 1 else nc // 2
            if padding == 'valid':
                pass
            elif padding == 'same':
                pass
            else:
                raise SerializationError('Unsupported layer padding: {}'.format(padding))

            params = load_layer_params_Conv2D(h5dict, layer['name'])

            layers.append(Dlib_add_layer(Dlib_con(num_filters, nr, nc, stride_y, stride_x, padding_y, padding_x, params), layers[-1], isInputLayer))
            if activation != 'linear':
                layers.append(Dlib_add_layer(Dlib_activation(activation), layers[-1]))
        ########################################################################
        # keras.layers.MaxPooling2D
        ########################################################################
        elif layerClass == 'MaxPooling2D':
            nr, nc             = layer['pool_size']
            stride_y, stride_x = layer['strides']
            padding            = layer['padding']

            # TODO: Figure out how to properly handle valid/same padding to dlib
            # until then we use the same as the default template parameters
            padding_y = 0 if stride_y != 1 else nr // 2
            padding_x = 0 if stride_x != 1 else nc // 2
            if padding == 'valid':
                pass
            elif padding == 'same':
                pass
            else:
                raise SerializationError('Unsupported layer padding: {}'.format(padding))

            layers.append(Dlib_add_layer(Dlib_max_pool(nr, nc, stride_y, stride_x, padding_y, padding_x), layers[-1]))
        ########################################################################
        # keras.layers.Flatten
        ########################################################################
        elif layerClass == 'Flatten':
            # For now we ignore Flatten() Layers, I think dlib works without them
            pass
        ########################################################################
        # keras.layers.Dropout
        ########################################################################
        elif layerClass == 'Dropout':
            layers.append(Dlib_add_layer(Dlib_dropout(layer['rate']), layers[-1], isInputLayer))
        ########################################################################
        # keras.layers.LeakyReLU
        ########################################################################
        elif layerClass == 'LeakyReLU':
            layers.append(Dlib_add_layer(Dlib_prelu(layer['alpha']), layers[-1]))
        ########################################################################
        # Other layers
        ########################################################################
        else:
            raise SerializationError('Unsupported layer class {}'.format(layerClass))

    layers.append(Dlib_add_loss_layer(Dlib_loss(loss), layers[-1]))
    return layers

# Helper function to reduce code duplication
def get_json_key(h5dict, name):
    json_str = h5dict[name]
    if json_str is None:
        raise ValueError('{} not found in .h5 model'.format(name))
    return json.loads(json_str.decode('utf-8'))

def convert_h5_to_dlib(inputFile, outHeader, outModel):
    representation = build_internal_rep(inputFile)
    ############################################################################
    # Generate .hpp header
    ############################################################################
    with open(outHeader, 'w') as f:
        includeGuard = 'INCLUDE_GUARD_DLIB_NET_HPP'
        f.write('#ifndef {0}\n#define {0}\n'.format(includeGuard))
        f.write('\n#include <dlib/dnn.h>\n')
        representation[-1].generate_header(f)
        f.write('\n#endif // {}\n'.format(includeGuard))

    ############################################################################
    # Serialize .dat Dlib model
    ############################################################################
    with open(outModel, 'wb') as f:
        representation[-1].serialize(f)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('h5', help='The .h5 model file to convert.', type=str)
    parser.add_argument('--out-header', default='dlib_net.hpp')
    parser.add_argument('--out-model', default='dlib_net.dat')
    parser.add_argument('--outdir', default='./')
    args = parser.parse_args()
    header_path = os.path.join(args.outdir, args.out_header)
    model_path = os.path.join(args.outdir, args.out_model)
    mkdir(args.outdir)
    convert_h5_to_dlib(args.h5, header_path, model_path)

if __name__ == '__main__':
    main()
