Пример #1
0
class UI:

    LOAD_TAB = 0
    ALIGN_TAB = 1
    STACK_TAB = 2
    SHARPEN_TAB = 3

    TITLE = "AstraStack"
    VERSION = "2.0.0"

    def __init__(self):
        self.parentConn, self.childConn = Pipe(duplex=True)
        self.pids = []
        self.newVersionUrl = ""
        self.video = None
        self.align = None
        self.stack = None
        self.sharpen = None
        self.mousePosition = None
        self.clickedDriftP1 = False
        self.clickedDriftP2 = False
        self.clickedAreaOfInterest = False

        self.builder = Gtk.Builder()
        self.builder.add_from_file("ui/ui.glade")

        self.window = self.builder.get_object("mainWindow")
        self.saveDialog = self.builder.get_object("saveDialog")
        self.openDialog = self.builder.get_object("openDialog")
        self.tabs = self.builder.get_object("tabs")
        self.cpus = self.builder.get_object("cpus")
        self.progressBox = self.builder.get_object("progressBox")
        self.progress = self.builder.get_object("progress")
        self.frame = self.builder.get_object("frame")
        self.overlay = self.builder.get_object("overlay")
        self.frameSlider = self.builder.get_object("frameSlider")
        self.frameScale = self.builder.get_object("frameScale")
        self.startFrame = self.builder.get_object("startFrame")
        self.endFrame = self.builder.get_object("endFrame")
        self.normalize = self.builder.get_object("normalize")
        self.alignChannels = self.builder.get_object("alignChannels")
        self.autoCrop = self.builder.get_object("autoCrop")
        self.transformation = self.builder.get_object("transformation")
        self.drizzleFactor = self.builder.get_object("drizzleFactor")
        self.drizzleInterpolation = self.builder.get_object(
            "drizzleInterpolation")
        self.limit = self.builder.get_object("limit")
        self.limitPercent = self.builder.get_object("limitPercent")
        self.averageRadio = self.builder.get_object("averageRadio")
        self.medianRadio = self.builder.get_object("medianRadio")

        self.openDialog.set_preview_widget(Gtk.Image())
        self.saveDialog.set_preview_widget(Gtk.Image())

        self.builder.get_object("alignTab").set_sensitive(False)
        self.builder.get_object("stackTab").set_sensitive(False)
        self.builder.get_object("processTab").set_sensitive(False)

        self.builder.get_object("blackLevel").add_mark(0, Gtk.PositionType.TOP,
                                                       None)
        self.builder.get_object("gamma").add_mark(100, Gtk.PositionType.TOP,
                                                  None)
        self.builder.get_object("value").add_mark(100, Gtk.PositionType.TOP,
                                                  None)
        self.builder.get_object("red").add_mark(100, Gtk.PositionType.TOP,
                                                None)
        self.builder.get_object("green").add_mark(100, Gtk.PositionType.TOP,
                                                  None)
        self.builder.get_object("blue").add_mark(100, Gtk.PositionType.TOP,
                                                 None)
        self.builder.get_object("saturation").add_mark(100,
                                                       Gtk.PositionType.TOP,
                                                       None)

        self.disableScroll()

        self.cpus.set_upper(min(
            61, cpu_count()))  # 61 is the maximum that Windows allows
        self.cpus.set_value(min(61, math.ceil(cpu_count() / 2)))
        g.pool = None

        self.processThread = None

        self.builder.connect_signals(self)

        # Needed so it can be temporarily removed
        self.limitPercentSignal = self.limitPercent.connect(
            "value-changed", self.setLimitPercent)

        g.driftP1 = (0, 0)
        g.driftP2 = (0, 0)

        g.areaOfInterestP1 = (0, 0)
        g.areaOfInterestP2 = (0, 0)

        self.window.show_all()
        self.checkNewVersion()
        self.setProgress()
        self.setNormalize()
        self.setAlignChannels()
        self.setTransformation()
        self.setDrizzleFactor()
        self.setDrizzleInterpolation()
        self.setAutoCrop()
        self.setThreads()
        self.frameScale.set_sensitive(False)
        g.reference = "0"

    # Cancels scroll event for widget
    def propagateScroll(self, widget, event):
        Gtk.propagate_event(widget.get_parent(), event)

    # Disables the scroll event from some fields
    def disableScroll(self):
        mask = Gdk.EventMask.BUTTON_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.KEY_RELEASE_MASK | Gdk.EventMask.TOUCH_MASK
        self.builder.get_object("denoiseWidget1").set_events(mask)
        self.builder.get_object("denoiseWidget2").set_events(mask)
        self.builder.get_object("denoiseWidget3").set_events(mask)
        self.builder.get_object("denoiseWidget4").set_events(mask)
        self.builder.get_object("denoiseWidget5").set_events(mask)

        self.builder.get_object("radiusWidget1").set_events(mask)
        self.builder.get_object("radiusWidget2").set_events(mask)
        self.builder.get_object("radiusWidget3").set_events(mask)
        self.builder.get_object("radiusWidget4").set_events(mask)
        self.builder.get_object("radiusWidget5").set_events(mask)

    # Sets up a listener so that processes can communicate with each other
    def createListener(self, function):
        def listener(function):
            while True:
                try:
                    msg = self.parentConn.recv()
                except:
                    return False
                if (msg == "stop"):
                    g.ui.setProgress()
                    return False
                function(msg)

        thread = Thread(target=listener, args=(function, ))
        thread.start()
        return thread

    # Shows the error dialog with the given title and message
    def showErrorDialog(self, message):
        dialog = self.builder.get_object("errorDialog")
        dialog.format_secondary_text(message)
        response = dialog.run()
        dialog.hide()
        return response

    # Shows the warning dialog with the given title and message
    def showWarningDialog(self, message):
        dialog = self.builder.get_object("warningDialog")
        dialog.format_secondary_text(message)
        response = dialog.run()
        dialog.hide()
        return response

    # Opens the About dialog
    def showAbout(self, *args):
        dialog = self.builder.get_object("about")
        dialog.set_program_name(UI.TITLE)
        dialog.set_version(UI.VERSION)
        response = dialog.run()
        dialog.hide()

    # Opens the user manual in the default pdf application
    def showManual(self, *args):
        if sys.platform.startswith('darwin'):
            subprocess.call(('open', "manual/Manual.pdf"))
        elif os.name == 'nt':  # For Windows
            os.startfile("manual\Manual.pdf")
        elif os.name == 'posix':  # For Linux, Mac, etc.
            subprocess.call(('xdg-open', "manual/Manual.pdf"))

    # Disable inputs
    def disableUI(self):
        self.builder.get_object("tabs").set_sensitive(False)
        self.builder.get_object("toolbar").set_sensitive(False)

    # Enable inputs
    def enableUI(self):
        self.builder.get_object("tabs").set_sensitive(True)
        self.builder.get_object("toolbar").set_sensitive(True)

    # The following is needed to forcibly refresh the value spacing of the slider
    def fixFrameSliderBug(self):
        self.frameScale.set_value_pos(Gtk.PositionType.RIGHT)
        self.frameScale.set_value_pos(Gtk.PositionType.LEFT)

    # Sets the number of threads to use
    def setThreads(self, *args):
        def initPool(method=None):
            if (method == "spawn"):
                GLib.idle_add(self.disableUI)
            g.nThreads = int(self.cpus.get_value())
            if (g.pool is not None):
                g.pool.shutdown()
            g.pool = ProcessPoolExecutor(g.nThreads)

            # This seems like the most reliable way to get the pid of pool processes
            self.pids = []
            before = list(map(lambda p: p.pid, active_children()))
            g.pool.submit(dummy, ()).result()
            after = list(map(lambda p: p.pid, active_children()))
            for pid in after:
                if (pid not in before):
                    self.pids.append(pid)
            if (method == "spawn"):
                GLib.idle_add(self.enableUI)

        # Behave a bit differently depending on platform
        if (get_start_method() == "spawn"):
            thread = Thread(target=initPool, args=(get_start_method(), ))
            thread.start()
        else:
            initPool()

    # Checks github to see if there is a new version available
    def checkNewVersion(self):
        def callUrl():
            try:
                contents = urllib.request.urlopen(
                    "https://api.github.com/repos/Finalfantasykid/AstraStack/releases/latest"
                ).read()
                obj = json.loads(contents)
                if (version.parse(obj['name']) > version.parse(UI.VERSION)):
                    self.newVersionUrl = obj['html_url']
                    button.show()
            except:
                return

        button = self.builder.get_object("newVersion")
        button.hide()
        thread = Thread(target=callUrl, args=())
        thread.start()

    # Opens the GitHub releases page in a browser
    def clickNewVersion(self, *args):
        webbrowser.open(self.newVersionUrl)

    # Checks to see if there will be enough memory to process the image
    def checkMemory(self, w=0, h=0):
        if (Sharpen.estimateMemoryUsage(w, h) >
                psutil.virtual_memory().available):
            response = self.showWarningDialog(
                "Your system may not have enough memory to process this file, are you sure you want to continue?"
            )
            return (response == Gtk.ResponseType.YES)
        return True

    # Shows preview image in file chooser dialog
    def updatePreview(self, dialog):
        path = dialog.get_preview_filename()
        pixbuf = None
        try:
            # First try as image
            pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
        except Exception:
            try:
                # Now try as video
                if ("video/" in mimetypes.guess_type(path)[0]):
                    video = Video()
                    img = cv2.cvtColor(video.getFrame(path, 0),
                                       cv2.COLOR_BGR2RGB)
                    height, width = img.shape[:2]

                    z = img.tobytes()
                    Z = GLib.Bytes.new(z)

                    pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
                        Z, GdkPixbuf.Colorspace.RGB, False, 8, width, height,
                        width * 3)
            except Exception:
                dialog.set_preview_widget_active(False)
        if (pixbuf is not None):
            # Valid pixbuf
            maxwidth, maxheight = 250, 250
            width, height = pixbuf.get_width(), pixbuf.get_height()
            scale = min(maxwidth / width, maxheight / height)
            if (scale < 1):
                width, height = int(width * scale), int(height * scale)
                pixbuf = pixbuf.scale_simple(width, height,
                                             GdkPixbuf.InterpType.BILINEAR)

            dialog.get_preview_widget().set_size_request(
                width + 10, height + 10)
            dialog.get_preview_widget().set_from_pixbuf(pixbuf)
            dialog.set_preview_widget_active(True)

    # Opens the file chooser to open load a file
    def openVideo(self, *args):
        self.openDialog.set_current_folder(path.expanduser("~"))
        self.openDialog.set_select_multiple(False)
        self.openDialog.set_filter(self.builder.get_object("videoFilter"))
        response = self.openDialog.run()
        self.openDialog.hide()
        if (response == Gtk.ResponseType.OK):
            try:
                g.file = self.openDialog.get_filename()
                self.video = Video()
                self.video.checkMemory()
                thread = Thread(target=self.video.run, args=())
                thread.start()
                self.disableUI()
            except MemoryError as error:
                self.enableUI()

    # Opens the file chooser to open load a file
    def openImageSequence(self, *args):
        self.openDialog.set_current_folder(path.expanduser("~"))
        self.openDialog.set_select_multiple(True)
        self.openDialog.set_filter(self.builder.get_object("imageFilter"))
        response = self.openDialog.run()
        self.openDialog.hide()
        if (response == Gtk.ResponseType.OK):
            try:
                g.file = self.openDialog.get_filenames()
                self.video = Video()
                self.video.checkMemory()
                thread = Thread(target=self.video.run, args=())
                thread.start()
                self.disableUI()
            except MemoryError as error:
                self.enableUI()

    # Opens the file chooser to open load a file
    def openImage(self, *args):
        self.openDialog.set_current_folder(path.expanduser("~"))
        self.openDialog.set_select_multiple(False)
        self.openDialog.set_filter(self.builder.get_object("imageFilter"))
        response = self.openDialog.run()
        self.openDialog.hide()
        if (response == Gtk.ResponseType.OK):
            self.disableUI()
            g.file = self.openDialog.get_filename()
            try:
                self.video = Video()
                img = cv2.imread(g.file)
                h, w = img.shape[:2]
                if (not self.checkMemory(w, h)):
                    raise MemoryError()

                self.window.set_title(path.split(g.file)[1] + " - " + UI.TITLE)
                self.saveDialog.set_current_name("")
                self.sharpen = Sharpen(g.file, True)
                self.builder.get_object("alignTab").set_sensitive(False)
                self.builder.get_object("stackTab").set_sensitive(False)
                self.builder.get_object("processTab").set_sensitive(True)
                self.tabs.set_current_page(UI.SHARPEN_TAB)
                self.frame.set_from_file(g.file)
            except MemoryError as error:
                pass
            except:  # Open Failed
                self.showErrorDialog(
                    "There was an error opening the image, make sure it is a valid image."
                )
            self.enableUI()

    # Opens the file chooser to save the final image
    def saveFileDialog(self, *args):
        self.saveDialog.set_current_folder(path.expanduser("~"))
        if (self.saveDialog.get_current_name() == ""):
            # Set default file to save if empty
            if (isinstance(g.file, list)):
                sList = g.file
                self.saveDialog.set_current_name(
                    Path(sList[0]).stem + "_" + Path(sList[-1]).stem + ".png")
            else:
                self.saveDialog.set_current_name(Path(g.file).stem + ".png")
        response = self.saveDialog.run()
        if (response == Gtk.ResponseType.OK):
            fileName = self.saveDialog.get_filename()
            try:
                cv2.imwrite(
                    fileName,
                    cv2.cvtColor(self.sharpen.finalImage.astype('uint8'),
                                 cv2.COLOR_RGB2BGR))
            except:  # Save Failed
                self.showErrorDialog(
                    "There was an error saving the image, make sure it is a valid file extension."
                )
        self.saveDialog.hide()

    # Called when the video is finished loading
    def finishedVideo(self):
        def update():
            self.tabs.next_page()

            self.frameScale.set_sensitive(True)

            self.startFrame.set_lower(0)
            self.startFrame.set_upper(len(self.video.frames) - 1)
            self.startFrame.set_value(0)

            self.endFrame.set_upper(len(self.video.frames) - 1)
            self.endFrame.set_lower(0)
            self.endFrame.set_value(len(self.video.frames) - 1)

            g.driftP1 = (0, 0)
            g.driftP2 = (0, 0)
            g.areaOfInterestP1 = (0, 0)
            g.areaOfInterestP2 = (0, 0)
            g.reference = self.video.sharpest
            self.frameSlider.set_value(self.video.sharpest)

            self.setReference()
            self.setStartFrame()
            self.setEndFrame()
            self.setDriftPoint()
            self.enableUI()
            if (isinstance(g.file, list)):
                sList = g.file
                self.window.set_title(
                    path.split(sList[0])[1] + " ... " +
                    path.split(sList[-1])[1] + " - " + UI.TITLE)
            else:
                self.window.set_title(path.split(g.file)[1] + " - " + UI.TITLE)
            self.saveDialog.set_current_name("")
            self.builder.get_object("alignTab").set_sensitive(True)
            self.builder.get_object("stackTab").set_sensitive(False)
            self.builder.get_object("processTab").set_sensitive(False)
            self.stack = None

        GLib.idle_add(update)

    # Called when the tab is changed.  Updates parts of the UI based on the tab
    def changeTab(self, notebook, page, page_num, user_data=None):
        if (page_num == UI.LOAD_TAB or page_num == UI.ALIGN_TAB):
            self.frameSlider.set_value(0)
            self.frameSlider.set_upper(len(self.video.frames) - 1)
            self.setStartFrame()
            self.setEndFrame()
            self.frameScale.show()
            self.updateImage(None, page_num)
        elif (page_num == UI.STACK_TAB):
            self.frameSlider.set_lower(0)
            self.frameSlider.set_upper(len(self.align.tmats) - 1)
            self.frameSlider.set_value(0)
            self.frameScale.show()
            self.updateImage(None, page_num)
        elif (page_num == UI.SHARPEN_TAB):
            self.frameScale.hide()
            self.sharpenImage()
        self.fixFrameSliderBug()

    # Changes the image frame to the frameSlider position
    def updateImage(self, adjustment=None, page_num=None):
        if (self.video is None):
            return
        if (page_num is None):
            page_num = self.tabs.get_current_page()
        if (page_num == UI.LOAD_TAB or page_num == UI.ALIGN_TAB):
            videoIndex = int(self.frameSlider.get_value())
            img = cv2.cvtColor(
                self.video.getFrame(g.file, self.video.frames[videoIndex]),
                cv2.COLOR_BGR2RGB)
            height, width = img.shape[:2]

            z = img.tobytes()
            Z = GLib.Bytes.new(z)

            pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(Z,
                                                     GdkPixbuf.Colorspace.RGB,
                                                     False, 8, width, height,
                                                     width * 3)
            self.frame.set_from_pixbuf(pixbuf)
        elif (page_num == UI.STACK_TAB):
            tmat = self.stack.tmats[int(self.frameSlider.get_value())]
            videoIndex = tmat[0]
            M = tmat[1]
            img = self.video.getFrame(g.file, videoIndex)
            if (g.autoCrop):
                ref = self.stack.refBG.astype(np.uint8)
            else:
                ref = None
            img = transform(img, ref, M, self.align.minX, self.align.maxX,
                            self.align.minY, self.align.maxY, g.drizzleFactor,
                            g.drizzleInterpolation)
            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
            height, width = img.shape[:2]

            z = img.tobytes()
            Z = GLib.Bytes.new(z)

            pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(Z,
                                                     GdkPixbuf.Colorspace.RGB,
                                                     False, 8, width, height,
                                                     width * 3)
            self.frame.set_from_pixbuf(pixbuf)

    # Draws a rectangle where the area of interest is
    def drawOverlay(self, widget, cr):
        width = widget.get_allocated_width()
        height = widget.get_allocated_height()

        def drawPoint(cr, x, y):
            cr.new_sub_path()
            cr.set_line_width(2)
            cr.set_source_rgb(1, 1, 1)

            cr.arc(x, y, 2, 0, 2 * math.pi)
            cr.stroke_preserve()

            cr.set_source_rgb(1, 0, 0)
            cr.fill()

        def drawRect(cr, x1, y1, x2, y2):
            cr.rectangle(0, 0, x1, y1)
            cr.rectangle(0, y1, x1, (y2 - y1))
            cr.rectangle(0, y1, x1, height * 2)
            cr.rectangle(x1, y2, (x2 - x1), height * 2)
            cr.rectangle(x2, y2, width * 2, height * 2)
            cr.rectangle(x2, y1, width * 2, (y2 - y1))
            cr.rectangle(x2, 0, width * 2, y1)
            cr.rectangle(x1, 0, (x2 - x1), y1)

            cr.set_source_rgba(0, 0, 0, 0.25)
            cr.fill()

            cr.set_line_width(1)
            cr.set_source_rgb(1, 0, 0)

            cr.rectangle(x1, y1, (x2 - x1), (y2 - y1))
            cr.stroke()

        if (self.tabs.get_current_page() == UI.ALIGN_TAB):
            current = self.frameSlider.get_value()

            # Area of Interest
            px1 = min(g.areaOfInterestP1[0], g.areaOfInterestP2[0])
            py1 = min(g.areaOfInterestP1[1], g.areaOfInterestP2[1])

            px2 = max(g.areaOfInterestP1[0], g.areaOfInterestP2[0])
            py2 = max(g.areaOfInterestP1[1], g.areaOfInterestP2[1])

            # Drift Points
            dx1 = g.driftP1[0]
            dy1 = g.driftP1[1]

            dx2 = g.driftP2[0]
            dy2 = g.driftP2[1]

            dx = 0
            dy = 0

            if (dx1 != 0 and dy1 != 0 and dx2 != 0 and dy2 != 0):
                dx = dx2 - dx1
                dy = dy2 - dy1

            if (px1 != 0 and py1 != 0 and px2 != 0 and py2 != 0):
                # Draw Area of Interest Rectangle
                drawRect(
                    cr, px1 + (dx / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame),
                    py1 + (dy / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame),
                    px2 + (dx / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame),
                    py2 + (dy / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame))

            if (dx1 != 0 and dy1 != 0 and current == g.startFrame):
                # Draw point on first frame
                drawPoint(cr, dx1, dy1)

            if (dx1 != 0 and dy1 != 0 and current != g.startFrame and dx2 != 0
                    and dy2 != 0 and current != g.endFrame):
                # Draw interpolated point
                drawPoint(
                    cr, dx1 + (dx / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame),
                    dy1 + (dy / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame))

            if (dx2 != 0 and dy2 != 0 and current == g.endFrame):
                # Draw point on last frame
                drawPoint(cr, dx2, dy2)

    # Sets the reference frame to the current visible frame
    def setReference(self, *args):
        g.reference = str(int(self.frameSlider.get_value()))
        self.builder.get_object("referenceLabel").set_text(g.reference)
        self.builder.get_object("alignButton").set_sensitive(True)

    # Updates the progress bar
    def setProgress(self, i=0, total=0, text=""):
        def update():
            if (total == 0):
                self.progressBox.hide()
            else:
                self.progressBox.show()
                self.progress.set_fraction(i / total)
                self.progress.set_text(text + " " +
                                       str(round((i / total) * 100)) + "%")

        GLib.idle_add(update)

    # Sets the start frame for trimming
    def setStartFrame(self, *args):
        g.startFrame = int(self.startFrame.get_value())
        self.endFrame.set_lower(g.startFrame + 1)
        self.frameSlider.set_lower(g.startFrame)
        self.frameSlider.set_value(
            max(g.startFrame, self.frameSlider.get_value()))
        if (int(g.startFrame) > int(g.reference)):
            # Reference is outside of the range, fix it
            g.reference = str(int(g.startFrame))
            self.builder.get_object("referenceLabel").set_text(g.reference)
        self.fixFrameSliderBug()

    # Sets the end frame for trimming
    def setEndFrame(self, *args):
        g.endFrame = int(self.endFrame.get_value())
        self.startFrame.set_upper(g.endFrame - 1)
        self.frameSlider.set_upper(g.endFrame)
        self.frameSlider.set_value(
            min(g.endFrame, self.frameSlider.get_value()))
        if (int(g.endFrame) < int(g.reference)):
            # Reference is outside of the range, fix it
            g.reference = str(int(g.endFrame))
            self.builder.get_object("referenceLabel").set_text(g.reference)
        self.fixFrameSliderBug()

    # Drift Point 1 Button Clicked
    def clickDriftP1(self, *args):
        self.clickedDriftP1 = False
        self.clickedDriftP2 = False
        self.clickedAreaOfInterest = False
        self.setDriftPoint()
        self.frameSlider.set_value(g.startFrame)
        self.clickedDriftP1 = True
        self.window.get_window().set_cursor(
            Gdk.Cursor(Gdk.CursorType.CROSSHAIR))

    # Drift Point 2 Button Clicked
    def clickDriftP2(self, *args):
        self.clickedDriftP1 = False
        self.clickedDriftP2 = False
        self.clickedAreaOfInterest = False
        self.setDriftPoint()
        self.frameSlider.set_value(g.endFrame)
        self.clickedDriftP2 = True
        self.window.get_window().set_cursor(
            Gdk.Cursor(Gdk.CursorType.CROSSHAIR))

    # Reset Drift Point 1 to (0, 0)
    def resetDriftP1(self, widget, event):
        if (event.button == 3):  # Right Click
            g.driftP1 = (0, 0)
            self.clickedDriftP1 = False
            self.clickedDriftP2 = False
            self.clickedAreaOfInterest = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
            self.overlay.queue_draw()

    # Reset Drift Point 2 to (0, 0)
    def resetDriftP2(self, widget, event):
        if (event.button == 3):  # Right Click
            g.driftP2 = (0, 0)
            self.clickedDriftP1 = False
            self.clickedDriftP2 = False
            self.clickedAreaOfInterest = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
            self.overlay.queue_draw()

    # Updates the drift point
    def setDriftPoint(self, *args):
        if (self.clickedDriftP1 or self.clickedDriftP2):
            if (self.clickedDriftP1):
                g.driftP1 = self.mousePosition
            elif (self.clickedDriftP2):
                g.driftP2 = self.mousePosition
            self.clickedDriftP1 = False
            self.clickedDriftP2 = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
            self.overlay.queue_draw()

    # Area of Interest button clicked
    def clickAreaOfInterest(self, *args):
        g.areaOfInterestP1 = (0, 0)
        g.areaOfInterestP2 = (0, 0)
        self.clickedDriftP1 = False
        self.clickedDriftP2 = False
        self.clickedAreaOfInterest = True
        self.frameSlider.set_value(g.startFrame)
        self.window.get_window().set_cursor(
            Gdk.Cursor(Gdk.CursorType.CROSSHAIR))
        self.overlay.queue_draw()

    # Reset Area of Interest to (0, 0)
    def resetAreaOfInterest(self, widget, event):
        if (event.button == 3):  # Right Click
            g.areaOfInterestP1 = (0, 0)
            g.areaOfInterestP2 = (0, 0)
            self.clickedDriftP1 = False
            self.clickedDriftP2 = False
            self.clickedAreaOfInterest = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
            self.overlay.queue_draw()

    # First point int the Area of Interest clicked, drag started
    def dragBegin(self, *args):
        if (self.clickedAreaOfInterest):
            g.areaOfInterestP1 = self.mousePosition

    # Mouse released after dragging Area of Interest
    def dragEnd(self, *args):
        if (self.clickedAreaOfInterest):
            g.areaOfInterestP2 = self.mousePosition
            self.clickedAreaOfInterest = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))

    # Called when the mouse moves over the frame
    def updateMousePosition(self, *args):
        pointer = self.frame.get_pointer()
        self.mousePosition = (min(max(0, pointer.x),
                                  self.frame.get_allocation().width),
                              min(max(0, pointer.y),
                                  self.frame.get_allocation().height))
        if (self.clickedAreaOfInterest):
            if (g.areaOfInterestP1 != (0, 0)):
                g.areaOfInterestP2 = self.mousePosition
            self.overlay.queue_draw()

    # Sets whether or not to normalize the frames during alignment
    def setNormalize(self, *args):
        g.normalize = self.normalize.get_active()

    # Sets whether or not to align channels separately
    def setAlignChannels(self, *args):
        g.alignChannels = self.alignChannels.get_active()

    # Sets the type of transformation
    def setTransformation(self, *args):
        text = self.transformation.get_active_text()
        if (text == "None"):
            g.transformation = -1
        elif (text == "Translation"):
            g.transformation = StackReg.TRANSLATION
        elif (text == "Rigid Body"):
            g.transformation = StackReg.RIGID_BODY
        elif (text == "Scaled Rotation"):
            g.transformation = StackReg.SCALED_ROTATION
        elif (text == "Affine"):
            g.transformation = StackReg.AFFINE

    # Sets the drizzle scaling factor
    def setDrizzleFactor(self, *args):
        text = self.drizzleFactor.get_active_text()
        if (text == "0.25X"):
            g.drizzleFactor = 0.25
        elif (text == "0.50X"):
            g.drizzleFactor = 0.50
        elif (text == "0.75X"):
            g.drizzleFactor = 0.75
        elif (text == "1.0X"):
            g.drizzleFactor = 1.0
        elif (text == "1.5X"):
            g.drizzleFactor = 1.5
        elif (text == "2.0X"):
            g.drizzleFactor = 2.0
        elif (text == "2.5X"):
            g.drizzleFactor = 2.5
        elif (text == "3.0X"):
            g.drizzleFactor = 3.0
        if (self.stack is not None):
            self.stack.generateRefBG()
        self.updateImage()

    # Sets the drizzle scaling factor
    def setDrizzleInterpolation(self, *args):
        text = self.drizzleInterpolation.get_active_text()
        if (text == "Nearest Neighbor"):
            g.drizzleInterpolation = cv2.INTER_NEAREST
        elif (text == "Bilinear"):
            g.drizzleInterpolation = cv2.INTER_LINEAR
        elif (text == "Bicubic"):
            g.drizzleInterpolation = cv2.INTER_CUBIC
        elif (text == "Lanczos"):
            g.drizzleInterpolation = cv2.INTER_LANCZOS4
        if (self.stack is not None):
            self.stack.generateRefBG()
        self.updateImage()

    # Sets whether or not to auto crop
    def setAutoCrop(self, *args):
        g.autoCrop = not self.autoCrop.get_active()
        self.updateImage()

    # Runs the Alignment step
    def clickAlign(self, *args):
        self.align = Align(self.video.frames[g.startFrame:g.endFrame + 1])
        thread = Thread(target=self.align.run, args=())
        thread.start()
        self.disableUI()

    # Kills all pool processes
    def killPool(self):
        for pid in self.pids:
            if (psutil.pid_exists(pid)):
                p = psutil.Process(pid)
                p.kill()

    # Stops the current action being performed
    def stopProcessing(self, *args):
        self.killPool()
        g.pool = None
        self.setThreads()
        self.setProgress()
        self.enableUI()

    # Called when the Alignment is complete
    def finishedAlign(self):
        def update():
            self.stack = Stack(self.align.tmats)
            self.tabs.next_page()
            self.enableUI()
            self.builder.get_object("alignTab").set_sensitive(True)
            self.builder.get_object("stackTab").set_sensitive(True)
            self.builder.get_object("processTab").set_sensitive(False)
            self.limit.set_upper(len(self.align.tmats))
            self.limit.set_value(int(len(self.align.tmats) / 2))
            self.limitPercent.set_value(
                round(self.limit.get_value() / len(self.align.tmats) * 100))
            self.setLimit()
            self.setLimitPercent()

        GLib.idle_add(update)

    # Sets the number of frames to use in the Stack
    def setLimit(self, *args):
        self.limitPercent.disconnect(self.limitPercentSignal)
        self.limit.set_upper(len(self.align.tmats))
        g.limit = int(self.limit.get_value())
        self.limitPercent.set_value(
            round(g.limit / len(self.align.tmats) * 100))
        self.limitPercentSignal = self.limitPercent.connect(
            "value-changed", self.setLimitPercent)

    # Sets the number of frames to use in the Stack
    def setLimitPercent(self, *args):
        limitPercent = self.limitPercent.get_value() / 100
        self.limit.set_value(round(limitPercent * len(self.align.tmats)))

    # Stack Button clicked
    def clickStack(self, *args):
        try:
            self.stack.checkMemory()
            thread = Thread(target=self.stack.run, args=())
            thread.start()
            self.disableUI()
        except MemoryError as error:
            self.enableUI()

    # Called when the stack is complete
    def finishedStack(self):
        def update():
            self.sharpen = Sharpen(self.stack.stackedImage)
            self.tabs.next_page()
            self.enableUI()
            self.builder.get_object("alignTab").set_sensitive(True)
            self.builder.get_object("stackTab").set_sensitive(True)
            self.builder.get_object("processTab").set_sensitive(True)

        GLib.idle_add(update)

    # Sharpens the final Stacked image
    def sharpenImage(self, *args):
        g.sharpen1 = self.builder.get_object("sharpen1").get_value()
        g.sharpen2 = self.builder.get_object("sharpen2").get_value()
        g.sharpen3 = self.builder.get_object("sharpen3").get_value()
        g.sharpen4 = self.builder.get_object("sharpen4").get_value()
        g.sharpen5 = self.builder.get_object("sharpen5").get_value()

        g.radius1 = self.builder.get_object("radius1").get_value()
        g.radius2 = self.builder.get_object("radius2").get_value()
        g.radius3 = self.builder.get_object("radius3").get_value()
        g.radius4 = self.builder.get_object("radius4").get_value()
        g.radius5 = self.builder.get_object("radius5").get_value()

        g.denoise1 = self.builder.get_object("denoise1").get_value()
        g.denoise2 = self.builder.get_object("denoise2").get_value()
        g.denoise3 = self.builder.get_object("denoise3").get_value()
        g.denoise4 = self.builder.get_object("denoise4").get_value()
        g.denoise5 = self.builder.get_object("denoise5").get_value()

        g.level1 = self.builder.get_object("level1").get_active()
        g.level2 = self.builder.get_object("level2").get_active()
        g.level3 = self.builder.get_object("level3").get_active()
        g.level4 = self.builder.get_object("level4").get_active()
        g.level5 = self.builder.get_object("level5").get_active()

        g.gamma = self.builder.get_object("gammaAdjust").get_value()
        g.blackLevel = self.builder.get_object("blackLevelAdjust").get_value()
        g.value = self.builder.get_object("valueAdjust").get_value()

        g.redAdjust = self.builder.get_object("redAdjust").get_value()
        g.greenAdjust = self.builder.get_object("greenAdjust").get_value()
        g.blueAdjust = self.builder.get_object("blueAdjust").get_value()
        g.saturation = self.builder.get_object("saturationAdjust").get_value()

        if (len(args) > 0
                and (self.builder.get_object("gammaAdjust") == args[0]
                     or self.builder.get_object("blackLevelAdjust") == args[0]
                     or self.builder.get_object("redAdjust") == args[0]
                     or self.builder.get_object("greenAdjust") == args[0]
                     or self.builder.get_object("blueAdjust") == args[0]
                     or self.builder.get_object("saturationAdjust") == args[0]
                     or self.builder.get_object("valueAdjust") == args[0])):
            processAgain = self.sharpen.processAgain
            processColor = True
        else:
            processAgain = True
            processColor = False

        if (self.sharpen is None):
            if (self.stack is not None):
                self.sharpen = Sharpen(self.stack.stackedImage)
            else:
                self.sharpen = Sharpen(g.file, True)
        if (self.processThread != None and self.processThread.is_alive()):
            self.sharpen.processAgain = processAgain
            self.sharpen.processColorAgain = processColor
        else:
            self.processThread = Thread(target=self.sharpen.run,
                                        args=(processAgain, processColor))
            self.processThread.start()

    # Called when sharpening is complete
    def finishedSharpening(self):
        def update():
            z = self.sharpen.finalImage.astype('uint8').tobytes()
            Z = GLib.Bytes.new(z)
            pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(Z,
                                                     GdkPixbuf.Colorspace.RGB,
                                                     False, 8, self.sharpen.w,
                                                     self.sharpen.h,
                                                     self.sharpen.w * 3)
            self.frame.set_from_pixbuf(pixbuf)

        GLib.idle_add(update)

    # Closes the application
    def close(self, *args):
        self.killPool()
        Gtk.main_quit()
        sys.exit()
Пример #2
0
class UI:

    LOAD_TAB = 0
    ALIGN_TAB = 1
    STACK_TAB = 2
    SHARPEN_TAB = 3

    TITLE = "AstraStack"
    VERSION = "1.4.1"

    def __init__(self):
        self.parentConn, self.childConn = Pipe(duplex=True)
        self.cleanTmp()
        self.newVersionUrl = ""
        self.video = None
        self.align = None
        self.stack = None
        self.sharpen = None
        self.mousePosition = None
        self.clickedDriftP1 = False
        self.clickedDriftP2 = False
        self.clickedProcessingArea = False

        self.builder = Gtk.Builder()
        self.builder.add_from_file("ui/ui.glade")

        self.window = self.builder.get_object("mainWindow")
        self.saveDialog = self.builder.get_object("saveDialog")
        self.openDialog = self.builder.get_object("openDialog")
        self.tabs = self.builder.get_object("tabs")
        self.cpus = self.builder.get_object("cpus")
        self.progress = self.builder.get_object("progress")
        self.frame = self.builder.get_object("frame")
        self.overlay = self.builder.get_object("overlay")
        self.frameSlider = self.builder.get_object("frameSlider")
        self.frameScale = self.builder.get_object("frameScale")
        self.startFrame = self.builder.get_object("startFrame")
        self.endFrame = self.builder.get_object("endFrame")
        self.normalize = self.builder.get_object("normalize")
        self.alignChannels = self.builder.get_object("alignChannels")
        self.transformation = self.builder.get_object("transformation")
        self.limit = self.builder.get_object("limit")
        self.limitPercent = self.builder.get_object("limitPercent")
        self.averageRadio = self.builder.get_object("averageRadio")
        self.medianRadio = self.builder.get_object("medianRadio")

        self.builder.get_object("alignTab").set_sensitive(False)
        self.builder.get_object("stackTab").set_sensitive(False)
        self.builder.get_object("processTab").set_sensitive(False)

        self.builder.get_object("blackLevel").add_mark(0, Gtk.PositionType.TOP,
                                                       None)
        self.builder.get_object("gamma").add_mark(100, Gtk.PositionType.TOP,
                                                  None)
        self.builder.get_object("value").add_mark(100, Gtk.PositionType.TOP,
                                                  None)
        self.builder.get_object("red").add_mark(255, Gtk.PositionType.TOP,
                                                None)
        self.builder.get_object("green").add_mark(255, Gtk.PositionType.TOP,
                                                  None)
        self.builder.get_object("blue").add_mark(255, Gtk.PositionType.TOP,
                                                 None)
        self.builder.get_object("saturation").add_mark(100,
                                                       Gtk.PositionType.TOP,
                                                       None)

        self.disableScroll()

        self.cpus.set_upper(min(
            61, cpu_count()))  # 61 is the maximum that Windows allows
        self.cpus.set_value(min(61, math.ceil(cpu_count() / 2)))
        g.pool = None

        self.processThread = None

        self.builder.connect_signals(self)

        # Needed so it can be temporarily removed
        self.limitPercentSignal = self.limitPercent.connect(
            "value-changed", self.setLimitPercent)

        g.driftP1 = (0, 0)
        g.driftP2 = (0, 0)

        g.processingAreaP1 = (0, 0)
        g.processingAreaP2 = (0, 0)

        self.window.show_all()
        self.checkNewVersion()
        self.setProgress()
        self.setNormalize()
        self.setAlignChannels()
        self.setTransformation()
        self.setThreads()
        self.frameScale.set_sensitive(False)
        g.reference = "0"

    # Cancels scroll event for widget
    def propagateScroll(self, widget, event):
        Gtk.propagate_event(widget.get_parent(), event)

    # Disables the scroll event from some fields
    def disableScroll(self):
        mask = Gdk.EventMask.BUTTON_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.KEY_RELEASE_MASK | Gdk.EventMask.TOUCH_MASK
        self.builder.get_object("denoiseWidget1").set_events(mask)
        self.builder.get_object("denoiseWidget2").set_events(mask)
        self.builder.get_object("denoiseWidget3").set_events(mask)
        self.builder.get_object("denoiseWidget4").set_events(mask)
        self.builder.get_object("denoiseWidget5").set_events(mask)

        self.builder.get_object("radiusWidget1").set_events(mask)
        self.builder.get_object("radiusWidget2").set_events(mask)
        self.builder.get_object("radiusWidget3").set_events(mask)
        self.builder.get_object("radiusWidget4").set_events(mask)
        self.builder.get_object("radiusWidget5").set_events(mask)

    # Sets up a listener so that processes can communicate with each other
    def createListener(self, function):
        def listener(function):
            while True:
                try:
                    msg = self.parentConn.recv()
                except:
                    return False
                if (msg == "stop"):
                    return False
                function(msg)

        thread = Thread(target=listener, args=(function, ))
        thread.start()
        return thread

    # Shows the error dialog with the given title and message
    def showErrorDialog(self, message):
        dialog = self.builder.get_object("errorDialog")
        dialog.format_secondary_text(message)
        response = dialog.run()
        dialog.hide()
        return response

    # Shows the warning dialog with the given title and message
    def showWarningDialog(self, message):
        dialog = self.builder.get_object("warningDialog")
        dialog.format_secondary_text(message)
        response = dialog.run()
        dialog.hide()
        return response

    # Opens the About dialog
    def showAbout(self, *args):
        dialog = self.builder.get_object("about")
        dialog.set_program_name(UI.TITLE)
        dialog.set_version(UI.VERSION)
        response = dialog.run()
        dialog.hide()

    # Disable inputs
    def disableUI(self):
        self.builder.get_object("sidePanel").set_sensitive(False)
        self.builder.get_object("toolbar").set_sensitive(False)

    # Enable inputs
    def enableUI(self):
        self.builder.get_object("sidePanel").set_sensitive(True)
        self.builder.get_object("toolbar").set_sensitive(True)

    # The following is needed to forcibly refresh the value spacing of the slider
    def fixFrameSliderBug(self):
        self.frameScale.set_value_pos(Gtk.PositionType.RIGHT)
        self.frameScale.set_value_pos(Gtk.PositionType.LEFT)

    # Sets the number of threads to use
    def setThreads(self, *args):
        g.nThreads = int(self.cpus.get_value())
        if (g.pool is not None):
            g.pool.shutdown()
        g.pool = ProcessPoolExecutor(g.nThreads)

    # Checks github to see if there is a new version available
    def checkNewVersion(self):
        def callUrl():
            try:
                contents = urllib.request.urlopen(
                    "https://api.github.com/repos/Finalfantasykid/AstraStack/releases/latest"
                ).read()
                obj = json.loads(contents)
                if (obj['name'] > UI.VERSION):
                    self.newVersionUrl = obj['html_url']
                    button.show()
            except:
                return

        button = self.builder.get_object("newVersion")
        button.hide()
        thread = Thread(target=callUrl, args=())
        thread.start()

    # Opens the GitHub releases page in a browser
    def clickNewVersion(self, *args):
        webbrowser.open(self.newVersionUrl)

    # Checks to see if there will be enough memory to process the image
    def checkMemory(self, w=0, h=0):
        if (Sharpen.estimateMemoryUsage(w, h) >
                psutil.virtual_memory().available):
            response = self.showWarningDialog(
                "Your system may not have enough memory to process this file, are you sure you want to continue?"
            )
            return (response == Gtk.ResponseType.YES)
        return True

    # Opens the file chooser to open load a file
    def openVideo(self, *args):
        self.openDialog.set_current_folder(path.expanduser("~"))
        self.openDialog.set_select_multiple(False)
        self.openDialog.set_filter(self.builder.get_object("videoFilter"))
        response = self.openDialog.run()
        self.openDialog.hide()
        if (response == Gtk.ResponseType.OK):
            try:
                g.file = self.openDialog.get_filename()
                self.video = Video()
                self.video.checkMemory()
                thread = Thread(target=self.video.run, args=())
                thread.start()
                self.disableUI()
            except MemoryError as error:
                self.enableUI()

    # Opens the file chooser to open load a file
    def openImageSequence(self, *args):
        self.openDialog.set_current_folder(path.expanduser("~"))
        self.openDialog.set_select_multiple(True)
        self.openDialog.set_filter(self.builder.get_object("imageFilter"))
        response = self.openDialog.run()
        self.openDialog.hide()
        if (response == Gtk.ResponseType.OK):
            try:
                g.file = self.openDialog.get_filenames()
                self.video = Video()
                self.video.checkMemory()
                thread = Thread(target=self.video.run, args=())
                thread.start()
                self.disableUI()
            except MemoryError as error:
                self.enableUI()

    # Opens the file chooser to open load a file
    def openImage(self, *args):
        self.openDialog.set_current_folder(path.expanduser("~"))
        self.openDialog.set_select_multiple(False)
        self.openDialog.set_filter(self.builder.get_object("imageFilter"))
        response = self.openDialog.run()
        self.openDialog.hide()
        if (response == Gtk.ResponseType.OK):
            self.disableUI()
            g.file = self.openDialog.get_filename()
            try:
                self.video = Video()
                self.video.mkdirs()
                img = cv2.imread(g.file)
                h, w = img.shape[:2]
                if (not self.checkMemory(w, h)):
                    raise MemoryError()
                cv2.imwrite(g.tmp + "stacked.png", img)

                self.window.set_title(path.split(g.file)[1] + " - " + UI.TITLE)
                self.saveDialog.set_current_name("")
                self.sharpen = Sharpen(g.tmp + "stacked.png", True)
                self.builder.get_object("alignTab").set_sensitive(False)
                self.builder.get_object("stackTab").set_sensitive(False)
                self.builder.get_object("processTab").set_sensitive(True)
                self.tabs.set_current_page(UI.SHARPEN_TAB)
                self.frame.set_from_file(g.tmp + "stacked.png")
            except MemoryError as error:
                pass
            except:  # Open Failed
                self.showErrorDialog(
                    "There was an error opening the image, make sure it is a valid image."
                )
            self.enableUI()

    # Opens the file chooser to save the final image
    def saveFileDialog(self, *args):
        self.saveDialog.set_current_folder(path.expanduser("~"))
        if (self.saveDialog.get_current_name() == ""):
            # Set default file to save if empty
            if (isinstance(g.file, list)):
                sList = sorted(g.file)
                self.saveDialog.set_current_name(
                    Path(sList[0]).stem + "_" + Path(sList[-1]).stem + ".png")
            else:
                self.saveDialog.set_current_name(Path(g.file).stem + ".png")
        response = self.saveDialog.run()
        if (response == Gtk.ResponseType.OK):
            fileName = self.saveDialog.get_filename()
            try:
                cv2.imwrite(
                    fileName,
                    cv2.cvtColor(self.sharpen.finalImage.astype('uint8'),
                                 cv2.COLOR_RGB2BGR))
            except:  # Save Failed
                self.showErrorDialog(
                    "There was an error saving the image, make sure it is a valid file extension."
                )
        self.saveDialog.hide()

    # Called when the video is finished loading
    def finishedVideo(self):
        def update():
            self.tabs.next_page()

            self.frameScale.set_sensitive(True)

            self.startFrame.set_lower(0)
            self.startFrame.set_upper(len(self.video.frames) - 1)
            self.startFrame.set_value(0)

            self.endFrame.set_upper(len(self.video.frames) - 1)
            self.endFrame.set_lower(0)
            self.endFrame.set_value(len(self.video.frames) - 1)

            g.driftP1 = (0, 0)
            g.driftP2 = (0, 0)
            g.processingAreaP1 = (0, 0)
            g.processingAreaP2 = (0, 0)
            g.reference = self.video.sharpest
            self.frameSlider.set_value(self.video.sharpest)

            self.setReference()
            self.setStartFrame()
            self.setEndFrame()
            self.setDriftPoint()
            self.enableUI()
            if (isinstance(g.file, list)):
                sList = sorted(g.file)
                self.window.set_title(
                    path.split(sList[0])[1] + " ... " +
                    path.split(sList[-1])[1] + " - " + UI.TITLE)
            else:
                self.window.set_title(path.split(g.file)[1] + " - " + UI.TITLE)
            self.saveDialog.set_current_name("")
            self.builder.get_object("alignTab").set_sensitive(True)
            self.builder.get_object("stackTab").set_sensitive(False)
            self.builder.get_object("processTab").set_sensitive(False)

        GLib.idle_add(update)

    # Called when the tab is changed.  Updates parts of the UI based on the tab
    def changeTab(self, notebook, page, page_num, user_data=None):
        if (page_num == UI.LOAD_TAB or page_num == UI.ALIGN_TAB):
            self.frameSlider.set_value(0)
            self.frameSlider.set_upper(len(self.video.frames) - 1)
            self.setStartFrame()
            self.setEndFrame()
            self.frameScale.show()
            self.frame.set_from_file(self.video.frames[int(
                self.frameSlider.get_value())])
        elif (page_num == UI.STACK_TAB):
            self.frameSlider.set_lower(0)
            self.frameSlider.set_upper(len(self.align.similarities) - 1)
            self.frameSlider.set_value(0)
            self.frameScale.show()
            self.frame.set_from_file(self.align.similarities[int(
                self.frameSlider.get_value())][0])
        elif (page_num == UI.SHARPEN_TAB):
            self.frameScale.hide()
            self.sharpenImage()
        self.fixFrameSliderBug()

    # Changes the image frame to the frameSlider position
    def updateImage(self, *args):
        page_num = self.tabs.get_current_page()
        if (page_num == UI.LOAD_TAB or page_num == UI.ALIGN_TAB):
            self.frame.set_from_file(self.video.frames[int(
                self.frameSlider.get_value())])
        elif (page_num == UI.STACK_TAB):
            self.frame.set_from_file(self.align.similarities[int(
                self.frameSlider.get_value())][0])

    # Draws a rectangle where the processing area is
    def drawOverlay(self, widget, cr):
        width = widget.get_allocated_width()
        height = widget.get_allocated_height()

        def drawPoint(cr, x, y):
            cr.new_sub_path()
            cr.set_line_width(2)
            cr.set_source_rgb(1, 1, 1)

            cr.arc(x, y, 2, 0, 2 * math.pi)
            cr.stroke_preserve()

            cr.set_source_rgb(1, 0, 0)
            cr.fill()

        def drawRect(cr, x1, y1, x2, y2):
            cr.rectangle(0, 0, x1, y1)
            cr.rectangle(0, y1, x1, (y2 - y1))
            cr.rectangle(0, y1, x1, height * 2)
            cr.rectangle(x1, y2, (x2 - x1), height * 2)
            cr.rectangle(x2, y2, width * 2, height * 2)
            cr.rectangle(x2, y1, width * 2, (y2 - y1))
            cr.rectangle(x2, 0, width * 2, y1)
            cr.rectangle(x1, 0, (x2 - x1), y1)

            cr.set_source_rgba(0, 0, 0, 0.25)
            cr.fill()

            cr.set_line_width(1)
            cr.set_source_rgb(1, 0, 0)

            cr.rectangle(x1, y1, (x2 - x1), (y2 - y1))
            cr.stroke()

        if (self.tabs.get_current_page() == UI.ALIGN_TAB):
            current = self.frameSlider.get_value()

            # Processing Area
            px1 = min(g.processingAreaP1[0], g.processingAreaP2[0])
            py1 = min(g.processingAreaP1[1], g.processingAreaP2[1])

            px2 = max(g.processingAreaP1[0], g.processingAreaP2[0])
            py2 = max(g.processingAreaP1[1], g.processingAreaP2[1])

            # Drift Points
            dx1 = g.driftP1[0]
            dy1 = g.driftP1[1]

            dx2 = g.driftP2[0]
            dy2 = g.driftP2[1]

            dx = 0
            dy = 0

            if (dx1 != 0 and dy1 != 0 and dx2 != 0 and dy2 != 0):
                dx = dx2 - dx1
                dy = dy2 - dy1

            if (px1 != 0 and py1 != 0 and px2 != 0 and py2 != 0):
                # Draw Processing Area Rectangle
                drawRect(
                    cr, px1 + (dx / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame),
                    py1 + (dy / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame),
                    px2 + (dx / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame),
                    py2 + (dy / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame))

            if (dx1 != 0 and dy1 != 0 and current == g.startFrame):
                # Draw point on first frame
                drawPoint(cr, dx1, dy1)

            if (dx1 != 0 and dy1 != 0 and current != g.startFrame and dx2 != 0
                    and dy2 != 0 and current != g.endFrame):
                # Draw interpolated point
                drawPoint(
                    cr, dx1 + (dx / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame),
                    dy1 + (dy / (g.endFrame - g.startFrame)) *
                    (current - g.startFrame))

            if (dx2 != 0 and dy2 != 0 and current == g.endFrame):
                # Draw point on last frame
                drawPoint(cr, dx2, dy2)

    # Sets the reference frame to the current visible frame
    def setReference(self, *args):
        g.reference = str(int(self.frameSlider.get_value()))
        self.builder.get_object("referenceLabel").set_text(g.reference)
        self.builder.get_object("alignButton").set_sensitive(True)

    # Updates the progress bar
    def setProgress(self, i=0, total=0, text=""):
        def update():
            if (total == 0):
                pass
                self.progress.hide()
            else:
                self.progress.show()
                self.progress.set_fraction(i / total)
                self.progress.set_text(text + " " +
                                       str(round((i / total) * 100)) + "%")

        GLib.idle_add(update)

    # Sets the start frame for trimming
    def setStartFrame(self, *args):
        g.startFrame = int(self.startFrame.get_value())
        self.endFrame.set_lower(g.startFrame + 1)
        self.frameSlider.set_lower(g.startFrame)
        self.frameSlider.set_value(
            max(g.startFrame, self.frameSlider.get_value()))
        if (int(g.startFrame) > int(g.reference)):
            # Reference is outside of the range, fix it
            g.reference = str(int(g.startFrame))
            self.builder.get_object("referenceLabel").set_text(g.reference)
        self.fixFrameSliderBug()

    # Sets the end frame for trimming
    def setEndFrame(self, *args):
        g.endFrame = int(self.endFrame.get_value())
        self.startFrame.set_upper(g.endFrame - 1)
        self.frameSlider.set_upper(g.endFrame)
        self.frameSlider.set_value(
            min(g.endFrame, self.frameSlider.get_value()))
        if (int(g.endFrame) < int(g.reference)):
            # Reference is outside of the range, fix it
            g.reference = str(int(g.endFrame))
            self.builder.get_object("referenceLabel").set_text(g.reference)
        self.fixFrameSliderBug()

    # Drift Point 1 Button Clicked
    def clickDriftP1(self, *args):
        self.clickedDriftP1 = False
        self.clickedDriftP2 = False
        self.clickedProcessingArea = False
        self.setDriftPoint()
        self.frameSlider.set_value(g.startFrame)
        self.clickedDriftP1 = True
        self.window.get_window().set_cursor(
            Gdk.Cursor(Gdk.CursorType.CROSSHAIR))

    # Drift Point 2 Button Clicked
    def clickDriftP2(self, *args):
        self.clickedDriftP1 = False
        self.clickedDriftP2 = False
        self.clickedProcessingArea = False
        self.setDriftPoint()
        self.frameSlider.set_value(g.endFrame)
        self.clickedDriftP2 = True
        self.window.get_window().set_cursor(
            Gdk.Cursor(Gdk.CursorType.CROSSHAIR))

    # Reset Drift Point 1 to (0, 0)
    def resetDriftP1(self, widget, event):
        if (event.button == 3):  # Right Click
            g.driftP1 = (0, 0)
            self.clickedDriftP1 = False
            self.clickedDriftP2 = False
            self.clickedProcessingArea = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
            self.overlay.queue_draw()

    # Reset Drift Point 2 to (0, 0)
    def resetDriftP2(self, widget, event):
        if (event.button == 3):  # Right Click
            g.driftP2 = (0, 0)
            self.clickedDriftP1 = False
            self.clickedDriftP2 = False
            self.clickedProcessingArea = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
            self.overlay.queue_draw()

    # Updates the drift point
    def setDriftPoint(self, *args):
        if (self.clickedDriftP1 or self.clickedDriftP2):
            if (self.clickedDriftP1):
                g.driftP1 = self.mousePosition
            elif (self.clickedDriftP2):
                g.driftP2 = self.mousePosition
            self.clickedDriftP1 = False
            self.clickedDriftP2 = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
            self.overlay.queue_draw()

    # Processing area button clicked
    def clickProcessingArea(self, *args):
        g.processingAreaP1 = (0, 0)
        g.processingAreaP2 = (0, 0)
        self.clickedDriftP1 = False
        self.clickedDriftP2 = False
        self.clickedProcessingArea = True
        self.frameSlider.set_value(g.startFrame)
        self.window.get_window().set_cursor(
            Gdk.Cursor(Gdk.CursorType.CROSSHAIR))
        self.overlay.queue_draw()

    # Reset Processing Area to (0, 0)
    def resetProcessingArea(self, widget, event):
        if (event.button == 3):  # Right Click
            g.processingAreaP1 = (0, 0)
            g.processingAreaP2 = (0, 0)
            self.clickedDriftP1 = False
            self.clickedDriftP2 = False
            self.clickedProcessingArea = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
            self.overlay.queue_draw()

    # First point int the processing area clicked, drag started
    def dragBegin(self, *args):
        if (self.clickedProcessingArea):
            g.processingAreaP1 = self.mousePosition

    # Mouse released after dragging processing area
    def dragEnd(self, *args):
        if (self.clickedProcessingArea):
            g.processingAreaP2 = self.mousePosition
            self.clickedProcessingArea = False
            self.window.get_window().set_cursor(
                Gdk.Cursor(Gdk.CursorType.LEFT_PTR))

    # Called when the mouse moves over the frame
    def updateMousePosition(self, *args):
        pointer = self.frame.get_pointer()
        self.mousePosition = (min(max(0, pointer.x),
                                  self.frame.get_allocation().width),
                              min(max(0, pointer.y),
                                  self.frame.get_allocation().height))
        if (self.clickedProcessingArea):
            if (g.processingAreaP1 != (0, 0)):
                g.processingAreaP2 = self.mousePosition
            self.overlay.queue_draw()

    # Sets whether or not to normalize the frames during alignment
    def setNormalize(self, *args):
        g.normalize = self.normalize.get_active()

    # Sets whether or not to align channels separately
    def setAlignChannels(self, *args):
        g.alignChannels = self.alignChannels.get_active()

    # Sets the type of transformation
    def setTransformation(self, *args):
        text = self.transformation.get_active_text()
        if (text == "Translation"):
            g.transformation = StackReg.TRANSLATION
        elif (text == "Rigid Body"):
            g.transformation = StackReg.RIGID_BODY
        elif (text == "Scaled Rotation"):
            g.transformation = StackReg.SCALED_ROTATION
        elif (text == "Affine"):
            g.transformation = StackReg.AFFINE

    # Runs the Alignment step
    def clickAlign(self, *args):
        self.align = Align(self.video.frames[g.startFrame:g.endFrame + 1])
        thread = Thread(target=self.align.run, args=())
        thread.start()
        self.disableUI()

    # Called when the Alignment is complete
    def finishedAlign(self):
        def update():
            self.tabs.next_page()
            self.enableUI()
            self.builder.get_object("alignTab").set_sensitive(True)
            self.builder.get_object("stackTab").set_sensitive(True)
            self.builder.get_object("processTab").set_sensitive(False)
            self.limit.set_upper(len(self.align.similarities))
            self.limit.set_value(int(len(self.align.similarities) / 2))
            self.limitPercent.set_value(
                round(self.limit.get_value() / len(self.align.similarities) *
                      100))
            self.setLimit()
            self.setLimitPercent()

        GLib.idle_add(update)

    # Sets the number of frames to use in the Stack
    def setLimit(self, *args):
        self.limitPercent.disconnect(self.limitPercentSignal)
        self.limit.set_upper(len(self.align.similarities))
        g.limit = int(self.limit.get_value())
        self.limitPercent.set_value(
            round(g.limit / len(self.align.similarities) * 100))
        self.limitPercentSignal = self.limitPercent.connect(
            "value-changed", self.setLimitPercent)

    # Sets the number of frames to use in the Stack
    def setLimitPercent(self, *args):
        limitPercent = self.limitPercent.get_value() / 100
        self.limit.set_value(round(limitPercent *
                                   len(self.align.similarities)))

    # Stack Button clicked
    def clickStack(self, *args):
        self.stack = Stack(self.align.similarities)
        thread = Thread(target=self.stack.run, args=())
        thread.start()
        self.disableUI()

    # Called when the stack is complete
    def finishedStack(self):
        def update():
            self.sharpen = Sharpen(self.stack.stackedImage)
            self.tabs.next_page()
            self.frame.set_from_file(g.tmp + "stacked.png")
            self.enableUI()
            self.builder.get_object("alignTab").set_sensitive(True)
            self.builder.get_object("stackTab").set_sensitive(True)
            self.builder.get_object("processTab").set_sensitive(True)

        GLib.idle_add(update)

    # Sharpens the final Stacked image
    def sharpenImage(self, *args):
        g.sharpen1 = self.builder.get_object("sharpen1").get_value()
        g.sharpen2 = self.builder.get_object("sharpen2").get_value()
        g.sharpen3 = self.builder.get_object("sharpen3").get_value()
        g.sharpen4 = self.builder.get_object("sharpen4").get_value()
        g.sharpen5 = self.builder.get_object("sharpen5").get_value()

        g.radius1 = self.builder.get_object("radius1").get_value()
        g.radius2 = self.builder.get_object("radius2").get_value()
        g.radius3 = self.builder.get_object("radius3").get_value()
        g.radius4 = self.builder.get_object("radius4").get_value()
        g.radius5 = self.builder.get_object("radius5").get_value()

        g.denoise1 = self.builder.get_object("denoise1").get_value()
        g.denoise2 = self.builder.get_object("denoise2").get_value()
        g.denoise3 = self.builder.get_object("denoise3").get_value()
        g.denoise4 = self.builder.get_object("denoise4").get_value()
        g.denoise5 = self.builder.get_object("denoise5").get_value()

        g.level1 = self.builder.get_object("level1").get_active()
        g.level2 = self.builder.get_object("level2").get_active()
        g.level3 = self.builder.get_object("level3").get_active()
        g.level4 = self.builder.get_object("level4").get_active()
        g.level5 = self.builder.get_object("level5").get_active()

        g.gamma = self.builder.get_object("gammaAdjust").get_value()
        g.blackLevel = self.builder.get_object("blackLevelAdjust").get_value()
        g.value = self.builder.get_object("valueAdjust").get_value()

        g.redAdjust = self.builder.get_object("redAdjust").get_value()
        g.greenAdjust = self.builder.get_object("greenAdjust").get_value()
        g.blueAdjust = self.builder.get_object("blueAdjust").get_value()
        g.saturation = self.builder.get_object("saturationAdjust").get_value()

        if (len(args) > 0
                and (self.builder.get_object("gammaAdjust") == args[0]
                     or self.builder.get_object("blackLevelAdjust") == args[0]
                     or self.builder.get_object("redAdjust") == args[0]
                     or self.builder.get_object("greenAdjust") == args[0]
                     or self.builder.get_object("blueAdjust") == args[0]
                     or self.builder.get_object("saturationAdjust") == args[0]
                     or self.builder.get_object("valueAdjust") == args[0])):
            processAgain = self.sharpen.processAgain
            processColor = True
        else:
            processAgain = True
            processColor = False

        if (self.sharpen is None):
            if (self.stack is not None):
                self.sharpen = Sharpen(self.stack.stackedImage)
            else:
                self.sharpen = Sharpen(g.tmp + "stacked.png", True)
        if (self.processThread != None and self.processThread.is_alive()):
            self.sharpen.processAgain = processAgain
            self.sharpen.processColorAgain = processColor
        else:
            self.processThread = Thread(target=self.sharpen.run,
                                        args=(processAgain, processColor))
            self.processThread.start()

    # Called when sharpening is complete
    def finishedSharpening(self):
        def update():
            z = self.sharpen.finalImage.astype('uint8').tobytes()
            Z = GLib.Bytes.new(z)
            pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(Z,
                                                     GdkPixbuf.Colorspace.RGB,
                                                     False, 8, self.sharpen.w,
                                                     self.sharpen.h,
                                                     self.sharpen.w * 3)
            self.frame.set_from_pixbuf(pixbuf)

        GLib.idle_add(update)

    # Cleans the tmp directory
    def cleanTmp(self):
        if (path.exists(g.tmp)):
            if (path.exists(g.tmp + "frames")):
                for file in scandir(g.tmp + "frames"):
                    unlink(file.path)
            if (path.exists(g.tmp + "cache")):
                for file in scandir(g.tmp + "cache"):
                    unlink(file.path)
            for file in scandir(g.tmp):
                if (path.isdir(file)):
                    rmdir(file.path)
                else:
                    unlink(file.path)
            rmdir(g.tmp)

    # Closes the application
    def close(self, *args):
        Gtk.main_quit()
        self.cleanTmp()