def export_coreml(self, filename, 
            include_non_maximum_suppression = True,
            iou_threshold = None,
            confidence_threshold = None):
        """
        Save the model in Core ML format. The Core ML model takes an image of
        fixed size as input and produces two output arrays: `confidence` and
        `coordinates`.

        The first one, `confidence` is an `N`-by-`C` array, where `N` is the
        number of instances predicted and `C` is the number of classes. The
        number `N` is fixed and will include many low-confidence predictions.
        The instances are not sorted by confidence, so the first one will
        generally not have the highest confidence (unlike in `predict`). Also
        unlike the `predict` function, the instances have not undergone
        what is called `non-maximum suppression`, which means there could be
        several instances close in location and size that have all discovered
        the same object instance. Confidences do not need to sum to 1 over the
        classes; any remaining probability is implied as confidence there is no
        object instance present at all at the given coordinates. The classes
        appear in the array alphabetically sorted.

        The second array `coordinates` is of size `N`-by-4, where the first
        dimension `N` again represents instances and corresponds to the
        `confidence` array. The second dimension represents `x`, `y`, `width`,
        `height`, in that order.  The values are represented in relative
        coordinates, so (0.5, 0.5) represents the center of the image and (1,
        1) the bottom right corner. You will need to multiply the relative
        values with the original image size before you resized it to the fixed
        input size to get pixel-value coordinates similar to `predict`.

        See Also
        --------
        save

        Parameters
        ----------
        filename : string
            The path of the file where we want to save the Core ML model.
       
        include_non_maximum_suppression : bool
            Non-maximum suppression is only available in iOS 12+.
            A boolean parameter to indicate whether the Core ML model should be
            saved with built-in non-maximum suppression or not. 
            This parameter is set to True by default.

        iou_threshold : float
            Threshold value for non-maximum suppression. Non-maximum suppression
            prevents multiple bounding boxes appearing over a single object. 
            This threshold, set between 0 and 1, controls how aggressive this 
            suppression is. A value of 1 means no maximum suppression will 
            occur, while a value of 0 will maximally suppress neighboring 
            boxes around a prediction.

        confidence_threshold : float
            Only return predictions above this level of confidence. The
            threshold can range from 0 to 1. 

        Examples
        --------
        >>> model.export_coreml('detector.mlmodel')
        """
        import mxnet as _mx
        from .._mxnet._mxnet_to_coreml import _mxnet_converter
        import coremltools
        from coremltools.models import datatypes, neural_network

        if iou_threshold is None: iou_threshold = self.non_maximum_suppression_threshold
        if confidence_threshold is None: confidence_threshold = 0.25

        preds_per_box = 5 + self.num_classes
        num_anchors = len(self.anchors)
        num_classes = self.num_classes
        batch_size = 1
        image_shape = (batch_size,) + tuple(self.input_image_shape)
        s_image_uint8 = _mx.sym.Variable(self.feature, shape=image_shape, dtype=_np.float32)
        s_image = s_image_uint8 / 255

        # Swap a maxpool+slice in mxnet to a coreml natively supported layer
        from copy import copy
        net = copy(self._model)
        net._children = copy(self._model._children)
        from ._model import _SpecialDarknetMaxpoolBlock
        op = _SpecialDarknetMaxpoolBlock(name='pool5')
        # Make sure we are removing the right layers
        assert (self._model[23].name == 'pool5' and
                self._model[24].name == 'specialcrop5')
        del net._children[24]
        net._children[23] = op

        s_ymap = net(s_image)
        mod = _mx.mod.Module(symbol=s_ymap, label_names=None, data_names=[self.feature])
        mod.bind(for_training=False, data_shapes=[(self.feature, image_shape)])

        # Copy over params from net
        mod.init_params()
        arg_params, aux_params = mod.get_params()
        net_params = net.collect_params()
        new_arg_params = {}
        for k, param in arg_params.items():
            new_arg_params[k] = net_params[k].data(net_params[k].list_ctx()[0])
        new_aux_params = {}
        for k, param in aux_params.items():
            new_aux_params[k] = net_params[k].data(net_params[k].list_ctx()[0])
        mod.set_params(new_arg_params, new_aux_params)

        input_names = [self.feature]
        input_dims = [list(self.input_image_shape)]
        input_types = [datatypes.Array(*dim) for dim in input_dims]
        input_features = list(zip(input_names, input_types))

        num_spatial = self._grid_shape[0] * self._grid_shape[1]
        num_bounding_boxes = num_anchors * num_spatial
        CONFIDENCE_STR = ("raw_confidence" if include_non_maximum_suppression 
            else "confidence")
        COORDINATES_STR = ("raw_coordinates" if include_non_maximum_suppression 
            else "coordinates")
        output_names = [
            CONFIDENCE_STR,
            COORDINATES_STR
        ]
        output_dims = [
            (num_bounding_boxes, num_classes),
            (num_bounding_boxes, 4),
        ]
        output_types = [datatypes.Array(*dim) for dim in output_dims]
        output_features = list(zip(output_names, output_types))
        mode = None
        builder = neural_network.NeuralNetworkBuilder(input_features, output_features, mode)
        _mxnet_converter.convert(mod, mode=None,
                                 input_shape=[(self.feature, image_shape)],
                                 builder=builder, verbose=False)

        prefix = '__tc__internal__'

        # (1, B, C+5, S*S)
        builder.add_reshape(name=prefix + 'ymap_sp_pre',
                            target_shape=[batch_size, num_anchors, preds_per_box, num_spatial],
                            mode=0,
                            input_name='conv8_fwd_output',
                            output_name=prefix + 'ymap_sp_pre')

        # (1, C+5, B, S*S)
        builder.add_permute(name=prefix + 'ymap_sp',
                            dim=[0, 2, 1, 3],
                            input_name=prefix + 'ymap_sp_pre',
                            output_name=prefix + 'ymap_sp')

        # POSITION: X/Y

        # (1, 2, B, S*S)
        builder.add_slice(name=prefix + 'raw_rel_xy_sp',
                          axis='channel',
                          start_index=0,
                          end_index=2,
                          stride=1,
                          input_name=prefix + 'ymap_sp',
                          output_name=prefix + 'raw_rel_xy_sp')
        # (1, 2, B, S*S)
        builder.add_activation(name=prefix + 'rel_xy_sp',
                               non_linearity='SIGMOID',
                               input_name=prefix + 'raw_rel_xy_sp',
                               output_name=prefix + 'rel_xy_sp')

        # (1, 2, B*H*W, 1)
        builder.add_reshape(name=prefix + 'rel_xy',
                            target_shape=[batch_size, 2, num_bounding_boxes, 1],
                            mode=0,
                            input_name=prefix + 'rel_xy_sp',
                            output_name=prefix + 'rel_xy')

        c_xy = _np.array(_np.meshgrid(_np.arange(self._grid_shape[1]),
                                      _np.arange(self._grid_shape[0])), dtype=_np.float32)

        c_xy_reshaped = (_np.tile(c_xy[:, _np.newaxis], (num_anchors, 1, 1))
                         .reshape(2, -1))[_np.newaxis, ..., _np.newaxis]

        # (1, 2, B*H*W, 1)
        builder.add_load_constant(prefix + 'constant_xy',
                                  constant_value=c_xy_reshaped,
                                  shape=c_xy_reshaped.shape[1:],
                                  output_name=prefix + 'constant_xy')

        # (1, 2, B*H*W, 1)
        builder.add_elementwise(name=prefix + 'xy',
                                mode='ADD',
                                input_names=[prefix + 'constant_xy', prefix + 'rel_xy'],
                                output_name=prefix + 'xy')

        # SHAPE: WIDTH/HEIGHT

        # (1, 2, B, S*S)
        builder.add_slice(name=prefix + 'raw_rel_wh_sp',
                          axis='channel',
                          start_index=2,
                          end_index=4,
                          stride=1,
                          input_name=prefix + 'ymap_sp',
                          output_name=prefix + 'raw_rel_wh_sp')

        # (1, 2, B, S*S)
        builder.add_unary(name=prefix + 'rel_wh_sp',
                          mode='exp',
                          input_name=prefix + 'raw_rel_wh_sp',
                          output_name=prefix + 'rel_wh_sp')

        # (1, 2*B, S, S)
        builder.add_reshape(name=prefix + 'rel_wh',
                            target_shape=[batch_size, 2 * num_anchors] + list(self._grid_shape),
                            mode=0,
                            input_name=prefix + 'rel_wh_sp',
                            output_name=prefix + 'rel_wh')

        np_anchors = _np.asarray(self.anchors, dtype=_np.float32).T
        anchors_0 = _np.tile(np_anchors.reshape([2 * num_anchors, 1, 1]), self._grid_shape)

        # (1, 2*B, S, S)
        builder.add_load_constant(name=prefix + 'c_anchors',
                                  constant_value=anchors_0,
                                  shape=anchors_0.shape,
                                  output_name=prefix + 'c_anchors')

        # (1, 2*B, S, S)
        builder.add_elementwise(name=prefix + 'wh_pre',
                                mode='MULTIPLY',
                                input_names=[prefix + 'c_anchors', prefix + 'rel_wh'],
                                output_name=prefix + 'wh_pre')

        # (1, 2, B*H*W, 1)
        builder.add_reshape(name=prefix + 'wh',
                            target_shape=[1, 2, num_bounding_boxes, 1],
                            mode=0,
                            input_name=prefix + 'wh_pre',
                            output_name=prefix + 'wh')

        # (1, 4, B*H*W, 1)
        builder.add_elementwise(name=prefix + 'boxes_out_transposed',
                                mode='CONCAT',
                                input_names=[prefix + 'xy', prefix + 'wh'],
                                output_name=prefix + 'boxes_out_transposed')

        # (1, B*H*W, 4, 1)
        builder.add_permute(name=prefix + 'boxes_out',
                            dim=[0, 2, 1, 3],
                            input_name=prefix + 'boxes_out_transposed',
                            output_name=prefix + 'boxes_out')

        scale = _np.zeros((num_bounding_boxes, 4, 1))
        scale[:, 0::2] = 1.0 / self._grid_shape[1]
        scale[:, 1::2] = 1.0 / self._grid_shape[0]

        # (1, B*H*W, 4, 1)
        builder.add_scale(name=COORDINATES_STR,
                          W=scale,
                          b=0,
                          has_bias=False,
                          shape_scale=(num_bounding_boxes, 4, 1),
                          input_name=prefix + 'boxes_out',
                          output_name=COORDINATES_STR)

        # CLASS PROBABILITIES AND OBJECT CONFIDENCE

        # (1, C, B, H*W)
        builder.add_slice(name=prefix + 'scores_sp',
                          axis='channel',
                          start_index=5,
                          end_index=preds_per_box,
                          stride=1,
                          input_name=prefix + 'ymap_sp',
                          output_name=prefix + 'scores_sp')

        # (1, C, B, H*W)
        builder.add_softmax(name=prefix + 'probs_sp',
                            input_name=prefix + 'scores_sp',
                            output_name=prefix + 'probs_sp')

        # (1, 1, B, H*W)
        builder.add_slice(name=prefix + 'logit_conf_sp',
                          axis='channel',
                          start_index=4,
                          end_index=5,
                          stride=1,
                          input_name=prefix + 'ymap_sp',
                          output_name=prefix + 'logit_conf_sp')

        # (1, 1, B, H*W)
        builder.add_activation(name=prefix + 'conf_sp',
                               non_linearity='SIGMOID',
                               input_name=prefix + 'logit_conf_sp',
                               output_name=prefix + 'conf_sp')

        # (1, C, B, H*W)
        if num_classes > 1:
            conf = prefix + 'conf_tiled_sp'
            builder.add_elementwise(name=prefix + 'conf_tiled_sp',
                                    mode='CONCAT',
                                    input_names=[prefix+'conf_sp']*num_classes,
                                    output_name=conf)
        else:
            conf = prefix + 'conf_sp'

        # (1, C, B, H*W)
        builder.add_elementwise(name=prefix + 'confprobs_sp',
                                mode='MULTIPLY',
                                input_names=[conf, prefix + 'probs_sp'],
                                output_name=prefix + 'confprobs_sp')

        # (1, C, B*H*W, 1)
        builder.add_reshape(name=prefix + 'confprobs_transposed',
                            target_shape=[1, num_classes, num_bounding_boxes, 1],
                            mode=0,
                            input_name=prefix + 'confprobs_sp',
                            output_name=prefix + 'confprobs_transposed')

        # (1, B*H*W, C, 1)
        builder.add_permute(name=CONFIDENCE_STR,
                            dim=[0, 2, 1, 3],
                            input_name=prefix + 'confprobs_transposed',
                            output_name=CONFIDENCE_STR)

        _mxnet_converter._set_input_output_layers(
            builder, input_names, output_names)
        builder.set_input(input_names, input_dims)
        builder.set_output(output_names, output_dims)
        builder.set_pre_processing_parameters(image_input_names=self.feature)
        model = builder.spec

        if include_non_maximum_suppression:
            # Non-Maximum Suppression is a post-processing algorithm
            # responsible for merging all detections that belong to the
            # same object.
            #  Core ML schematic   
            #                        +------------------------------------+
            #                        | Pipeline                           |
            #                        |                                    |
            #                        |  +------------+   +-------------+  |
            #                        |  | Neural     |   | Non-maximum |  |
            #                        |  | network    +---> suppression +----->  confidences
            #               Image  +---->            |   |             |  |
            #                        |  |            +--->             +----->  coordinates
            #                        |  |            |   |             |  |
            # Optional inputs:       |  +------------+   +-^---^-------+  |
            #                        |                     |   |          |
            #    IOU threshold     +-----------------------+   |          |
            #                        |                         |          |
            # Confidence threshold +---------------------------+          |
            #                        +------------------------------------+

            model_neural_network = model.neuralNetwork
            model.specificationVersion = 3
            model.pipeline.ParseFromString(b'')
            model.pipeline.models.add()
            model.pipeline.models[0].neuralNetwork.ParseFromString(b'')
            model.pipeline.models.add()
            model.pipeline.models[1].nonMaximumSuppression.ParseFromString(b'')
            # begin: Neural network  model
            nn_model = model.pipeline.models[0]

            nn_model.description.ParseFromString(b'')
            input_image = model.description.input[0]
            input_image.type.imageType.width = self.input_image_shape[1]
            input_image.type.imageType.height = self.input_image_shape[2]
            nn_model.description.input.add()
            nn_model.description.input[0].ParseFromString(
                input_image.SerializeToString())

            for i in range(2):
                del model.description.output[i].type.multiArrayType.shape[:]
            names = ["raw_confidence", "raw_coordinates"]
            bounds = [self.num_classes, 4]
            for i in range(2):
                output_i = model.description.output[i]
                output_i.name = names[i]
                for j in range(2):
                    ma_type = output_i.type.multiArrayType
                    ma_type.shapeRange.sizeRanges.add()
                    ma_type.shapeRange.sizeRanges[j].lowerBound = (
                        bounds[i] if j == 1 else 0)
                    ma_type.shapeRange.sizeRanges[j].upperBound = (
                        bounds[i] if j == 1 else -1)
                nn_model.description.output.add()
                nn_model.description.output[i].ParseFromString(
                    output_i.SerializeToString())

                ma_type = nn_model.description.output[i].type.multiArrayType
                ma_type.shape.append(num_bounding_boxes)
                ma_type.shape.append(bounds[i])
            
            # Think more about this line
            nn_model.neuralNetwork.ParseFromString(
                model_neural_network.SerializeToString())
            nn_model.specificationVersion = model.specificationVersion
            # end: Neural network  model

            # begin: Non maximum suppression model
            nms_model = model.pipeline.models[1]
            nms_model_nonMaxSup = nms_model.nonMaximumSuppression
            
            for i in range(2):
                output_i = model.description.output[i]
                nms_model.description.input.add()
                nms_model.description.input[i].ParseFromString(
                    output_i.SerializeToString())

                nms_model.description.output.add()
                nms_model.description.output[i].ParseFromString(
                    output_i.SerializeToString())
                nms_model.description.output[i].name = (
                    'confidence' if i==0 else 'coordinates')
            
            nms_model_nonMaxSup.iouThreshold = iou_threshold
            nms_model_nonMaxSup.confidenceThreshold = confidence_threshold
            nms_model_nonMaxSup.confidenceInputFeatureName = 'raw_confidence'
            nms_model_nonMaxSup.coordinatesInputFeatureName = 'raw_coordinates'
            nms_model_nonMaxSup.confidenceOutputFeatureName = 'confidence'
            nms_model_nonMaxSup.coordinatesOutputFeatureName = 'coordinates'
            nms_model.specificationVersion = model.specificationVersion
            nms_model_nonMaxSup.stringClassLabels.vector.extend(self.classes)

            for i in range(2):
                nms_model.description.input[i].ParseFromString(
                    nn_model.description.output[i].SerializeToString()
                )

            if include_non_maximum_suppression:
                # Iou Threshold
                IOU_THRESHOLD_STRING = 'iouThreshold'
                model.description.input.add()
                model.description.input[1].type.doubleType.ParseFromString(b'')
                model.description.input[1].name = IOU_THRESHOLD_STRING
                nms_model.description.input.add()
                nms_model.description.input[2].ParseFromString(
                    model.description.input[1].SerializeToString()
                )
                nms_model_nonMaxSup.iouThresholdInputFeatureName = IOU_THRESHOLD_STRING
                
                # Confidence Threshold
                CONFIDENCE_THRESHOLD_STRING = 'confidenceThreshold'
                model.description.input.add()
                model.description.input[2].type.doubleType.ParseFromString(b'')
                model.description.input[2].name = CONFIDENCE_THRESHOLD_STRING

                nms_model.description.input.add()
                nms_model.description.input[3].ParseFromString(
                    model.description.input[2].SerializeToString())

                nms_model_nonMaxSup.confidenceThresholdInputFeatureName = \
                    CONFIDENCE_THRESHOLD_STRING
                
            # end: Non maximum suppression model
            model.description.output[0].name = 'confidence'
            model.description.output[1].name = 'coordinates'

        iouThresholdString = '(optional) IOU Threshold override (default: {})'
        confidenceThresholdString = ('(optional)' + 
            ' Confidence Threshold override (default: {})')
        model_type = 'object detector (%s)' % self.model
        if include_non_maximum_suppression:
            model_type += ' with non-maximum suppression'
        model.description.metadata.shortDescription = \
            _coreml_utils._mlmodel_short_description(model_type)
        model.description.input[0].shortDescription = 'Input image'
        if include_non_maximum_suppression:
            iouThresholdString = '(optional) IOU Threshold override (default: {})'
            model.description.input[1].shortDescription = \
                iouThresholdString.format(iou_threshold)
            confidenceThresholdString = ('(optional)' + 
                ' Confidence Threshold override (default: {})')
            model.description.input[2].shortDescription = \
                confidenceThresholdString.format(confidence_threshold)
        model.description.output[0].shortDescription = \
            u'Boxes \xd7 Class confidence (see user-defined metadata "classes")'
        model.description.output[1].shortDescription = \
            u'Boxes \xd7 [x, y, width, height] (relative to image size)'
        version = ObjectDetector._PYTHON_OBJECT_DETECTOR_VERSION
        partial_user_defined_metadata = {
            'model': self.model,
            'max_iterations': str(self.max_iterations),
            'training_iterations': str(self.training_iterations),
            'include_non_maximum_suppression': str(
                include_non_maximum_suppression),
            'non_maximum_suppression_threshold': str(
                iou_threshold),
            'confidence_threshold': str(confidence_threshold),
            'iou_threshold': str(iou_threshold),
            'feature': self.feature,
            'annotations': self.annotations,
            'classes': ','.join(self.classes)
        }
        user_defined_metadata = _coreml_utils._get_model_metadata(
            self.__class__.__name__,
            partial_user_defined_metadata,
            version)
        model.description.metadata.userDefined.update(user_defined_metadata)
        from coremltools.models.utils import save_spec as _save_spec
        _save_spec(model, filename)