#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Author: David R
#

import wx
import numpy as np
import cv2
import tkinter as tk
import time
import os
from skimage.feature import peak_local_max
from skimage.morphology import watershed
from scipy import ndimage
from tkinter import filedialog

# INITIALIZE PATH
saveVerify_path = []
filename = 'path.config'
if '_MEIPASS2' in os.environ:
    filename = os.path.join(os.environ['_MEIPASS2'], filenamen)
for line in open(filename):
    saveVerify_path.append(line.rstrip('\n'))
print("Save path: " + saveVerify_path[0])
print("Verify path: " + saveVerify_path[1])

detailOutput = []

class MainFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # MainFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.MINIMIZE_BOX | wx.SYSTEM_MENU
        wx.Frame.__init__(self, *args, **kwds)
        self.btnSettingFolder = wx.BitmapButton(self, wx.ID_ANY, wx.Bitmap("Resources\\FolderButton2.bmp", wx.BITMAP_TYPE_ANY))
        self.btnAbout = wx.BitmapButton(self, wx.ID_ANY, wx.Bitmap("Resources\\AboutButton2.bmp", wx.BITMAP_TYPE_ANY))
        self.btnHelp = wx.BitmapButton(self, wx.ID_ANY, wx.Bitmap("Resources\\HelpButton2.bmp", wx.BITMAP_TYPE_ANY))
        self.panelIO = wx.Panel(self, wx.ID_ANY)
        self.panelVerify = wx.Panel(self, wx.ID_ANY)
        self.txtCtrlWBC = wx.TextCtrl(self, wx.ID_ANY, "\n", style=wx.TE_CENTRE | wx.TE_READONLY)
        self.btnOpenFile = wx.Button(self, wx.ID_ANY, "Open File", style=wx.BORDER_NONE)
        self.btnDetection = wx.Button(self, wx.ID_ANY, "Detect", style=wx.BORDER_NONE)
        self.btnVerify = wx.Button(self, wx.ID_ANY, "Verify", style=wx.BORDER_NONE)
        self.btnVerify.Disable()
        self.btnSave = wx.Button(self, wx.ID_ANY, "Save", style=wx.BORDER_NONE)
        self.btnSave.Disable()

        self.__set_properties()
        self.__do_layout()

        self.Bind(wx.EVT_BUTTON, self.btnSettingFolder_pressed, self.btnSettingFolder)
        self.Bind(wx.EVT_BUTTON, self.btnAbout_pressed, self.btnAbout)
        self.Bind(wx.EVT_BUTTON, self.btnHelp_pressed, self.btnHelp)
        self.Bind(wx.EVT_BUTTON, self.btnOpenFile_pressed, self.btnOpenFile)
        self.Bind(wx.EVT_BUTTON, self.btnDetection_pressed, self.btnDetection)
        self.Bind(wx.EVT_BUTTON, self.btnVerify_pressed, self.btnVerify)
        self.Bind(wx.EVT_BUTTON, self.btnSave_pressed, self.btnSave)

        # DEFINE IMG PATH
        self.img_path = ''

    def __set_properties(self):
        # MainFrame.__set_properties
        self.SetTitle("Unstained WBC Detection")
        _icon = wx.NullIcon
        _icon.CopyFromBitmap(wx.Bitmap("Resources\\IconProgram.png", wx.BITMAP_TYPE_ANY))
        self.SetIcon(_icon)
        self.SetBackgroundColour(wx.Colour(204, 50, 50))
        self.btnSettingFolder.SetToolTip("Setting Folder")
        self.btnSettingFolder.SetSize(self.btnSettingFolder.GetBestSize())
        self.btnAbout.SetBackgroundColour(wx.Colour(47, 47, 47))
        self.btnAbout.SetToolTip("About")
        self.btnAbout.SetSize(self.btnAbout.GetBestSize())
        self.btnHelp.SetToolTip("Help")
        self.btnHelp.SetSize(self.btnHelp.GetBestSize())
        self.panelIO.SetMinSize((600, 450))
        self.panelIO.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panelVerify.SetMinSize((600, 450))
        self.panelVerify.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.txtCtrlWBC.SetMinSize((50, 22))
        self.txtCtrlWBC.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.btnOpenFile.SetMinSize((295, 40))
        self.btnOpenFile.SetBackgroundColour(wx.Colour(47, 47, 47))
        self.btnOpenFile.SetForegroundColour(wx.Colour(255, 255, 255))
        self.btnOpenFile.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.btnOpenFile.SetToolTip("Input file citra")
        self.btnDetection.SetMinSize((295, 40))
        self.btnDetection.SetBackgroundColour(wx.Colour(47, 47, 47))
        self.btnDetection.SetForegroundColour(wx.Colour(255, 255, 255))
        self.btnDetection.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.btnDetection.SetToolTip("Deteksi sel darah putih")
        self.btnVerify.SetMinSize((295, 40))
        self.btnVerify.SetBackgroundColour(wx.Colour(47, 47, 47))
        self.btnVerify.SetForegroundColour(wx.Colour(255, 255, 255))
        self.btnVerify.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.btnVerify.SetToolTip("Verifikasi hasil deteksi")
        self.btnSave.SetMinSize((295, 40))
        self.btnSave.SetBackgroundColour(wx.Colour(47, 47, 47))
        self.btnSave.SetForegroundColour(wx.Colour(255, 255, 255))
        self.btnSave.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.btnSave.SetToolTip("Simpan hasil deteksi")

    def __do_layout(self):
        # MainFrame.__do_layout
        sizer_24 = wx.BoxSizer(wx.VERTICAL)
        grid_sizer_1 = wx.FlexGridSizer(0, 4, 0, 0)
        sizer_26 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_1 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_2 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_4 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_6 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_5 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_4.Add((10, 20), 0, 0, 0)
        sizer_5.Add(self.btnSettingFolder, 0, wx.ALIGN_BOTTOM | wx.LEFT | wx.RIGHT, 5)
        sizer_4.Add(sizer_5, 1, wx.EXPAND, 0)
        bitmap_1 = wx.StaticBitmap(self, wx.ID_ANY, wx.Bitmap("Resources\\LogoProgram.png", wx.BITMAP_TYPE_ANY))
        sizer_4.Add(bitmap_1, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, 10)
        sizer_6.Add((43, 20), 0, 0, 0)
        sizer_6.Add(self.btnAbout, 0, wx.ALIGN_BOTTOM | wx.LEFT | wx.RIGHT, 5)
        sizer_6.Add(self.btnHelp, 0, wx.ALIGN_BOTTOM, 0)
        sizer_6.Add((10, 20), 0, 0, 0)
        sizer_4.Add(sizer_6, 1, wx.EXPAND, 0)
        sizer_24.Add(sizer_4, 1, wx.EXPAND, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        grid_sizer_1.Add((20, 10), 0, 0, 0)
        grid_sizer_1.Add((20, 10), 0, 0, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        sizer_3.Add((0, 0), 0, 0, 0)
        sizer_3.Add((0, 0), 0, 0, 0)
        sizer_3.Add((0, 0), 0, 0, 0)
        grid_sizer_1.Add(sizer_3, 1, wx.EXPAND, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        grid_sizer_1.Add((10, 20), 0, 0, 0)
        grid_sizer_1.Add(self.panelIO, 1, wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
        grid_sizer_1.Add(self.panelVerify, 1, wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
        grid_sizer_1.Add((10, 20), 0, 0, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        label_4 = wx.StaticText(self, wx.ID_ANY, "Unstained WBC Image  |", style=wx.ALIGN_RIGHT)
        label_4.SetForegroundColour(wx.Colour(255, 255, 255))
        label_4.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        sizer_2.Add(label_4, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.LEFT, 114)
        label_1 = wx.StaticText(self, wx.ID_ANY, "Detected WBC:", style=wx.ALIGN_RIGHT)
        label_1.SetForegroundColour(wx.Colour(255, 255, 255))
        label_1.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        sizer_2.Add(label_1, 0, wx.ALIGN_RIGHT | wx.LEFT | wx.RIGHT, 5)
        sizer_2.Add(self.txtCtrlWBC, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, 5)
        grid_sizer_1.Add(sizer_2, 1, wx.EXPAND, 0)
        label_5 = wx.StaticText(self, wx.ID_ANY, "Stained WBC Image", style=wx.ALIGN_CENTER)
        label_5.SetForegroundColour(wx.Colour(255, 255, 255))
        label_5.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_1.Add(label_5, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        sizer_1.Add(self.btnOpenFile, 0, wx.ALIGN_CENTER | wx.ALL, 5)
        sizer_1.Add(self.btnDetection, 0, wx.ALL, 5)
        grid_sizer_1.Add(sizer_1, 1, wx.EXPAND, 0)
        sizer_26.Add(self.btnVerify, 0, wx.ALL, 5)
        sizer_26.Add(self.btnSave, 0, wx.ALL, 5)
        grid_sizer_1.Add(sizer_26, 1, wx.EXPAND, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        grid_sizer_1.Add((20, 10), 0, 0, 0)
        grid_sizer_1.Add((20, 10), 0, 0, 0)
        grid_sizer_1.Add((0, 0), 0, 0, 0)
        sizer_24.Add(grid_sizer_1, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_24)
        sizer_24.Fit(self)
        self.Layout()

    def btnSettingFolder_pressed(self, event):
        # OPEN FRAME SETTING
        try:
            frame = SettingFrame(None, wx.ID_ANY, "")
            frame.Show()
        except:
            event.Skip()

    def btnAbout_pressed(self, event):
        # OPEN FRAME ABOUT
        try:
            frame = AboutFrame(None, wx.ID_ANY, "")
            frame.Show()
        except:
            event.Skip()

    def btnHelp_pressed(self, event):
        # OPEN FRAME HELP
        try:
            frame = HelpFrame(None, wx.ID_ANY, "")
            frame.Show()
        except:
            event.Skip()

    def btnOpenFile_pressed(self, event):
        # ----------------------------------------------------------------------
        # -------------------- OPEN AND READ IMAGE FILE ------------------------
        # ----------------------------------------------------------------------
        #open file dialog
        tk.Tk().withdraw()
        self.img_path = filedialog.askopenfilename(title="Select Image",
                                              filetypes=[(".JPG", "*.jpg")])

        if self.img_path:
            self.btnSave.Disable()
            self.btnVerify.Disable()
            self.txtCtrlWBC.SetValue("")
            try:
                # BUAT TAMPILIN KE PANEL
                self.bmp = wx.Image(self.img_path,wx.BITMAP_TYPE_JPEG)
                (w, h) = self.bmp.GetSize() # AMBIL SIZE IMG
                (wp, hp) = self.panelIO.GetSize()

                # kondisi resize image
                if h > hp:
                    h = hp
                if w > wp:
                    w = wp

                # SCALE + CONVERT TO BITMAP
                self.bmp = self.bmp.Scale(w,h).ConvertToBitmap()

                # TARO KE PANEL
                self.obj = wx.StaticBitmap(self.panelIO, -1, self.bmp,(0,0),(wp, hp))
                self.obj.Refresh()

                # CLEAR VERIFY PANEL (setiap openfile, clear panel verify)
                (wp, hp) = self.panelVerify.GetSize()
                emptyImg = wx.Image(wp, hp, True) # create empty image (black)
                emptyImg.Replace(0,0,0,255,255,255) # replace ke 255 (white)
                self.obj = wx.StaticBitmap(self.panelVerify, -1, emptyImg.ConvertToBitmap(),(0,0),(wp, hp))
                self.obj.Refresh()

                # clear detailOutput
                detailOutput.clear()
            except:
                wx.MessageBox(message="Gagal input citra!",
                                      caption='Load Failed',
                                      style=wx.OK | wx.ICON_ERROR)

    def btnDetection_pressed(self, event):
        def detection(img_path):
            detailOutput.clear()
            try:
                img = cv2.imread(img_path, 1) # read img
                detailOutput.append(img) #DETAIL 0
                img = cv2.resize(img,None,fx=0.32, fy=0.32, interpolation = cv2.INTER_AREA) # resize img
                detectedWBC = img.copy()   # reserve for detected WBC area
##                cv2.imshow('original',img)

                # start timer
                timeExec = time.time()

                # ----------------------------------------------------------------------
                # ------------------------- GAMMA CORRECTION ---------------------------
                # ----------------------------------------------------------------------
                def adjust_gamma(image, gamma=1.0):
                    invGamma = 1.0 / gamma
                    table = np.array([((i / 255.0) ** invGamma) * 255
                                     for i in np.arange(0, 256)]).astype("uint8")

                    return cv2.LUT(image.astype(np.uint8), table.astype(np.uint8))

                gamma = 0.4   # ubah gamma value
                img = adjust_gamma(img, gamma=gamma)

                # ----------------------------------------------------------------------
                # ----------------------- Extracting Image Saturation n Filtering  -------------------------
                # ----------------------------------------------------------------------
                #-----Converting image to HSV Color model-----------------------------------
                hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

                #Split HSV
                h, s, v = cv2.split(hsv)

                #CLAHE pada Saturation
                clahe = cv2.createCLAHE(clipLimit=15.0, tileGridSize=(8,8))
                cl = clahe.apply(s)

                #Merge
                limg = cv2.merge((cl,s,v))

                #Convert to RGB
                final = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
                detailOutput.append(final.copy()) #DETAIL 1

                # ----------------------------------------------------------------------
                # ----------------------- PRE-PROCESSING IMAGE -------------------------
                # ----------------------------------------------------------------------
                # SMOOTHING AND SHARPENING
                final = cv2.GaussianBlur(final, (5,5), 0)
                detailOutput.append(final) #DETAIL 2
                kernel = np.array([[-1,-1,-1],
                                   [-1,9,-1],
                                   [-1,-1,-1]])
                final = cv2.filter2D(final, -1, kernel)
                detailOutput.append(final.copy()) #DETAIL 3

                # GRAYSCALE, THRESHOLD, MORPHOLOGICAL OPERATION
                gray = cv2.cvtColor(final, cv2.COLOR_BGR2GRAY)
                gray = cv2.GaussianBlur(gray, (7,7), 0)
                detailOutput.append(gray.copy()) #DETAIL 4
                ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_OTSU)

                # create copy untuk var global detail
                detailOutput.append(thresh.copy()) #DETAIL 5

                # Morph Operation
                kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
                ##kernel2 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
                ##opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
                ##close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel2)
                dilate = cv2.dilate(thresh,kernel,iterations = 1)
                tempMorph = dilate.copy()
##                cv2.imshow('Morph', tempMorph)

                # FILLING HOLES
                _,contour,hier = cv2.findContours(tempMorph,cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE)

                for cnt in contour:
                    cv2.drawContours(tempMorph,[cnt],0,255,-1)

                detailOutput.append(tempMorph.copy()) #DETAIL 6
##                cv2.imshow('Filling Holes', tempMorph)
##                cv2.waitKey(0)
##                cv2.destroyAllWindows()

                # ----------------------------------------------------------------------
                # ---------------------- WATERSHED SEGMENTATION ------------------------
                # ----------------------------------------------------------------------
                # REMOVE NOISE
                tempMorph = cv2.morphologyEx(tempMorph,cv2.MORPH_OPEN,kernel, iterations = 2)
##                cv2.imshow('Noise Removal', tempMorph)
##                cv2.waitKey(0)
##                cv2.destroyAllWindows()

                # DISTANCE TRANSFORM USING EUCLIDEAN DISTANCE
                D = ndimage.distance_transform_edt(tempMorph)
                localMax = peak_local_max(D, indices=False, min_distance=8, labels=thresh)

                # LABELLING USING CCA AND APPLY WATERSHED
                markers = ndimage.label(localMax, structure=np.ones((3, 3)))[0]
                labels = watershed(-D, markers, mask=tempMorph)
                print("[INFO] {} unique segments found".format(len(np.unique(labels)) - 1))

                # LOOPING UNTUK SETIAP LABEL WATERSHED
                for label in np.unique(labels):
                   # jika label 0 = background, skip.
                   if label == 0:
                       continue

                   # selain label 0, maka dianggap area segmen RBC dan dijadikan hitam (blackout area)
                   mask = np.zeros(gray.shape, dtype="uint8")
                   dilate[labels == label] = 0

                detailOutput.append(dilate.copy()) #DETAIL 7
                # output image
##                cv2.imshow("Output", dilate)
##                cv2.waitKey(0)

                # ----------------------------------------------------------------------
                # ------------------------- POST-PROCESSING ----------------------------
                # ----------------------------------------------------------------------
                # CLEANING
                tempClean = dilate.copy()
                kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(15,15))
                tempClean = cv2.dilate(tempClean, kernel)
                detailOutput.append(tempClean) #DETAIL 8
                ##tempClean = cv2.morphologyEx(tempClean, cv2.MORPH_OPEN, kernel)
##                cv2.imshow("Clean", tempClean)
##                cv2.waitKey(0)

                # CROP WBC AREA DAN TAMPILKAN PADA LAYAR
                _,contours,hier = cv2.findContours(tempClean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

                self.WBC_Count = 0
                self.croppedWBC = []
                self.xywh = []
                for cnt in contours:
                    temp = []
                    area = cv2.contourArea(cnt)
                    cv2.drawContours(img, cnt, -1, 255, 1)
                    x, y, w, h = cv2.boundingRect(cnt)
                    ratio = float(w)/h
                    if area < 4450 or area > 11400:
                      continue
                    if ratio < 0.5 or ratio > 2.3:
                      continue
                    self.croppedWBC.append(detectedWBC[y:y+h,x:x+w])
##                  cv2.imshow("Cropped WBC" + str(WBC_Count), self.croppedWBC[WBC_Count])
##                  cv2.drawContours(detectedWBC, cnt, -1, 255, 1)
                    cv2.rectangle(detectedWBC, (x,y), (x+w,y+h), (255,0,0), 1)
                    cv2.putText(detectedWBC, "Area={}".format(self.WBC_Count+1),(x,y-3),0,0.4,(255,0,0),1)
                    temp.extend([x,y,w,h])
                    self.xywh.append(temp)
                    self.WBC_Count += 1
                print(self.xywh)
                detailOutput.append(detectedWBC) #DETAIL 9
                # end timer
                timeExec = time.time() - timeExec

                #### save ####
##                cv2.imwrite("ValidNonWBC/HasilDeteksi/" + os.path.basename(self.img_path)[:-4] + ".jpg", detectedWBC)
                # TAMPILIN AREA WBC KE PANEL DAN TXT CTRL
                if self.WBC_Count > 0:
                    # SET NILAI txtCtrlWBC (menampilkan jumlah WBC yang terdeteksi)
                    self.txtCtrlWBC.SetValue(str(self.WBC_Count))

                    # MUNCULIN KE PANEL DAN RESIZE
                    (wPanel, hPanel) = self.panelIO.GetSize()
                    detectedWBC = cv2.resize(detectedWBC,(wPanel,hPanel), interpolation = cv2.INTER_AREA)
                    ####### save hasil deteksi ########
                    cv2.imwrite("Detection Output/" + os.path.basename(self.img_path)[:-4] + ".jpg", detectedWBC)
                    ###################################
                    detectedWBC = cv2.cvtColor(detectedWBC, cv2.COLOR_BGR2RGB)
                    wxImg = wx.ImageFromBuffer(detectedWBC.shape[1], detectedWBC.shape[0], detectedWBC)
##                    wxImg = wx.Bitmap.FromBuffer(detectedWBC.shape[1], detectedWBC.shape[0], detectedWBC)
##                    wxImg = wxImg.ConvertToImage()

##                    self.bmp = wx.Image(wxImg, wx.BITMAP_TYPE_ANY)
##                    self.bmp = self.bmp.Scale(wPanel,hPanel).ConvertToBitmap()
                    wxBitmap = wxImg.ConvertToBitmap()

                    # TARO KE PANEL
                    self.obj = wx.StaticBitmap(self.panelIO, -1, wxBitmap,(0,0),(wPanel, hPanel))
                    self.obj.Refresh()
                else:
                    self.txtCtrlWBC.SetValue("0")
                    wx.MessageBox(message="Sel darah putih tidak ditemukan.",
                              caption='WBC not found',
                              style=wx.OK | wx.ICON_INFORMATION)

##                cv2.imshow("Ouput Contours", img)
##                cv2.imshow("OuputWBC Contours", detectedWBC)
##                cv2.waitKey(0)
##                cv2.destroyAllWindows()
            except:
                wx.MessageBox(message="Deteksi gagal!",
                                      caption='Detection Failed',
                                      style=wx.OK | wx.ICON_ERROR)

            # MEASURE TIME
            img_name = os.path.basename(self.img_path)
            print("---- %s seconds ----" % (timeExec))
            timestr = time.strftime("%d-%m-%Y %H:%M:%S")
            file = open("time.txt", "a")
            file.write("Image file: " + img_name + "\n")
            file.write("Testing date: " + timestr + "\n")
            file.write("Detected WBC: " + str(self.WBC_Count) +"\n")
            file.write("Execution time: " + str(timeExec) + "\n---------------------------------------------\n")
            file.close()

        # ----------------------------------------------------------------------
        # ------------------------- START DETECTION ----------------------------
        # ----------------------------------------------------------------------
        if self.img_path:
            detection(self.img_path)
            self.btnVerify.Enable()
            if self.WBC_Count > 0:
                self.btnSave.Enable()
        else:
            wx.MessageBox(message="File citra tidak ditemukan!",
                                      caption='Masukkan Citra Input',
                                      style=wx.OK | wx.ICON_ERROR)

    def btnVerify_pressed(self, event):
        def image_resize(image, width = None, height = None, inter = cv2.INTER_AREA):
            dim = None
            (h, w) = image.shape[:2]
            if width is None and height is None:
                return image
            if width is None:
                r = height / float(h)
                dim = (int(w * r), height)
                print("W", dim)
                print(dim[0])
            else:
                r = width / float(w)
                dim = (width, int(h * r))
                print("h", dim)
                print(dim[0])
            resized = cv2.resize(image, dim, interpolation = inter)
            return resized

        try:
            # ambil hanya nama img (path paling akhir)
            img_name = os.path.basename(self.img_path)
            verify_path = saveVerify_path[1] + img_name

            # load image untuk kasi rectangle area
            verifyImg = cv2.imread(verify_path)
            for i in range(len(self.xywh)):
                cv2.rectangle(verifyImg, (self.xywh[i][0],self.xywh[i][1]),
                              (self.xywh[i][0]+self.xywh[i][2],
                               self.xywh[i][1]+self.xywh[i][3]), (255,0,0), 1)

            ## save verify image (ada box) ##
            cv2.imwrite("Detection Output/" + os.path.basename(self.img_path)[:-4] + "v.jpg", verifyImg)
            ####################
            verifyImg = cv2.cvtColor(verifyImg, cv2.COLOR_BGR2RGB)

            (wp, hp) = self.panelVerify.GetSize()
            (h, w, _) = verifyImg.shape # AMBIL SIZE IMG
            print (wp,hp, w, h)
            # kondisi resize image
            if h > hp:
                h = hp
            if w > wp:
                w = wp

            # klo pake img tanpa crop
##            verifyImg = cv2.resize(verifyImg, (w,h), interpolation = cv2.INTER_AREA)
            # klo pake img dengan crop
            verifyImg = image_resize(verifyImg, height = hp)
            (h, w, _) = verifyImg.shape
            if w > wp:
                verifyImg = image_resize(verifyImg, width = wp)

            wxBuffer = wx.ImageFromBuffer(verifyImg.shape[1], verifyImg.shape[0], verifyImg)
            # SCALE + CONVERT TO BITMAP
            wxBitmap = wxBuffer.ConvertToBitmap()
            # TARO KE PANEL
            self.obj = wx.StaticBitmap(self.panelVerify, -1, wxBitmap,(0,0),(wp, hp))
            self.obj.Refresh()
        except:
            wx.MessageBox(message="Citra untuk verifikasi tidak ditemukan!",
                                      caption='Image Verify Not Found',
                                      style=wx.OK | wx.ICON_ERROR)

    def btnSave_pressed(self, event):
        try:
            save_path = saveVerify_path[0]
            img_name = os.path.basename(self.img_path) # ambil nama img (path paling belakang)
            for i in range(self.WBC_Count): # save cropped area
                cv2.imwrite(save_path + img_name + "_" + str(i) + ".jpg", self.croppedWBC[i])

            # POP-UP success
            wx.MessageBox(message="Area deteksi berhasil disimpan: " + save_path,
                                  caption='Save Success',
                                  style=wx.OK | wx.ICON_INFORMATION)
        except:
            # POP-UP fail
            wx.MessageBox(message="Gagal menyimpan hasil deteksi!",
                                  caption='Save Failed',
                                  style=wx.OK | wx.ICON_ERROR)

# end of class MainFrame

class AboutFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # AboutFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.SYSTEM_MENU
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((700, 360))
        self.AboutOK = wx.Button(self, wx.ID_ANY, "OK")

        self.__set_properties()
        self.__do_layout()

        self.Bind(wx.EVT_BUTTON, self.btnAboutOK_pressed, self.AboutOK)

    def __set_properties(self):
        # AboutFrame.__set_properties
        self.SetTitle("About Unstained WBC Detection")
        _icon = wx.NullIcon
        _icon.CopyFromBitmap(wx.Bitmap("Resources\\AboutButton2.bmp", wx.BITMAP_TYPE_ANY))
        self.SetIcon(_icon)
        self.SetBackgroundColour(wx.Colour(47, 47, 47))
        self.AboutOK.SetMinSize((200, 26))
        self.AboutOK.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.AboutOK.SetToolTip("Back to Home")

    def __do_layout(self):
        # AboutFrame.__do_layout
        sizer_27 = wx.BoxSizer(wx.VERTICAL)
        sizer_28 = wx.BoxSizer(wx.VERTICAL)
        grid_sizer_2 = wx.GridSizer(1, 1, 0, 0)
        label_6 = wx.StaticText(self, wx.ID_ANY, "UNSTAINED WHITE BLOOD CELL DETECTION\nVersion 2.1.0 (201118) Release", style=wx.ALIGN_CENTER)
        label_6.SetBackgroundColour(wx.Colour(47, 47, 47))
        label_6.SetForegroundColour(wx.Colour(0, 127, 255))
        label_6.SetFont(wx.Font(16, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift"))
        sizer_28.Add(label_6, 0, wx.ALIGN_CENTER | wx.ALL | wx.EXPAND, 20)
        label_7 = wx.StaticText(self, wx.ID_ANY, "Program pendeteksian sel darah putih pada citra \npreparat tanpa pewarnaan (unstained).", style=wx.ALIGN_CENTER)
        label_7.SetBackgroundColour(wx.Colour(47, 47, 47))
        label_7.SetForegroundColour(wx.Colour(255, 255, 255))
        label_7.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift"))
        sizer_28.Add(label_7, 0, wx.ALIGN_CENTER | wx.ALL, 5)
        static_line_1 = wx.StaticLine(self, wx.ID_ANY, style=wx.LI_VERTICAL)
        static_line_1.SetMinSize((550, 2))
        sizer_28.Add(static_line_1, 0, wx.ALIGN_CENTER | wx.ALL, 10)
        label_11 = wx.StaticText(self, wx.ID_ANY, "David Reynaldo\nLina", style=wx.ALIGN_CENTER)
        label_11.SetBackgroundColour(wx.Colour(47, 47, 47))
        label_11.SetForegroundColour(wx.Colour(255, 255, 255))
        label_11.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift"))
        grid_sizer_2.Add(label_11, 0, wx.ALIGN_CENTER | wx.EXPAND | wx.LEFT | wx.RIGHT, 30)
        sizer_28.Add(grid_sizer_2, 0, wx.EXPAND, 0)
        label_13 = wx.StaticText(self, wx.ID_ANY, "Fakultas Teknologi Informasi - Teknik Informatika\nUniversitas Tarumanagara (2018)", style=wx.ALIGN_CENTER)
        label_13.SetBackgroundColour(wx.Colour(47, 47, 47))
        label_13.SetForegroundColour(wx.Colour(255, 255, 255))
        label_13.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift"))
        sizer_28.Add(label_13, 0, wx.ALIGN_CENTER | wx.ALL, 10)
        sizer_28.Add(self.AboutOK, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT | wx.TOP, 10)
        sizer_27.Add(sizer_28, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_27)
        self.Layout()

    def btnAboutOK_pressed(self, event):
        self.Close()

# end of class AboutFrame

class HelpFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # HelpFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.btnDetail = wx.Button(self, wx.ID_ANY, "Detail Program")
        self.HelpOK = wx.Button(self, wx.ID_ANY, "OK")

        self.__set_properties()
        self.__do_layout()
        self.cekDeteksi()

        self.Bind(wx.EVT_BUTTON, self.btnDetail_pressed, self.btnDetail)
        self.Bind(wx.EVT_BUTTON, self.btnHelpOK_pressed, self.HelpOK)

    def cekDeteksi(self):
        if not detailOutput:
            self.btnDetail.Disable()
        else:
            self.btnDetail.Enable()

    def __set_properties(self):
        # HelpFrame.__set_properties
        self.SetTitle("Help")
        _icon = wx.NullIcon
        _icon.CopyFromBitmap(wx.Bitmap("Resources\\HelpButton2.bmp", wx.BITMAP_TYPE_ANY))
        self.SetIcon(_icon)
        self.SetBackgroundColour(wx.Colour(47, 47, 47))
        self.btnDetail.SetMinSize((200, 26))
        self.btnDetail.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.btnDetail.SetToolTip("Detail Program")
        self.HelpOK.SetMinSize((200, 26))
        self.HelpOK.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.HelpOK.SetToolTip("Back to Home")

    def __do_layout(self):
        # HelpFrame.__do_layout
        sizer_30 = wx.BoxSizer(wx.VERTICAL)
        lblHelp1 = wx.StaticText(self, wx.ID_ANY, "Petunjuk Penggunaan Program", style=wx.ALIGN_CENTER)
        lblHelp1.SetBackgroundColour(wx.Colour(47, 47, 47))
        lblHelp1.SetForegroundColour(wx.Colour(0, 127, 255))
        lblHelp1.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        sizer_30.Add(lblHelp1, 0, wx.ALIGN_CENTER | wx.ALL | wx.EXPAND, 20)
        static_line_2 = wx.StaticLine(self, wx.ID_ANY)
        sizer_30.Add(static_line_2, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 40)
        lblHelp2 = wx.StaticText(self, wx.ID_ANY, "Informasi tombol:\n- Open File\t: Memilih file citra yang ingin dideteksi\n- Detect\t\t: Proses pendeteksian sel darah putih\n- Verify\t\t: Verifikasi hasil deteksi\n- Save\t\t: Menyimpan citra output\n- About\t\t: Informasi mengenai program\n- Help\t\t: Petunjuk penggunaan program\n- Folder Setting\t: Pengaturan lokasi folder verify/save\n\nTutorial menggunakan program:\n1. Tekan tombol 'Open File' dan pilih file citra preparat sel darah tanpa pewarnaan (.jpg) yang ingin dideteksi.\n2. Kemudian tekan tombol 'Detect', maka area hasil deteksi sel darah putih akan ditampilkan.\n3. Tekan tombol verify untuk mencocokkan apakah area yang terdeteksi benar atau tidak.\n4. Tekan tombol save jika ingin menyimpan citra hasil deteksi (output).\n\nDetail program:", style=wx.ST_NO_AUTORESIZE)
        lblHelp2.SetBackgroundColour(wx.Colour(47, 47, 47))
        lblHelp2.SetForegroundColour(wx.Colour(255, 255, 255))
        lblHelp2.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.NORMAL, 0, "Bahnschrift"))
        sizer_30.Add(lblHelp2, 5, wx.LEFT | wx.RIGHT | wx.TOP, 20)
        sizer_30.Add(self.btnDetail, 0, wx.LEFT | wx.RIGHT, 20)
        lblDetail = wx.StaticText(self, wx.ID_ANY, "*Lakukan deteksi terlebih dahulu untuk melihat tahapan pemrosesan program secara detail", style=wx.ST_NO_AUTORESIZE)
        lblDetail.SetBackgroundColour(wx.Colour(47, 47, 47))
        lblDetail.SetForegroundColour(wx.Colour(255, 255, 255))
        lblDetail.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.NORMAL, 0, "Bahnschrift"))
        sizer_30.Add(lblDetail, 0, wx.BOTTOM | wx.LEFT | wx.RIGHT, 20)
        sizer_30.Add(self.HelpOK, 0, wx.ALIGN_CENTER | wx.BOTTOM | wx.LEFT | wx.RIGHT, 20)
        self.SetSizer(sizer_30)
        sizer_30.Fit(self)
        self.Layout()

    def btnDetail_pressed(self, event):
        detailFrame = DetailFrame(None, wx.ID_ANY, "")
        detailFrame.Show()

    def btnHelpOK_pressed(self, event):
        self.Close()

# end of class HelpFrame

class SettingFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # SettingFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.SYSTEM_MENU
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((665, 335))
        self.text_ctrl_1 = wx.TextCtrl(self, wx.ID_ANY, saveVerify_path[0])
        self.btnSelectSave = wx.Button(self, wx.ID_ANY, "Select")
        self.text_ctrl_2 = wx.TextCtrl(self, wx.ID_ANY, saveVerify_path[1])
        self.btnSelectVerify = wx.Button(self, wx.ID_ANY, "Select")
        self.btnApply = wx.Button(self, wx.ID_ANY, "Apply")

        self.__set_properties()
        self.__do_layout()

        self.Bind(wx.EVT_BUTTON, self.btnSelectSave_pressed, self.btnSelectSave)
        self.Bind(wx.EVT_BUTTON, self.btnSelectVerify_pressed, self.btnSelectVerify)
        self.Bind(wx.EVT_BUTTON, self.btnApply_pressed, self.btnApply)

    def __set_properties(self):
        # SettingFrame.__set_properties
        self.SetTitle("Folder Setting")
        _icon = wx.NullIcon
        _icon.CopyFromBitmap(wx.Bitmap("Resources\\FolderBtn.bmp", wx.BITMAP_TYPE_ANY))
        self.SetIcon(_icon)
        self.SetBackgroundColour(wx.Colour(47, 47, 47))
        self.text_ctrl_1.SetMinSize((450, 30))
        self.text_ctrl_1.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.NORMAL, 0, "Bahnschrift"))
        self.btnSelectSave.SetMinSize((110, 30))
        self.btnSelectSave.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.text_ctrl_2.SetMinSize((450, 30))
        self.text_ctrl_2.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.NORMAL, 0, "Bahnschrift"))
        self.btnSelectVerify.SetMinSize((110, 30))
        self.btnSelectVerify.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.btnApply.SetMinSize((250, 30))
        self.btnApply.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))

    def __do_layout(self):
        # SettingFrame.__do_layout
        sizer_7 = wx.BoxSizer(wx.VERTICAL)
        sizer_8 = wx.BoxSizer(wx.VERTICAL)
        sizer_10 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_9 = wx.BoxSizer(wx.HORIZONTAL)
        label_6 = wx.StaticText(self, wx.ID_ANY, "Pengaturan Folder", style=wx.ALIGN_CENTER)
        label_6.SetBackgroundColour(wx.Colour(47, 47, 47))
        label_6.SetForegroundColour(wx.Colour(0, 127, 255))
        label_6.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift"))
        sizer_8.Add(label_6, 0, wx.ALIGN_CENTER | wx.ALL | wx.EXPAND, 20)
        static_line_2 = wx.StaticLine(self, wx.ID_ANY)
        sizer_8.Add(static_line_2, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 40)
        label_7 = wx.StaticText(self, wx.ID_ANY, "\nSave folder path :")
        label_7.SetBackgroundColour(wx.Colour(47, 47, 47))
        label_7.SetForegroundColour(wx.Colour(255, 255, 255))
        label_7.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift"))
        sizer_8.Add(label_7, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 40)
        sizer_9.Add((30, 20), 0, 0, 0)
        sizer_9.Add(self.text_ctrl_1, 0, wx.LEFT | wx.RIGHT, 10)
        sizer_9.Add(self.btnSelectSave, 0, 0, 0)
        sizer_8.Add(sizer_9, 1, wx.EXPAND, 0)
        label_8 = wx.StaticText(self, wx.ID_ANY, "Verify folder path :")
        label_8.SetBackgroundColour(wx.Colour(47, 47, 47))
        label_8.SetForegroundColour(wx.Colour(255, 255, 255))
        label_8.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift"))
        sizer_8.Add(label_8, 0, wx.LEFT | wx.RIGHT, 40)
        sizer_10.Add((30, 20), 0, 0, 0)
        sizer_10.Add(self.text_ctrl_2, 0, wx.LEFT | wx.RIGHT, 10)
        sizer_10.Add(self.btnSelectVerify, 0, 0, 0)
        sizer_8.Add(sizer_10, 1, wx.EXPAND, 0)
        sizer_8.Add(self.btnApply, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, 10)
        sizer_8.Add((20, 15), 0, 0, 0)
        sizer_7.Add(sizer_8, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_7)
        self.Layout()

    def btnSelectSave_pressed(self, event):
        try:
            tk.Tk().withdraw()
            save_dir = filedialog.askdirectory(title="Select Save Folder")
            if save_dir:
                save_dir = save_dir + "/"
                self.text_ctrl_1.SetValue(save_dir)
        except:
            event.Skip()

    def btnSelectVerify_pressed(self, event):
        try:
            tk.Tk().withdraw()
            verify_dir = filedialog.askdirectory(title="Select Verify Image Folder")
            if verify_dir:
                verify_dir = verify_dir + "/"
                self.text_ctrl_2.SetValue(verify_dir)
        except:
            event.Skip()

    def btnApply_pressed(self, event):
        try:
            # buka dan read data path.config
            with open(filename, 'r') as file:
                data = file.readlines()

            # ubah path sesuai txtctrl
            data[0] = self.text_ctrl_1.GetValue() + "\n"
            data[1] = self.text_ctrl_2.GetValue()

            # overwrite path.config file
            with open(filename, 'w') as file:
                file.writelines(data)

            # update saveVerify_path global
            saveVerify_path[0] = data[0].rstrip()
            saveVerify_path[1] = data[1].rstrip()
        except:
            event.Skip()

        # tutup window
        self.Close()

# end of class SettingFrame

class DetailFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # DetailFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.SYSTEM_MENU
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((1376, 635))
        self.panel_1 = wx.Panel(self, wx.ID_ANY)
        self.panel_2 = wx.Panel(self, wx.ID_ANY)
        self.panel_3 = wx.Panel(self, wx.ID_ANY)
        self.panel_4 = wx.Panel(self, wx.ID_ANY)
        self.panel_5 = wx.Panel(self, wx.ID_ANY)
        self.panel_6 = wx.Panel(self, wx.ID_ANY)
        self.panel_7 = wx.Panel(self, wx.ID_ANY)
        self.panel_8 = wx.Panel(self, wx.ID_ANY)
        self.panel_9 = wx.Panel(self, wx.ID_ANY)
        self.panel_10 = wx.Panel(self, wx.ID_ANY)
        self.panel_13 = wx.Panel(self, wx.ID_ANY)
        self.btnBack = wx.Button(self, wx.ID_ANY, "< Back", style=wx.BORDER_NONE)
        self.panel_14 = wx.Panel(self, wx.ID_ANY)

        self.__set_properties()
        self.__do_layout()

        self.Bind(wx.EVT_BUTTON, self.btnBack_pressed, self.btnBack)

        # update panel (munculin semua image)
        self.updatePanel()

    def updatePanel(self):
        # convert semua detailoutput ke bitmap
        imgPanel = []
        (wp, hp) = self.panel_1.GetSize()
        for i in range(len(detailOutput)):
            if ((len(detailOutput[i].shape)) == 2): # cek apa img binary/bgr
                temp = cv2.cvtColor(detailOutput[i], cv2.COLOR_GRAY2RGB) # convert
            else:
                temp = cv2.cvtColor(detailOutput[i], cv2.COLOR_BGR2RGB)
            temp = cv2.resize(temp,(wp,hp), interpolation = cv2.INTER_AREA)
            temp = wx.ImageFromBuffer(temp.shape[1], temp.shape[0], temp)
            temp = temp.ConvertToBitmap()
            imgPanel.append(temp) # append list

        print(imgPanel)

        self.obj = wx.StaticBitmap(self.panel_1, -1, imgPanel[0],(0,0),(wp, hp))
        self.obj = wx.StaticBitmap(self.panel_2, -1, imgPanel[1],(0,0),(wp, hp))
        self.obj = wx.StaticBitmap(self.panel_3, -1, imgPanel[2],(0,0),(wp, hp))
        self.obj = wx.StaticBitmap(self.panel_4, -1, imgPanel[3],(0,0),(wp, hp))
        self.obj = wx.StaticBitmap(self.panel_5, -1, imgPanel[4],(0,0),(wp, hp))
        self.obj = wx.StaticBitmap(self.panel_6, -1, imgPanel[5],(0,0),(wp, hp))
        self.obj = wx.StaticBitmap(self.panel_7, -1, imgPanel[6],(0,0),(wp, hp))
        self.obj = wx.StaticBitmap(self.panel_8, -1, imgPanel[7],(0,0),(wp, hp))
        self.obj = wx.StaticBitmap(self.panel_9, -1, imgPanel[8],(0,0),(wp, hp))
        self.obj = wx.StaticBitmap(self.panel_10, -1, imgPanel[9],(0,0),(wp, hp))
        self.obj.Refresh()

    def __set_properties(self):
        # DetailFrame.__set_properties
        self.SetTitle("Detail")
        self.SetBackgroundColour(wx.Colour(165, 42, 42))
        self.panel_1.SetMinSize((260, 195))
        self.panel_1.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panel_2.SetMinSize((260, 195))
        self.panel_2.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panel_3.SetMinSize((260, 195))
        self.panel_3.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panel_4.SetMinSize((260, 195))
        self.panel_4.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panel_5.SetMinSize((260, 195))
        self.panel_5.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panel_6.SetMinSize((260, 195))
        self.panel_6.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panel_7.SetMinSize((260, 195))
        self.panel_7.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panel_8.SetMinSize((260, 195))
        self.panel_8.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panel_9.SetMinSize((260, 195))
        self.panel_9.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.panel_10.SetMinSize((260, 195))
        self.panel_10.SetBackgroundColour(wx.Colour(255, 255, 255))
        self.btnBack.SetMinSize((600, 30))
        self.btnBack.SetBackgroundColour(wx.Colour(47, 47, 47))
        self.btnBack.SetForegroundColour(wx.Colour(255, 255, 255))
        self.btnBack.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        self.btnBack.SetToolTip("Back")

    def __do_layout(self):
        # DetailFrame.__do_layout
        sizer_11 = wx.BoxSizer(wx.VERTICAL)
        sizer_12 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_13 = wx.BoxSizer(wx.VERTICAL)
        sizer_15 = wx.BoxSizer(wx.HORIZONTAL)
        grid_sizer_3 = wx.FlexGridSizer(0, 5, 0, 0)
        sizer_12.Add((10, 20), 0, 0, 0)
        sizer_13.Add((20, 10), 0, 0, 0)
        label_6 = wx.StaticText(self, wx.ID_ANY, "Detail Program", style=wx.ALIGN_CENTER)
        label_6.SetBackgroundColour(wx.Colour(165, 42, 42))
        label_6.SetForegroundColour(wx.Colour(255, 255, 255))
        label_6.SetFont(wx.Font(20, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift"))
        sizer_13.Add(label_6, 0, wx.ALIGN_CENTER | wx.ALL | wx.EXPAND, 20)
        grid_sizer_3.Add(self.panel_1, 1, wx.BOTTOM | wx.EXPAND | wx.RIGHT | wx.TOP, 5)
        grid_sizer_3.Add(self.panel_2, 1, wx.ALL | wx.EXPAND, 5)
        grid_sizer_3.Add(self.panel_3, 1, wx.ALL | wx.EXPAND, 5)
        grid_sizer_3.Add(self.panel_4, 1, wx.ALL | wx.EXPAND, 5)
        grid_sizer_3.Add(self.panel_5, 1, wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.TOP, 5)
        label_5 = wx.StaticText(self, wx.ID_ANY, "1. Input Image", style=wx.ALIGN_CENTER)
        label_5.SetForegroundColour(wx.Colour(255, 255, 255))
        label_5.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_5, 0, wx.EXPAND, 5)
        label_7 = wx.StaticText(self, wx.ID_ANY, "2. Saturation Value", style=wx.ALIGN_CENTER)
        label_7.SetForegroundColour(wx.Colour(255, 255, 255))
        label_7.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_7, 0, wx.EXPAND, 5)
        label_8 = wx.StaticText(self, wx.ID_ANY, "3. Smoothing", style=wx.ALIGN_CENTER)
        label_8.SetForegroundColour(wx.Colour(255, 255, 255))
        label_8.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_8, 0, wx.EXPAND, 5)
        label_9 = wx.StaticText(self, wx.ID_ANY, "4. Sharpening", style=wx.ALIGN_CENTER)
        label_9.SetForegroundColour(wx.Colour(255, 255, 255))
        label_9.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_9, 0, wx.EXPAND, 5)
        label_10 = wx.StaticText(self, wx.ID_ANY, "5. Grayscale ", style=wx.ALIGN_CENTER)
        label_10.SetForegroundColour(wx.Colour(255, 255, 255))
        label_10.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_10, 0, wx.EXPAND, 5)
        grid_sizer_3.Add(self.panel_6, 1, wx.BOTTOM | wx.EXPAND | wx.RIGHT | wx.TOP, 5)
        grid_sizer_3.Add(self.panel_7, 1, wx.ALL | wx.EXPAND, 5)
        grid_sizer_3.Add(self.panel_8, 1, wx.ALL | wx.EXPAND, 5)
        grid_sizer_3.Add(self.panel_9, 1, wx.ALL | wx.EXPAND, 5)
        grid_sizer_3.Add(self.panel_10, 1, wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.TOP, 5)
        label_11 = wx.StaticText(self, wx.ID_ANY, "6. Otsu Threshold", style=wx.ALIGN_CENTER)
        label_11.SetForegroundColour(wx.Colour(255, 255, 255))
        label_11.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_11, 0, wx.EXPAND, 5)
        label_12 = wx.StaticText(self, wx.ID_ANY, "7. Morphological Operation", style=wx.ALIGN_CENTER)
        label_12.SetForegroundColour(wx.Colour(255, 255, 255))
        label_12.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_12, 0, wx.EXPAND, 5)
        label_13 = wx.StaticText(self, wx.ID_ANY, "8. Watershed", style=wx.ALIGN_CENTER)
        label_13.SetForegroundColour(wx.Colour(255, 255, 255))
        label_13.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_13, 0, wx.BOTTOM | wx.EXPAND, 5)
        label_14 = wx.StaticText(self, wx.ID_ANY, "9. Dilation", style=wx.ALIGN_CENTER)
        label_14.SetForegroundColour(wx.Colour(255, 255, 255))
        label_14.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_14, 0, wx.EXPAND, 5)
        label_15 = wx.StaticText(self, wx.ID_ANY, "10. Detection", style=wx.ALIGN_CENTER)
        label_15.SetForegroundColour(wx.Colour(255, 255, 255))
        label_15.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "Bahnschrift SemiBold"))
        grid_sizer_3.Add(label_15, 0, wx.EXPAND, 5)
        sizer_13.Add(grid_sizer_3, 1, wx.EXPAND, 0)
        sizer_15.Add(self.panel_13, 1, 0, 0)
        sizer_15.Add(self.btnBack, 0, wx.TOP, 10)
        sizer_15.Add(self.panel_14, 1, 0, 0)
        sizer_13.Add(sizer_15, 1, wx.EXPAND, 0)
        sizer_13.Add((20, 10), 0, 0, 0)
        sizer_12.Add(sizer_13, 1, wx.EXPAND, 0)
        sizer_12.Add((10, 20), 0, 0, 0)
        sizer_11.Add(sizer_12, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_11)
        self.Layout()

    def btnBack_pressed(self, event):
        self.Close()

# end of class DetailFrame

class MyApp(wx.App):
    def OnInit(self):
        self.locale = wx.Locale(wx.LANGUAGE_ENGLISH)
        self.frame = MainFrame(None, wx.ID_ANY, "")
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

# end of class MyApp

if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()


# Copyright (c) 2017 The Khronos Group Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import division, print_function, absolute_import

import copy
import typing
from functools import partial

import numpy as np
import six

from nnef_tools.conversion import converter
from nnef_tools.conversion import transforms
from nnef_tools.core import graph_utils
from nnef_tools.core import utils
from nnef_tools.io.nnef.nnef_graph import *
from nnef_tools.io.nnef.parser_config import NNEFParserConfig
from nnef_tools.io.onnx.onnx_graph import *
from nnef_tools.io.onnx.onnx_io import NumpyDTypeByONNXDType
from nnef_tools.shape_inference import shape_inference as infer

_onnx_dtype_by_numpy_dtype = {str(np.dtype(v)): k
                              for k, v in six.iteritems(NumpyDTypeByONNXDType)
                              if v is not None}

_onnx_dtype_by_nnef_dtype = {
    'scalar': 'FLOAT',
    'integer': 'INT64',
    'logical': 'BOOL',
}

_numpy_dtype_by_nnef_dtype = {
    'scalar': 'float32',
    'integer': 'int64',
    'logical': 'bool',
}

_onnx_pad_mode_by_nnef_border = {'constant': 'constant',
                                 'reflect': 'reflect',
                                 'replicate': 'edge'}

class Converter(converter.Converter[NNEFTensor, NNEFOperation, NNEFGraph,
                                    ONNXTensor, ONNXOperation, ONNXGraph]):

    def __init__(self,
                 custom_converter_by_op_name=None,
                 enable_default_conversion=False,
                 enable_imprecise_image_resize=False):
        converters = {}
        converters.update(_DefaultConverters)
        if custom_converter_by_op_name is not None:
            converters.update(custom_converter_by_op_name)
        default_op_converter = convert_default if enable_default_conversion else None

        super(Converter, self).__init__(op_converter_by_name=converters,
                                        default_op_converter=default_op_converter)

        self._enable_imprecise_image_resize = enable_imprecise_image_resize

    def create_graph(self, source_graph):
        # type: (NNEFGraph)->ONNXGraph

        return ONNXGraph(name=source_graph.name)

    def convert_tensor(self, source_tensor, target_graph):
        # type: (NNEFTensor, ONNXGraph)->ONNXTensor
        # TODO handle constants here
        return ONNXTensor(graph=target_graph,
                          name=source_tensor.name,
                          shape=list(source_tensor.shape),
                          dtype=(self.numpy_dtype_to_onnx_dtype(source_tensor.data.dtype)
                                 if isinstance(source_tensor.data, np.ndarray)
                                 else self.nnef_dtype_to_onnx_dtype(source_tensor.dtype)),
                          data=copy.copy(source_tensor.data))

    def convert_graph(self, source_graph):
        graph_utils.remove_unreachable(source_graph)
        target_graph = super(Converter, self).convert_graph(source_graph)  # type: NNEFGraph
        graph_utils.remove_unreachable(target_graph)
        target_graph.generate_missing_names()
        return target_graph

    @property
    def enable_imprecise_image_resize(self):
        return self._enable_imprecise_image_resize

    @staticmethod
    def numpy_dtype_to_onnx_dtype(numpy_dtype):
        return _onnx_dtype_by_numpy_dtype[str(np.dtype(numpy_dtype))]

    @staticmethod
    def nnef_dtype_to_onnx_dtype(nnef_dtype):
        return _onnx_dtype_by_nnef_dtype[nnef_dtype]

    @staticmethod
    def nnef_dtype_to_numpy_dtype(nnef_dtype):
        return np.dtype(_numpy_dtype_by_nnef_dtype[nnef_dtype])

    @staticmethod
    def add_squeeze(onnx_graph, onnx_tensor, axes, squeezed_tensor=None):
        # type: (ONNXGraph, ONNXTensor, typing.List[int], typing.Optional[ONNXTensor])->ONNXTensor
        if squeezed_tensor is None:
            squeezed_tensor = ONNXTensor(graph=onnx_graph,
                                         shape=transforms.squeezed_shape(onnx_tensor.shape, axes),
                                         dtype=onnx_tensor.dtype)
        return ONNXOperation(graph=onnx_graph,
                             name="Squeeze",
                             inputs=onnx_tensor,
                             attribs=dict(axes=axes),
                             outputs=squeezed_tensor).output

    @staticmethod
    def add_unsqueeze(onnx_graph, onnx_tensor, axes, unsqueezed_tensor=None):
        # type: (ONNXGraph, ONNXTensor, typing.List[int], typing.Optional[ONNXTensor])->ONNXTensor
        if unsqueezed_tensor is None:
            unsqueezed_tensor = ONNXTensor(graph=onnx_graph,
                                           shape=transforms.unsqueezed_shape(onnx_tensor.shape, axes),
                                           dtype=onnx_tensor.dtype)
        return ONNXOperation(graph=onnx_graph,
                             name="Unsqueeze",
                             inputs=onnx_tensor,
                             attribs=dict(axes=axes),
                             outputs=unsqueezed_tensor).output

    @staticmethod
    def add_expand(onnx_graph, onnx_tensor, shape):
        # type: (ONNXGraph, ONNXTensor, typing.List[int])->ONNXTensor
        return ONNXOperation(graph=onnx_graph,
                             name="Expand",
                             inputs=(onnx_tensor,
                                     ONNXTensor(graph=onnx_graph,
                                                shape=[4],
                                                dtype='INT64',
                                                data=list(shape))),
                             outputs=ONNXTensor(graph=onnx_graph,
                                                shape=list(shape),
                                                dtype=onnx_tensor.dtype)).output

    @staticmethod
    def onnx_pads(nnef_padding):
        # type: (typing.List[typing.Tuple[int, int]])->typing.List[int]
        return utils.concat_lists(utils.zip_inverse(2, nnef_padding))

    @staticmethod
    def onnx_pad_mode(nnef_border):
        assert nnef_border in _onnx_pad_mode_by_nnef_border
        return _onnx_pad_mode_by_nnef_border[nnef_border]

    @staticmethod
    def constant_1d_tensor(graph, list_, dtype):
        # type:(ONNXGraph,  typing.List[typing.Any], str)->ONNXTensor
        return ONNXTensor(graph=graph, shape=[len(list_)], dtype=dtype, data=list(list_))

    @staticmethod
    def constant_0d_tensor(graph, value, dtype):
        # type:(ONNXGraph,  typing.Any, str)->ONNXTensor
        return ONNXTensor(graph=graph, shape=[], dtype=dtype, data=[value])

def convert_default(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    print("Warning: Converter of {} is not implemented, doing default conversion.".format(nnef_op.name))

    def flatten(x):
        return utils.flatten(x) if isinstance(x, (list, tuple)) else x

    ONNXOperation(graph=onnx_graph,
                  name=nnef_op.name,
                  inputs=converter.converted_tensors(nnef_op.inputs),
                  attribs={k: flatten(v) for k, v in six.iteritems(nnef_op.attribs)},
                  outputs=converter.converted_tensors(nnef_op.outputs))

def UNSUPPORTED(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    if converter.default_op_converter is not None:
        converter.default_op_converter(converter, nnef_op, onnx_graph)
    else:
        raise utils.NNEFToolsException('NNEF to ONNX: Unsupported op: {}'.format(nnef_op.name))

def UNNEEDED(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    assert False, "This should not be called!"

def NONATOMIC(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    assert False, "This should not be called!"

def generic_convert_binary(converter, nnef_op, onnx_graph, target_name, broadcast_workaround=False):
    # type: (Converter, NNEFOperation, ONNXGraph, str, bool)->None

    if broadcast_workaround:
        x, y = converter.converted_tensors(nnef_op.inputs)
        z = converter.converted_tensor(nnef_op.output)

        # Caffe2 onnx backend cannot broadcast in Pow
        if x.shape != z.shape:
            x = converter.add_expand(onnx_graph, x, z.shape)
        if y.shape != z.shape:
            y = converter.add_expand(onnx_graph, y, z.shape)

        ONNXOperation(
            graph=onnx_graph,
            name=target_name,
            inputs=(x, y),
            outputs=z)
    else:
        x, y = converter.converted_tensors(nnef_op.inputs)
        z = converter.converted_tensor(nnef_op.output)

        ONNXOperation(
            graph=onnx_graph,
            name=target_name,
            inputs=(converter.add_unsqueeze(onnx_graph, x, list(range(x.rank, y.rank))) if 0 < x.rank < y.rank else x,
                    converter.add_unsqueeze(onnx_graph, y, list(range(y.rank, x.rank))) if 0 < y.rank < x.rank else y),
            outputs=z)

def generic_convert_unary(converter, nnef_op, onnx_graph, target_name, copy_attribs=None):
    # type: (Converter, NNEFOperation, ONNXGraph, str, typing.List[str])->None
    onnx_op = ONNXOperation(graph=onnx_graph,
                            name=target_name,
                            inputs=converter.converted_tensor(nnef_op.input),
                            outputs=converter.converted_tensor(nnef_op.output))
    if copy_attribs:
        for attr in copy_attribs:
            onnx_op.attribs[attr] = copy.deepcopy(nnef_op.attribs[attr])

def convert_lrn(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))
    nnef_size = nnef_op.attribs['size']

    assert len(nnef_size) >= 2 and all(dim == 1 or i == 1 for i, dim in enumerate(nnef_size)), \
        'Only channel LRN is supported'

    ONNXOperation(graph=onnx_graph,
                  name='LRN',
                  inputs=input,
                  outputs=output,
                  attribs=dict(size=nnef_size[1],
                               alpha=nnef_op.attribs['alpha'],
                               beta=nnef_op.attribs['beta'],
                               bias=nnef_op.attribs['bias']))

def convert_matmul(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None
    A, B = converter.converted_tensors(nnef_op.inputs)
    C = converter.converted_tensor(nnef_op.output)

    assert A.rank <= 2 and B.rank <= 2, "Batch matmul is unsupported in ONNX"

    ONNXOperation(graph=onnx_graph,
                  name='Gemm',
                  inputs=(A, B, converter.constant_0d_tensor(graph=onnx_graph, value=0.0, dtype=C.dtype)),
                  outputs=C,
                  attribs=dict(transA=1 if nnef_op.attribs['transposeA'] else 0,
                               transB=1 if nnef_op.attribs['transposeB'] else 0))

def convert_linear(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None
    A, B, C = converter.converted_tensors(nnef_op.inputs)
    D = converter.converted_tensor(nnef_op.output)

    assert A.rank <= 2 and B.rank <= 2 and C.rank <= 2, "Batch matmul is unsupported in ONNX"

    ONNXOperation(graph=onnx_graph,
                  name='Gemm',
                  inputs=(A, B, C),
                  outputs=D,
                  attribs=dict(transA=0, transB=1))

def partial_convert_conv_deconv(converter, nnef_op, onnx_graph, is_deconv, input, filter, bias, output):
    # type: (Converter, NNEFOperation, ONNXGraph, bool, ONNXTensor, ONNXTensor, ONNXTensor, ONNXTensor)->None

    strides = list(nnef_op.attribs['stride'])
    if not strides:
        strides = [1] * (input.rank - 2)

    dilations = nnef_op.attribs['dilation']
    if not dilations:
        dilations = [1] * (input.rank - 2)

    groups = nnef_op.attribs.get('groups', 1)  # default needed because box does not have groups
    if groups == 0:
        groups = input.shape[1]

    assert nnef_op.attribs['border'] == 'constant'

    if is_deconv:
        pads = nnef_op.attribs['padding']
        if not pads:  # auto pad
            calc_output_size = [i * s for i, s in (input.shape[2:], strides)]
            pads = infer.same_padding(upscaled_input=calc_output_size,
                                      filter=filter.shape[2:],
                                      stride=strides,
                                      dilation=dilations)
        else:
            calc_output_size = infer.conv(input=input.shape,
                                          filter=filter.shape[2:],
                                          padding=pads,
                                          stride=strides,
                                          dilation=dilations,
                                          groups=groups,
                                          output_channels=output.shape[1],
                                          format=infer.Format.NCHW,
                                          deconv=True)[2:]
        output_size = output.shape[2:]
        output_padding = [o - c for o, c in zip(output_size, calc_output_size)]
    else:
        pads = nnef_op.attribs['padding']
        if not pads:
            pads = infer.same_padding(upscaled_input=input.shape[2:],
                                      filter=filter.shape[2:],
                                      stride=strides,
                                      dilation=dilations)
        output_padding = [0] * len(pads)

    pads = converter.onnx_pads(pads)

    if bias.is_constant and bias.data == [0.0]:
        inputs = (input, filter)
    else:
        if bias.rank == 2:
            assert bias.shape[0] == 1
            bias = converter.add_squeeze(onnx_graph=onnx_graph, onnx_tensor=bias, axes=[0])
        inputs = (input, filter, bias)

    op = ONNXOperation(graph=onnx_graph,
                       name='ConvTranspose' if is_deconv else 'Conv',
                       inputs=inputs,
                       attribs=dict(
                           kernel_shape=filter.shape[2:],  # Not mandatory, but Caffe2 fails without this
                           strides=strides,
                           dilations=dilations,
                           pads=pads,
                           group=groups),
                       outputs=output)

    if is_deconv:
        op.attribs['output_padding'] = output_padding

def generic_convert_conv_deconv(converter, nnef_op, onnx_graph, is_deconv):
    # type: (Converter, NNEFOperation, ONNXGraph, bool)->None

    input, filter, bias = converter.converted_tensors(nnef_op.inputs)
    output = converter.converted_tensor(nnef_op.output)
    partial_convert_conv_deconv(converter, nnef_op, onnx_graph, is_deconv, input, filter, bias, output)

def convert_box(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    if nnef_op.attribs['size'] == [1] * input.rank:
        onnx_op = ONNXOperation(graph=onnx_graph,
                                name='Pad',
                                inputs=input,
                                attribs=dict(mode=converter.onnx_pad_mode(nnef_op.attribs['border']),
                                             pads=converter.onnx_pads(nnef_op.attribs['padding'])),
                                outputs=output)

        if onnx_op.attribs['mode'] == 'constant':
            onnx_op.attribs['value'] = 0.0
        return

    if nnef_op.attribs['normalize']:
        partial_convert_pool(converter, nnef_op, onnx_graph,
                             target_name='AveragePool', input=input, outputs=(output,))
    else:
        temporary = ONNXTensor(graph=onnx_graph, shape=list(output.shape), dtype=output.dtype)
        partial_convert_pool(converter, nnef_op, onnx_graph,
                             target_name='AveragePool', input=input, outputs=(temporary,), force_constant=True)
        ONNXOperation(
            graph=onnx_graph,
            name='Mul',
            inputs=(temporary,
                    converter.constant_0d_tensor(onnx_graph, float(np.product(nnef_op.attribs['size'])), 'FLOAT')),
            outputs=output)

def generic_convert_pool(converter, nnef_op, onnx_graph, target_name):
    # type: (Converter, NNEFOperation, ONNXGraph, str)->None

    input = converter.converted_tensor(nnef_op.input)
    outputs = converter.converted_tensors(nnef_op.outputs)
    partial_convert_pool(converter, nnef_op, onnx_graph, target_name, input, outputs)

def partial_convert_pool(converter, nnef_op, onnx_graph, target_name, input, outputs, force_constant=False):
    # type: (Converter, NNEFOperation, ONNXGraph, str, ONNXTensor, typing.Tuple[ONNXTensor, ...], bool)->None
    if nnef_op.name == 'argmax_pool':
        outputs = (ONNXTensor(graph=onnx_graph, shape=list(outputs[0].shape), dtype=input.dtype),) + tuple(outputs)

    strides = list(nnef_op.attribs['stride'])
    if not strides:
        strides = [1] * input.rank
    dilations = nnef_op.attribs['dilation']
    if not dilations:
        dilations = [1] * input.rank

    assert nnef_op.attribs['border'] in ['constant', 'ignore']

    pads = nnef_op.attribs['padding']
    if not pads:
        pads = infer.same_padding(upscaled_input=input.shape[2:],
                                  filter=nnef_op.attribs['size'][2:],
                                  stride=strides[2:],
                                  dilation=dilations[2:])
    else:
        assert pads[:2] == [(0, 0), (0, 0)], "Padding in batch and channel dimensions ({}) is not supported in ONNX".format(pads)
        pads = pads[2:]
    pads = converter.onnx_pads(pads)

    assert nnef_op.attribs['size'][:2] == strides[:2] == dilations[:2] == [1, 1], \
        'Pooling in batch and channel dimensions is not supported in ONNX'
    strides = strides[2:]
    dilations = dilations[2:]

    assert all(d == 1 for d in dilations), 'Dilation is not supported for pooling in ONNX'

    onnx_op = ONNXOperation(graph=onnx_graph,
                            name=target_name,
                            inputs=input,
                            attribs=dict(kernel_shape=nnef_op.attribs['size'][2:],
                                         pads=pads,
                                         strides=strides),
                            outputs=outputs)

    if target_name == 'AveragePool':
        onnx_op.attribs['count_include_pad'] = 1 if (nnef_op.attribs['border'] == 'constant' or force_constant) else 0

def convert_desample(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, indices = converter.converted_tensors(nnef_op.inputs)
    output = converter.converted_tensor(nnef_op.output)

    strides = list(nnef_op.attribs['stride'])
    if not strides:
        strides = [1] * input.rank
    dilations = nnef_op.attribs['dilation']
    if not dilations:
        dilations = [1] * input.rank

    assert nnef_op.attribs['border'] in ['constant', 'ignore']

    pads = nnef_op.attribs['padding']
    if not pads:  # auto pad
        calc_output_size = [i * s for i, s in (input.shape[2:], strides)]
        pads = infer.same_padding(upscaled_input=calc_output_size,
                                  filter=nnef_op.attribs['size'],
                                  stride=strides,
                                  dilation=dilations)
    else:
        assert pads[:2] == [(0, 0), (0, 0)], "Padding in batch and channel dimensions ({}) is not supported in ONNX".format(pads[:2])
        pads = pads[2:]
    pads = converter.onnx_pads(pads)

    assert nnef_op.attribs['size'][:2] == strides[:2] == dilations[:2] == [1, 1], \
        'Pooling in batch and channel dimensions is not supported in ONNX'
    strides = strides[2:]
    dilations = dilations[2:]

    assert all(d == 1 for d in dilations), 'Dilation is not supported for pooling in ONNX'

    ONNXOperation(graph=onnx_graph,
                  name='MaxUnpool',
                  inputs=(input, indices),
                  attribs=dict(kernel_shape=nnef_op.attribs['size'][2:],
                               pads=pads,
                               strides=strides),
                  outputs=output)

def convert_reshape(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    shape = nnef_op.attribs['shape']
    axis_start = nnef_op.attribs['axis_start']
    axis_count = nnef_op.attribs['axis_count']
    if axis_count == -1:
        axis_count = input.rank - axis_start

    onnx_shape = input.shape[:axis_start] + shape + input.shape[axis_start + axis_count:]

    ONNXOperation(graph=onnx_graph,
                  name='Reshape',
                  inputs=(input, converter.constant_1d_tensor(graph=onnx_graph,
                                                              list_=onnx_shape,
                                                              dtype='INT64')),
                  outputs=output)

def convert_transpose(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    if not nnef_op.attribs['axes']:
        ONNXOperation(graph=onnx_graph, name='Identity', inputs=input, outputs=output)
        return

    perm = list(nnef_op.attribs['axes'])
    if len(perm) < input.rank:
        perm += list(range(len(perm), input.rank))

    ONNXOperation(graph=onnx_graph,
                  name='Transpose',
                  inputs=input,
                  attribs=dict(perm=perm),
                  outputs=output)

def convert_concat(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    inputs = converter.converted_tensors(nnef_op.inputs)
    output = converter.converted_tensor(nnef_op.output)

    ONNXOperation(graph=onnx_graph,
                  name='Concat',
                  inputs=inputs,
                  attribs=dict(axis=nnef_op.attribs['axis']),
                  outputs=output)

def convert_split(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input = converter.converted_tensor(nnef_op.input)
    outputs = converter.converted_tensors(nnef_op.outputs)

    axis = nnef_op.attribs['axis']
    ratios = nnef_op.attribs['ratios']
    assert input.shape[axis] % sum(ratios) == 0
    unit = input.shape[axis] // sum(ratios)

    ONNXOperation(graph=onnx_graph,
                  name='Split',
                  inputs=input,
                  attribs=dict(axis=axis,
                               split=[ratio * unit for ratio in ratios]),
                  outputs=outputs)

def convert_stack(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    inputs = converter.converted_tensors(nnef_op.inputs)
    output = converter.converted_tensor(nnef_op.output)

    inputs = [converter.add_unsqueeze(onnx_graph, input, [nnef_op.attribs['axis']]) for input in inputs]

    ONNXOperation(graph=onnx_graph,
                  name='Concat',
                  inputs=inputs,
                  attribs=dict(axis=nnef_op.attribs['axis']),
                  outputs=output)

def convert_unstack(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input = converter.converted_tensor(nnef_op.input)
    outputs = converter.converted_tensors(nnef_op.outputs)

    axis = nnef_op.attribs['axis']
    num_parts = input.shape[axis]

    def unsqueeze(shape, axis):
        shape = list(shape)
        shape.insert(axis, 1)
        return shape

    intermediates = [ONNXTensor(graph=onnx_graph, shape=unsqueeze(output.shape, axis), dtype=output.dtype)
                     for output in outputs]

    ONNXOperation(graph=onnx_graph,
                  name='Split',
                  inputs=input,
                  attribs=dict(axis=axis, split=[1] * num_parts),
                  outputs=intermediates)

    for intermediate, output in zip(intermediates, outputs):
        converter.add_unsqueeze(onnx_graph, intermediate, axis, unsqueezed_tensor=output)

def convert_slice(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    if not nnef_op.attribs['axes']:
        ONNXOperation(graph=onnx_graph, name='Identity', inputs=input, outputs=output)
        return

    axes = list(nnef_op.attribs['axes'])
    starts = list(nnef_op.attribs['begin'])

    ends = list(utils.INT32_MAX if e == 0 else e for e in nnef_op.attribs['end'])

    ONNXOperation(graph=onnx_graph,
                  name='Slice',
                  inputs=input,
                  attribs=dict(axes=axes, starts=starts, ends=ends),
                  outputs=output)

def generic_convert_binary1_or_binary2(converter, nnef_op, onnx_graph, target_name1, target_name2):
    # type: (Converter, NNEFOperation, ONNXGraph, str, str)->None

    x, y = converter.converted_tensors(nnef_op.inputs)
    z = converter.converted_tensor(nnef_op.output)

    binary1 = ONNXOperation(
        graph=onnx_graph,
        name=target_name1,
        inputs=(converter.add_unsqueeze(onnx_graph, x, list(range(x.rank, y.rank))) if 0 < x.rank < y.rank else x,
                converter.add_unsqueeze(onnx_graph, y, list(range(y.rank, x.rank))) if 0 < y.rank < x.rank else y),
        outputs=ONNXTensor(graph=onnx_graph, shape=list(z.shape), dtype=z.dtype))

    binary2 = ONNXOperation(
        graph=onnx_graph,
        name=target_name2,
        inputs=(converter.add_unsqueeze(onnx_graph, x, list(range(x.rank, y.rank))) if 0 < x.rank < y.rank else x,
                converter.add_unsqueeze(onnx_graph, y, list(range(y.rank, x.rank))) if 0 < y.rank < x.rank else y),
        outputs=ONNXTensor(graph=onnx_graph, shape=list(z.shape), dtype=z.dtype))

    ONNXOperation(
        graph=onnx_graph,
        name="Or",
        inputs=(binary1.output, binary2.output),
        outputs=z)

def convert_ne(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    x, y = converter.converted_tensors(nnef_op.inputs)
    z = converter.converted_tensor(nnef_op.output)

    equal = ONNXOperation(
        graph=onnx_graph,
        name='Equal',
        inputs=(converter.add_unsqueeze(onnx_graph, x, list(range(x.rank, y.rank))) if 0 < x.rank < y.rank else x,
                converter.add_unsqueeze(onnx_graph, y, list(range(y.rank, x.rank))) if 0 < y.rank < x.rank else y),
        outputs=ONNXTensor(graph=onnx_graph, shape=list(z.shape), dtype=z.dtype))

    ONNXOperation(
        graph=onnx_graph,
        name="Not",
        inputs=equal.output,
        outputs=z)

def convert_round(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    add = ONNXOperation(graph=onnx_graph,
                        name='Add',
                        inputs=(input, converter.constant_0d_tensor(graph=onnx_graph, value=0.5, dtype=input.dtype)),
                        outputs=ONNXTensor(graph=onnx_graph, shape=list(output.shape), dtype=output.dtype))

    ONNXOperation(graph=onnx_graph,
                  name='Floor',
                  inputs=add.output,
                  outputs=output)

def convert_select(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    cond, true_value, false_value = converter.converted_tensors(nnef_op.inputs)
    output = converter.converted_tensor(nnef_op.output)

    max_rank = max(t.rank for t in [cond, true_value, false_value])

    def broadcast(t):
        return converter.add_unsqueeze(onnx_graph, t, list(range(t.rank, max_rank))) if 0 < t.rank < max_rank else t

    ONNXOperation(
        graph=onnx_graph,
        name='Where',
        inputs=(broadcast(cond), broadcast(true_value), broadcast(false_value)),
        outputs=output)

def convert_clamp(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    x, a, b = converter.converted_tensors(nnef_op.inputs)
    y = converter.converted_tensor(nnef_op.output)

    if a.is_constant and a.shape == [] and b.is_constant and b.shape == []:
        ONNXOperation(
            graph=onnx_graph,
            name='Clip',
            inputs=x,
            attribs=dict(min=a.data[0], max=b.data[0]),
            outputs=y)
    else:
        max_rank = max(t.rank for t in [x, a, b])

        def broadcast(t):
            return converter.add_unsqueeze(onnx_graph, t, list(range(t.rank, max_rank))) if 0 < t.rank < max_rank else t

        min = ONNXOperation(
            graph=onnx_graph,
            name='Min',
            inputs=(broadcast(x), broadcast(b)),
            outputs=ONNXTensor(graph=onnx_graph, shape=list(y.shape), dtype=y.dtype),
        )

        ONNXOperation(
            graph=onnx_graph,
            name='Max',
            inputs=(min.output, broadcast(a)),
            outputs=y,
        )

def convert_multilinear_upsample(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None
    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    if nnef_op.attribs['method'] != 'symmetric':
        if converter.enable_imprecise_image_resize:
            print("Warning: method={} is unsupported in multilinear_upsample, "
                  "using symmetric, because enable_imprecise_image_resize was True"
                  .format(nnef_op.attribs["method"]))
        else:
            assert False, "Error: method={} is unsupported in multilinear_upsample. " \
                          "Use enable_imprecise_image_resize=True, to suppress this error." \
                .format(nnef_op.attribs["method"])

    if nnef_op.attribs['border'] != 'replicate':
        if converter.enable_imprecise_image_resize:
            print("Warning: border={} is unsupported in multilinear_upsample, "
                  "using replicate, because enable_imprecise_image_resize was True"
                  .format(nnef_op.attribs["border"]))
        else:
            assert False, "Error: border={} is unsupported in multilinear_upsample. " \
                          "Use enable_imprecise_image_resize=True, to suppress this error." \
                .format(nnef_op.attribs["border"])

    scales = [float(f) for f in [1, 1] + nnef_op.attribs['factor']]

    ONNXOperation(graph=onnx_graph,
                  name='Upsample',
                  inputs=(input, converter.constant_1d_tensor(graph=onnx_graph, list_=scales, dtype='FLOAT')),
                  attribs=dict(mode='linear'),
                  outputs=output)

def generic_convert_reduce(converter, nnef_op, onnx_graph, target_name, target_name_if_normalized="", one_axis=False):
    # type: (Converter, NNEFOperation, ONNXGraph, str, str, bool)->None
    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    if not nnef_op.attribs['axes']:
        ONNXOperation(graph=onnx_graph, name='Identity', inputs=input, outputs=output)
        return

    if one_axis:
        assert len(nnef_op.attribs['axes']) == 1, "{} supports only one axis in ONNX".format(target_name)

    onnx_op = ONNXOperation(graph=onnx_graph,
                            name=(target_name_if_normalized
                                  if target_name_if_normalized and nnef_op.attribs['normalize']
                                  else target_name),
                            inputs=input,
                            attribs=dict(keepdims=1),
                            outputs=output)

    if one_axis:
        onnx_op.attribs['axis'] = nnef_op.attribs['axes'][0]
    else:
        onnx_op.attribs['axes'] = list(nnef_op.attribs['axes'])

def convert_prelu(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None
    input, slope = converter.converted_tensors(nnef_op.inputs)
    output = converter.converted_tensor(nnef_op.output)

    ONNXOperation(graph=onnx_graph,
                  name='PRelu',
                  inputs=(input, converter.add_squeeze(onnx_graph, slope, [0])),
                  outputs=output)

def convert_batch_normalization(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, mean, variance, offset, scale = converter.converted_tensors(nnef_op.inputs)
    output = converter.converted_tensor(nnef_op.output)

    if input.rank < 3:  # Caffe2 BatchNorm works only for rank >= 3
        axes = list(range(input.rank, 3))
        input = converter.add_unsqueeze(onnx_graph=onnx_graph, onnx_tensor=input, axes=axes)
        final_output = output
        output = ONNXTensor(graph=onnx_graph, shape=list(input.shape), dtype=input.dtype)
        converter.add_squeeze(onnx_graph=onnx_graph, onnx_tensor=output, axes=axes, squeezed_tensor=final_output)

    ONNXOperation(graph=onnx_graph,
                  name='BatchNormalization',
                  inputs=(input,
                          converter.add_squeeze(onnx_graph, scale, [0]),
                          converter.add_squeeze(onnx_graph, offset, [0]),
                          converter.add_squeeze(onnx_graph, mean, [0]),
                          converter.add_squeeze(onnx_graph, variance, [0])),
                  attribs=dict(epsilon=nnef_op.attribs['epsilon']),
                  outputs=output)

def convert_copy_n(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input = converter.converted_tensor(nnef_op.input)
    outputs = converter.converted_tensors(nnef_op.outputs)

    for output in outputs:
        ONNXOperation(graph=onnx_graph,
                      name='Identity',
                      inputs=input,
                      outputs=output)

def convert_nearest_upsample(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    scales = [float(f) for f in [1, 1] + nnef_op.attribs['factor']]

    ONNXOperation(graph=onnx_graph,
                  name='Upsample',
                  inputs=(input, converter.constant_1d_tensor(onnx_graph, scales, 'FLOAT')),
                  attribs=dict(mode='nearest'),
                  outputs=output)

def convert_tile(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    ONNXOperation(graph=onnx_graph,
                  name='Tile',
                  inputs=(input,
                          converter.constant_1d_tensor(graph=onnx_graph,
                                                       list_=nnef_op.attribs['repeats'],
                                                       dtype='INT64')),
                  outputs=output)

def convert_pad(converter, nnef_op, onnx_graph):
    # type: (Converter, NNEFOperation, ONNXGraph)->None

    input, output = converter.converted_tensors((nnef_op.input, nnef_op.output))

    onnx_op = ONNXOperation(graph=onnx_graph,
                            name='Pad',
                            inputs=input,
                            attribs=dict(mode=converter.onnx_pad_mode(nnef_op.attribs['border']),
                                         pads=converter.onnx_pads(nnef_op.attribs['padding'])),
                            outputs=output)

    if onnx_op.attribs['mode'] == 'constant':
        onnx_op.attribs['value'] = 0.0

_DefaultConverters = {
    'external': UNNEEDED,
    'variable': UNNEEDED,
    'constant': UNNEEDED,
    'update': UNSUPPORTED,
    'reshape': convert_reshape,
    'transpose': convert_transpose,
    'concat': convert_concat,
    'split': convert_split,
    'slice': convert_slice,
    'squeeze': partial(generic_convert_unary, target_name='Squeeze', copy_attribs=['axes']),
    'unsqueeze': partial(generic_convert_unary, target_name='Unsqueeze', copy_attribs=['axes']),
    'stack': convert_stack,
    'unstack': convert_unstack,
    'add': partial(generic_convert_binary, target_name='Add'),
    'sub': partial(generic_convert_binary, target_name='Sub'),
    'mul': partial(generic_convert_binary, target_name='Mul'),
    'div': partial(generic_convert_binary, target_name='Div'),
    'pow': partial(generic_convert_binary, target_name='Pow', broadcast_workaround=True),
    'exp': partial(generic_convert_unary, target_name='Exp'),
    'log': partial(generic_convert_unary, target_name='Log'),
    'abs': partial(generic_convert_unary, target_name='Abs'),
    'sign': partial(generic_convert_unary, target_name='Sign'),
    'rcp': partial(generic_convert_unary, target_name='Reciprocal'),
    'neg': partial(generic_convert_unary, target_name='Neg'),
    'copy': partial(generic_convert_unary, target_name='Identity'),
    'lt': partial(generic_convert_binary, target_name='Less'),
    'gt': partial(generic_convert_binary, target_name='Greater'),
    'le': partial(generic_convert_binary1_or_binary2, target_name1='Less', target_name2='Equal'),
    'ge': partial(generic_convert_binary1_or_binary2, target_name1='Greater', target_name2='Equal'),
    'eq': partial(generic_convert_binary, target_name='Equal'),
    'ne': convert_ne,
    'and': partial(generic_convert_binary, target_name='And'),
    'or': partial(generic_convert_binary, target_name='Or'),
    'not': partial(generic_convert_unary, target_name='Not'),
    'floor': partial(generic_convert_unary, target_name='Floor'),
    'ceil': partial(generic_convert_unary, target_name='Ceil'),
    'round': convert_round,
    'select': convert_select,
    'sqr': NONATOMIC,
    'sqrt': partial(generic_convert_unary, target_name='Sqrt'),
    'rsqr': NONATOMIC,
    'rsqrt': NONATOMIC,
    'log2': NONATOMIC,
    'min': partial(generic_convert_binary, target_name='Min', broadcast_workaround=True),
    'max': partial(generic_convert_binary, target_name='Max', broadcast_workaround=True),
    'clamp': convert_clamp,
    'matmul': convert_matmul,
    'conv': partial(generic_convert_conv_deconv, is_deconv=False),
    'deconv': partial(generic_convert_conv_deconv, is_deconv=True),
    'box': convert_box,
    'debox': UNSUPPORTED,
    'argmax_pool': partial(generic_convert_pool, target_name='MaxPool'),  # not typo
    'sample': UNSUPPORTED,
    'desample': convert_desample,
    'nearest_downsample': NONATOMIC,
    'area_downsample': NONATOMIC,
    'nearest_upsample': convert_nearest_upsample,
    'multilinear_upsample': convert_multilinear_upsample,
    'sum_reduce': partial(generic_convert_reduce, target_name='ReduceSum', target_name_if_normalized='ReduceMean'),
    'max_reduce': partial(generic_convert_reduce, target_name='ReduceMax'),
    'min_reduce': partial(generic_convert_reduce, target_name='ReduceMin'),
    'argmax_reduce': partial(generic_convert_reduce, target_name='ArgMax', one_axis=True),
    'argmin_reduce': partial(generic_convert_reduce, target_name='ArgMin', one_axis=True),
    'mean_reduce': partial(generic_convert_reduce, target_name='ReduceMean'),
    'moments': NONATOMIC,
    'relu': partial(generic_convert_unary, target_name='Relu'),
    'sigmoid': partial(generic_convert_unary, target_name='Sigmoid'),
    'tanh': partial(generic_convert_unary, target_name='Tanh'),
    'softabs': NONATOMIC,
    'softmax': partial(generic_convert_unary, target_name='Softmax'),
    'softplus': partial(generic_convert_unary, target_name='Softplus'),
    'elu': partial(generic_convert_unary, target_name='Elu'),
    'prelu': convert_prelu,
    'leaky_relu': partial(generic_convert_unary, target_name='LeakyRelu', copy_attribs=['alpha']),
    'max_pool_with_index': partial(generic_convert_pool, target_name='MaxPool'),  # not typo
    'max_pool': partial(generic_convert_pool, target_name='MaxPool'),
    'avg_pool': partial(generic_convert_pool, target_name='AveragePool'),
    'rms_pool': NONATOMIC,
    'linear': convert_linear,
    'separable_conv': NONATOMIC,
    'separable_deconv': NONATOMIC,
    'local_response_normalization': convert_lrn,
    'local_mean_normalization': NONATOMIC,
    'local_variance_normalization': NONATOMIC,
    'local_contrast_normalization': NONATOMIC,
    'l1_normalization': NONATOMIC,
    'l2_normalization': NONATOMIC,
    'batch_normalization': convert_batch_normalization,
    'avg_roi_pool': UNSUPPORTED,
    'max_roi_pool': UNSUPPORTED,  # maybe supported
    'roi_resample': UNSUPPORTED,
    'avg_roi_align': UNSUPPORTED,
    'max_roi_align': UNSUPPORTED,
    'linear_quantize': NONATOMIC,
    'logarithmic_quantize': NONATOMIC,
    'copy_n': convert_copy_n,
    'tile': convert_tile,
    'pad': convert_pad,
    'sin': partial(generic_convert_unary, target_name="Sin"),
    'cos': partial(generic_convert_unary, target_name="Cos"),
}

# TODO add to class as static(?) method
# NNEF must be parsed with this before calling nnef_to_tf.Converter on it
ParserConfig = NNEFParserConfig(lowered=[k for k, v in six.iteritems(_DefaultConverters) if v is NONATOMIC])