Browse Source

Added caching in places such as layers and layer masks for speed,
Layer masks are now visible (in red) when the edit button is clicked,
The preview (in the actual raster editor anyway) now converts the OpenCV
object into a Pixbuf without a temp file.

Signed-off-by: Billy Barrow <billyb@pcthingz.com>

Billy Barrow 8 years ago
parent
commit
446a890724
4 changed files with 268 additions and 100 deletions
  1. 33 9
      PF2/Layer.py
  2. 6 3
      PF2/VectorMask/Path.py
  3. 17 9
      PF2/VectorMask/__init__.py
  4. 212 79
      PF2/__init__.py

+ 33 - 9
PF2/Layer.py

@@ -4,7 +4,6 @@ import cv2
 
 class Layer:
     def __init__(self, base, name, on_change):
-        print("init", name)
         self.mask = VectorMask.VectorMask()
         self.tools = []
         self.tool_map = {}
@@ -27,12 +26,13 @@ class Layer:
         self.tool_stack.set_transition_type(6)
         self.tool_stack.set_homogeneous(False)
 
+        self.layer_copy = None
+
 
         self.layer_changed = on_change
 
 
     def add_tool(self, tool):
-        print("add tool", tool)
         self.tool_box.add(tool.tool_button)
         self.tool_stack.add(tool.widget)
         self.tool_map[tool.tool_button] = tool
@@ -78,7 +78,6 @@ class Layer:
         return layerDict
 
     def set_from_layer_dict(self, dict):
-        print("set_from_layer_dict", dict)
         # Load Tool Data
         for tool in self.tools:
             if(tool.id in dict["tools"]):
@@ -135,18 +134,43 @@ class Layer:
             # Here we would blend with the mask
             else:
                 mask_map = self.mask.get_mask_map()
-                height, width = layer.shape[:2]
-                mask_map = cv2.resize(mask_map, (width, height), interpolation=cv2.INTER_AREA)
-                mask_map = mask_map * self.opacity
+                if(mask_map != None):
+                    # Only process if there is actually an existing mask
+                    height, width = layer.shape[:2]
+                    mask_map = cv2.resize(mask_map, (width, height), interpolation=cv2.INTER_AREA)
+                    mask_map = mask_map * self.opacity
+
+                    inverted_map = (mask_map * -1) + 1
+                    for i in range(0,3):
+                        image[0:, 0:, i] = (layer[0:, 0:, i] * mask_map) + (image[0:, 0:, i] * inverted_map)
+
+        self.layer_copy = image.copy()
+
+        return image
+
+    def get_layer_mask_preview(self, image):
+        mask =  self.mask.get_mask_map()
 
-                inverted_map = (mask_map * -1) + 1
-                for i in range(0,3):
-                    image[0:, 0:, i] = (layer[0:, 0:, i] * mask_map) + (image[0:, 0:, i] * inverted_map)
+        if(mask != None):
+            w, h = image.shape[:2]
+
+            # Bits per pixel
+            bpp = float(str(image.dtype).replace("uint", "").replace("float", ""))
+            # Pixel value range
+            np = float(2 ** bpp - 1)
+
+            mask = cv2.resize(mask, (h, w), interpolation=cv2.INTER_AREA)
+            inverted_map = (mask * -1) + 1
+            image[0:, 0:, 2] = (np * mask) + (image[0:, 0:, 2] * inverted_map)
 
         return image
 
 
 
+
+
+
+
     def show_all(self):
 
         self.tool_box.show_all()

+ 6 - 3
PF2/VectorMask/Path.py

@@ -10,22 +10,25 @@ class Path:
         self.scale = scale
         self.brush_feather = brush_feather
         self.additive = additive
+        self.has_rendered = False
 
     def add_point(self, x, y, previewShape = None, fill = None):
         self.points.append([x,y])
+        self.has_rendered = False
         if(previewShape != None and fill != None and len(self.points) > 1):
             preview = numpy.zeros(previewShape, dtype=numpy.uint8)
             points = self.points[-2:]
-            print(points)
             sx = points[0][0]
             sy = points[0][1]
             fx = points[1][0]
             fy = points[1][1]
             cv2.line(preview, (sx, sy), (fx, fy), (fill), int(self.brush_size), cv2.LINE_4)
+
             return preview
 
 
     def get_mask_map(self, mask):
+        self.has_rendered = True
         if(self.additive):
             return self.draw_additive_path(mask)
         else:
@@ -42,11 +45,11 @@ class Path:
 
         if(self.brush_feather > 1):
             blur_size = 2 * round((round(self.brush_feather) + 1) / 2) - 1
-            map = cv2.GaussianBlur(map, (int(blur_size), int(blur_size)), 0)
+            map = cv2.blur(map, (int(blur_size), int(blur_size)))
             map = map[:, :, numpy.newaxis]
 
         # Painful workaround for int overflow
-        mask2 = mask.astype(numpy.uint64)
+        mask2 = mask.astype(numpy.uint16)
         mask2 = mask2 + map
         mask2[mask2 > 255] = 255
         mask = mask2.astype(numpy.uint8)

+ 17 - 9
PF2/VectorMask/__init__.py

@@ -7,6 +7,7 @@ class VectorMask:
         self.paths = []
         self.width = 0
         self.height = 0
+        self.__map = None
 
     def set_dimentions(self, width, height):
         self.width = width
@@ -18,21 +19,28 @@ class VectorMask:
         return path
 
     def get_mask_map(self):
-        map = numpy.zeros((self.height, self.width, 1), dtype=numpy.uint8)
+        if(self.has_updated()):
 
-        print(map.shape)
+            map = numpy.zeros((self.height, self.width, 1), dtype=numpy.uint8)
 
-        for path in self.paths:
-            map = path.get_mask_map(map)
+            for path in self.paths:
+                map = path.get_mask_map(map)
+
+            map32 = map.astype(numpy.float32)
+            map32 = map32 / 255.0
+
+            self.__map = map32
 
-        print(map.shape)
+        return self.__map
+
+    def has_updated(self):
+        res = True
+        for path in self.paths:
+            res &= path.has_rendered
 
-        map32 = map.astype(numpy.float32)
-        map32 = map32 / 255.0
+        return not res
 
-        print(map32.shape)
 
-        return map32
 
     def get_vector_mask_dict(self):
         paths = []

+ 212 - 79
PF2/__init__.py

@@ -143,6 +143,14 @@ class PF2(Activity.Activity):
         self.mousedown = False
         self.layer_order = []
         self.pre_undo_layer_name = "base"
+        self.pre_undo_editing = False
+        self.pre_undo_erasing = False
+        self.last_selected_layer = None
+        self.was_editing_mask = False
+        self.source_image = None
+        self.is_scaling = False
+        self.current_processing_layer_name = "base"
+        self.current_processing_layer_index = 0
 
         # Setup Open Dialog
         self.ui["open_window"].set_transient_for(self.root)
@@ -303,14 +311,32 @@ class PF2(Activity.Activity):
 
 
     def on_layer_change(self, layer):
+        print(layer, "changed")
         if(self.has_loaded):
             if(not self.change_occurred):
                 self.change_occurred = True
-                thread = threading.Thread(target=self.update_image)
+                thread = threading.Thread(target=self.update_image, args=(False, layer))
                 thread.start()
             else:
                 self.additional_change_occurred = True
 
+    def on_layer_mask_change(self, layer):
+        if(self.has_loaded):
+            if(not self.change_occurred):
+                self.change_occurred = True
+                threading.Thread(target=self.__on_layer_mask_change).start()
+                self.save_image_data()
+
+    def __on_layer_mask_change(self):
+        image = self.get_selected_layer().layer_copy.copy()
+        image = self.get_selected_layer().get_layer_mask_preview(image)
+        self.image_is_dirty = True
+        self.image = image
+        self.update_preview()
+        self.change_occurred = False
+        self.image_is_dirty = False
+
+
     def on_lower_peak_toggled(self, sender):
         self.lower_peak_on = sender.get_active()
         if (self.lower_peak_on):
@@ -329,8 +355,8 @@ class PF2(Activity.Activity):
             thread = threading.Thread(target=self.update_image, args=(True,))
             thread.start()
 
-    def image_opened(self, depth):
-        self.root.get_titlebar().set_subtitle("%s (%s Bit)" % (self.image_path, depth))
+    def image_opened(self):
+        self.root.get_titlebar().set_subtitle("%s (%s Bit)" % (self.image_path, self.bit_depth))
         self.hide_message()
 
 
@@ -357,12 +383,16 @@ class PF2(Activity.Activity):
 
     def on_undo(self, sender):
         self.pre_undo_layer_name = self.get_selected_layer().name
+        self.pre_undo_erasing = self.ui["mask_erase_toggle"].get_active()
+        self.pre_undo_editing = self.ui["edit_layer_mask_button"].get_active()
         self.undo_position -= 1
         self.update_undo_state()
         self.update_from_undo_stack(self.undo_stack[self.undo_position])
 
     def on_redo(self, sender):
         self.pre_undo_layer_name = self.get_selected_layer().name
+        self.pre_undo_erasing = self.ui["mask_erase_toggle"].get_active()
+        self.pre_undo_editing = self.ui["edit_layer_mask_button"].get_active()
         self.undo_position += 1
         self.update_undo_state()
         self.update_from_undo_stack(self.undo_stack[self.undo_position])
@@ -375,82 +405,109 @@ class PF2(Activity.Activity):
 
     ## Background Tasks ##
     def open_image(self, w, h):
+
         self.load_image_data()
         try:
+            # Get Bit Depth
+            imdepth = cv2.imread(self.image_path, 2 | 1)
+            self.bit_depth = str(imdepth.dtype).replace("uint", "").replace("float", "")
+
+            # Read the 8Bit Preview
+            self.source_image = cv2.imread(self.image_path).astype(numpy.uint8)
+
+            # Load the image
             self.resize_preview(w, h)
         except:
             pass
         while(self.image == None):
             time.sleep(1)
-        GLib.idle_add(self.image_opened, str(self.image.dtype).replace("uint", "").replace("float", ""))
+        GLib.idle_add(self.image_opened)
         time.sleep(1)
         self.has_loaded = True
 
 
 
     def resize_preview(self, w, h):
-        # Inhibit undo stack to prevent
-        # Adding an action on resize
-        self.undoing = True
+        if(self.source_image != None):
+            # Inhibit undo stack to prevent
+            # Adding an action on resize
+            self.undoing = True
 
-        self.original_image = cv2.imread(self.image_path, 2 | 1)
-        height, width = self.original_image.shape[:2]
 
-        self.aheight = height
-        self.awidth = width
 
-        self.pheight = h
-        self.pwidth = w
+            self.original_image = self.source_image.copy()
 
-        # Get fitting size
-        ratio = float(w)/width
-        if(height*ratio > h):
-            ratio = float(h)/height
+            height, width = self.original_image.shape[:2]
 
-        nw = width * ratio
-        nh = height * ratio
 
-        # Do quick ui resize
-        if(self.image != None) and (os.path.exists("/tmp/phf2-preview-%s.png" % getpass.getuser())):
-            # If we have an edited version, show that
-            self.pimage = GdkPixbuf.Pixbuf.new_from_file_at_scale("/tmp/phf2-preview-%s.png" % getpass.getuser(),
-                                                                         int(nw), int(nh), True)
-            GLib.idle_add(self.show_current)
+            self.aheight = height
+            self.awidth = width
 
-        self.poriginal = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.image_path,
-                                                            int(nw), int(nh), True)
-        if(self.image == None):
-            # Otherwise show the original
-            GLib.idle_add(self.show_original)
+            self.pheight = h
+            self.pwidth = w
 
+            # Get fitting size
+            ratio = float(w)/width
+            if(height*ratio > h):
+                ratio = float(h)/height
 
-        # Resize OPENCV Copy
-        self.original_image = cv2.resize(self.original_image, (int(nw), int(nh)), interpolation = cv2.INTER_AREA)
+            nw = width * ratio
+            nh = height * ratio
 
-        self.image_is_dirty = True
 
-        self.image = numpy.copy(self.original_image)
+            # Do quick ui resize
+            # if(self.image != None):
+            #     # If we have an edited version, show that
+            #     image = cv2.resize(self.image, (int(nw), int(nh)), interpolation=cv2.INTER_NEAREST)
+            #     self.pimage = self.pimage = self.create_pixbuf(image)
+            #     GLib.idle_add(self.show_current)
 
-        # Update image
-        if (not self.change_occurred):
-            self.change_occurred = True
-            self.update_image()
-        else:
-            self.additional_change_occurred = True
+            if(self.pimage != None and not self.is_scaling):
+                self.is_scaling = True
+                # If we have an edited version, show that
+                image = self.pimage.scale_simple(int(nw), int(nh), GdkPixbuf.InterpType.NEAREST)
+                self.pimage = image
+                GLib.idle_add(self.show_current)
+                self.is_scaling = False
+
+            self.poriginal = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.image_path,
+                                                                int(nw), int(nh), True)
+            if(self.pimage == None):
+                # Otherwise show the original
+                GLib.idle_add(self.show_original)
+
+
+            # Resize OPENCV Copy
+            self.original_image = cv2.resize(self.original_image, (int(nw), int(nh)), interpolation = cv2.INTER_AREA)
 
-    def update_image(self, immediate=False):
+            self.image_is_dirty = True
+
+            self.image = self.original_image.copy()
+
+            # Update image
+            if (not self.change_occurred):
+                self.change_occurred = True
+                self.update_image()
+            else:
+                self.additional_change_occurred = True
+
+
+
+    def update_image(self, immediate=False, changed_layer=None):
         if(not immediate):
             time.sleep(0.5)
         self.additional_change_occurred = False
         GLib.idle_add(self.start_work)
         image = numpy.copy(self.original_image)
-        self.image = self.run_stack(image)
+        rst = time.time()
+        self.image = self.run_stack(image, changed_layer=changed_layer)
+        self.process_mask()
         self.image_is_dirty = False
         if(self.additional_change_occurred):
             self.update_image()
         else:
             self.save_image_data()
-            self.draw_hist(self.image)
+            threading.Thread(target=self.draw_hist, args=(self.image,)).start()
             self.process_peaks()
             self.update_preview()
             GLib.idle_add(self.stop_work)
@@ -459,15 +516,41 @@ class PF2(Activity.Activity):
 
 
 
-    def run_stack(self, image, callback=None):
+    def run_stack(self, image, callback=None, changed_layer=None):
         if(not self.running_stack):
             self.running_stack = True
 
             baseImage = image.copy()
 
-            for layer in self.layers:
-                print(layer)
-                image = layer.render_layer(baseImage, image, callback)
+            image = None
+
+            carry = True
+            if(baseImage.shape == self.image.shape) and (changed_layer != None) and (changed_layer in self.layers):
+                changed_layer_index = self.layers.index(changed_layer)
+                if(changed_layer_index > 0):
+                    carry = False
+                    layer_under = self.layers[changed_layer_index -1]
+                    image = layer_under.layer_copy.copy()
+                    for layer in self.layers[changed_layer_index: ]:
+                        self.current_processing_layer_name = layer.name
+                        self.current_processing_layer_index = self.layers.index(layer)
+                        if(self.additional_change_occurred):
+                            break
+                        print(layer)
+                        image = layer.render_layer(baseImage, image, callback)
+
+
+            if(carry):
+                for layer in self.layers:
+                    self.current_processing_layer_name = layer.name
+                    self.current_processing_layer_index = self.layers.index(layer)
+                    if (self.additional_change_occurred):
+                        break
+                    print(layer)
+                    image = layer.render_layer(baseImage, image, callback)
+
+
+
 
             self.running_stack = False
             return image
@@ -479,12 +562,27 @@ class PF2(Activity.Activity):
             return self.run_stack(image, callback)
 
     def update_preview(self):
-        fname = "/tmp/phf2-preview-%s.png" % getpass.getuser()
-        if(os.path.exists(fname)):
-            os.unlink(fname)
-        cv2.imwrite(fname, self.image)
-        self.pimage = GdkPixbuf.Pixbuf.new_from_file(fname)
-        GLib.idle_add(self.show_current)
+        if(not self.is_scaling):
+            image = self.create_pixbuf(self.image)
+            self.pimage = image
+
+            GLib.idle_add(self.show_current)
+
+
+
+    def create_pixbuf(self, im):
+        image = cv2.cvtColor(im.copy(), cv2.COLOR_BGR2RGB).astype(numpy.uint8)
+
+        pb = GdkPixbuf.Pixbuf.new_from_data(image.tostring(),
+                                            GdkPixbuf.Colorspace.RGB,
+                                            False,
+                                            8,
+                                            self.image.shape[1],
+                                            self.image.shape[0],
+                                            self.image.shape[2] * self.image.shape[1])
+
+        return pb
+
 
     def draw_hist(self, image):
         path = "/tmp/phf2-hist-%s.png" % getpass.getuser()
@@ -508,6 +606,14 @@ class PF2(Activity.Activity):
         if(do_update):
             self.update_preview()
 
+    def process_mask(self, do_update=False):
+        if(self.ui["edit_layer_mask_button"].get_active()):
+            self.image = self.get_selected_layer().get_layer_mask_preview(self.image)
+
+        if (do_update):
+            self.update_preview()
+
+
 
     ## FILE STUFF ##
 
@@ -582,7 +688,7 @@ class PF2(Activity.Activity):
             self.undo_position = 0
 
         GLib.idle_add(self.update_undo_state)
-        time.sleep(2)
+        #time.sleep(2)
         self.undoing = False
 
 
@@ -598,15 +704,17 @@ class PF2(Activity.Activity):
     def update_from_undo_stack(self, data):
         self.undoing = True
         self.delete_all_editable_layers()
+        print(data["layer-order"])
         for layer_name in data["layer-order"]:
             if (layer_name == "base"):
                 self.base_layer.set_from_layer_dict(data["layers"][layer_name])
             else:
-                ilayer = self.create_layer(layer_name, False, layer_name == self.pre_undo_layer_name)
+                ilayer = self.create_layer(layer_name, False, layer_name == self.pre_undo_layer_name, True)
                 ilayer.set_from_layer_dict(data["layers"][layer_name])
                 self.show_layers()
 
 
+
     def get_export_image(self, w, h):
         GLib.idle_add(self.on_export_started)
         GLib.idle_add(self.show_message, "Exporting Photo", "Please wait…", True, True)
@@ -618,8 +726,13 @@ class PF2(Activity.Activity):
         return img
 
     def export_progress_callback(self, name, count, current):
-        GLib.idle_add(self.show_message, "Exporting Photo", "Processing: %s" % name, True, True)
-        GLib.idle_add(self.update_message_progress, current, count)
+        layer_name = self.current_processing_layer_name
+        if(layer_name == "base"):
+            layer_name = "Base Layer"
+        GLib.idle_add(self.show_message, "Exporting Photo", "%s: %s" % (layer_name,
+                                                                                    name), True, True)
+        GLib.idle_add(self.update_message_progress, current+(self.current_processing_layer_index*count),
+                      count*len(self.layers))
 
 
 
@@ -649,28 +762,32 @@ class PF2(Activity.Activity):
 
             print(x, y)
 
-            if(not self.image_is_dirty):
-                fill = (0, 0, 255)
-                if(erase):
-                    fill = (255, 0, 0)
-
-                preview = self.current_layer_path.add_point(int(x), int(y), (pheight, pwidth, 3), fill)
+            fill = (0, 0, 255)
+            if(erase):
+                fill = (255, 0, 0)
 
-                # Bits per pixel
-                bpp = float(str(self.image.dtype).replace("uint", "").replace("float", ""))
-                # Pixel value range
-                np = float(2 ** bpp - 1)
+            self.draw_path(x, y, pheight, pwidth, fill)
 
-                self.image[preview == 255] = np
-                cv2.imwrite("/tmp/phf2-preview-%s-drag.png" % getpass.getuser(), self.image)
-                temppbuf = GdkPixbuf.Pixbuf.new_from_file("/tmp/phf2-preview-%s-drag.png" % getpass.getuser())
-                self.ui["preview"].set_from_pixbuf(temppbuf)
-
-            self.on_layer_change(layer)
+            self.on_layer_mask_change(layer)
 
             self.mouse_down_coords_changed(widget, event)
             return True
 
+
+    def draw_path(self, x, y, pheight, pwidth, fill):
+        preview = self.current_layer_path.add_point(int(x), int(y), (pheight, pwidth, 3), fill)
+
+        # Bits per pixel
+        bpp = float(str(self.image.dtype).replace("uint", "").replace("float", ""))
+        # Pixel value range
+        np = float(2 ** bpp - 1)
+
+        self.image[preview == 255] = np
+        cv2.imwrite("/tmp/phf2-preview-%s-drag.png" % getpass.getuser(), self.image)
+        temppbuf = GdkPixbuf.Pixbuf.new_from_file("/tmp/phf2-preview-%s-drag.png" % getpass.getuser())
+        self.ui["preview"].set_from_pixbuf(temppbuf)
+
+
     def new_path(self, widget, event):
         draw = self.ui["mask_draw_toggle"].get_active()
         erase = self.ui["mask_erase_toggle"].get_active()
@@ -696,13 +813,21 @@ class PF2(Activity.Activity):
         self.ui["mask_draw_toggle"].set_active(widget.get_active())
         self.ui["mask_erase_toggle"].set_active(False)
         self.ui["scroll_window"].queue_draw()
+        if (widget.get_active()):
+            thread = threading.Thread(target=self.process_mask, args=(True,))
+            thread.start()
+        elif(self.was_editing_mask):
+            self.on_layer_change(self.get_selected_layer())
+
+        self.was_editing_mask = widget.get_active()
 
     def update_layer_opacity(self, sender):
         layer = self.get_selected_layer()
-        layer.set_opacity(sender.get_value())
+        if(layer.opacity != sender.get_value()):
+            layer.set_opacity(sender.get_value())
 
 
-    def create_layer(self, layer_name, is_base, select = False):
+    def create_layer(self, layer_name, is_base, select = False, set_pre_undo_draw_state = False):
         layer = Layer.Layer(is_base, layer_name, self.on_layer_change)
         for tool in self.tools:
             tool_instance = tool()
@@ -710,11 +835,11 @@ class PF2(Activity.Activity):
 
         self.layers += [layer,]
 
-        GLib.idle_add(self.create_layer_ui, layer, select)
+        GLib.idle_add(self.create_layer_ui, layer, select, set_pre_undo_draw_state)
 
         return layer
 
-    def create_layer_ui(self, layer, select):
+    def create_layer_ui(self, layer, select, set_pre_undo_draw_state = False):
         layer_box = Gtk.HBox()
         layer_box.set_hexpand(False)
         layer_box.set_halign(Gtk.Align.START)
@@ -755,6 +880,11 @@ class PF2(Activity.Activity):
         if(select):
             self.select_layer(layer)
 
+        if(set_pre_undo_draw_state) and self.get_selected_layer().editable:
+            print(self.pre_undo_editing, self.pre_undo_erasing)
+            self.ui["mask_erase_toggle"].set_active(self.pre_undo_erasing)
+            self.ui["edit_layer_mask_button"].set_active(self.pre_undo_editing)
+
 
     def layer_ui_activated(self, widget, row):
         layer_index = row.get_index()
@@ -768,6 +898,8 @@ class PF2(Activity.Activity):
         self.ui["layer_opacity"].set_value(self.layers[layer_index].opacity)
         if(self.ui["edit_layer_mask_button"].get_active()):
             self.ui["edit_layer_mask_button"].set_active(self.layers[layer_index].editable)
+            self.on_layer_change(self.last_selected_layer)
+            self.last_selected_layer = self.get_selected_layer()
 
     def toggle_layer(self, sender, layer):
         layer.set_enabled(sender.get_active())
@@ -858,7 +990,7 @@ class PF2(Activity.Activity):
                 context.set_source_rgb(255, 0, 0)
             else:
                 context.set_source_rgb(255, 255, 255)
-            context.arc(self.mousex, self.mousey, size/2.0, 0.0, 2 * numpy.pi);
+            context.arc(self.mousex, self.mousey, size/2.0, 0.0, 2 * numpy.pi)
             context.stroke()
 
     def mouse_coords_changed(self, widget, event):
@@ -876,4 +1008,5 @@ class PF2(Activity.Activity):
 
     def layer_blend_mode_changed(self, sender):
         layer = self.get_selected_layer()
-        layer.set_blending_mode(sender.get_active_text().lower())
+        if(layer.blend_mode != sender.get_active_text().lower()):
+            layer.set_blending_mode(sender.get_active_text().lower())