new feature for comix (with patch)


antisol

Member
Joined
Feb 9, 2014
Messages
70
Hi,

I've hacked a new feature into comix for use on my pandora, and I figured I'd share it here first.

The new feature is called "smart crop", and it automatically crops the borders from the image to maximise the viewing area. I find it useful for viewing my old x-men comics, which have a large white border area. The crop feature will work for any border colour and has an adjustable threshold in preferences in order to set its sensitivity. The feature can be toggled with the "s" key.

I've bundled the modified files into a tarball. To use it on your pandora, you can create a folder called src in pandora/appdata/comix and extract the tarball there. The tarball is at: http://antisol.org/comix_files.tgz

I'll also send this in to the comix maintainers, but it would be great if the PND maintainer could consider including this in the PND.

Here's the diff:

Code:
diff --git a/image.py b/image.py
index 0603f0a..e1ad8c7 100644
--- a/image.py
+++ b/image.py
@@ -5,6 +5,7 @@ import Image
 import ImageEnhance
 import ImageOps
 import ImageStat
+import math
 
 from preferences import prefs
 
@@ -133,6 +134,106 @@ def add_border(pixbuf, thickness, colour=0x000000FF):
         canvas, thickness, thickness)
     return canvas
 
+def auto_crop(pixbuf,threshold=192,padding=3):
+    """
+    Return a pixbuf with borders auto-cropped from the image
+    threshold is the percentage of difference allowed in colour
+    padding controls both the amount of border space allowed (approximate)
+        and the granularity and speed of the search (lower padding value ==
+        slower crop, you don't want to use 1 for this)
+    """
+    debug=False
+    
+    r,g,b,a = get_most_common_edge_colour(pixbuf)
+    r = r >> 8
+    g = g >> 8
+    b = b >> 8
+    
+    height = pixbuf.get_height()
+    width = pixbuf.get_width()
+
+    top=0
+    bottom=int((height*0.5))
+    left=int(width*.5)
+    right=int(width*.5)
+    
+    #print "Image is %sx%s, initial crop %s %s %s %s." % (width,height,top,bottom,left,right)
+    
+    im = pixbuf_to_pil(pixbuf)
+    data = im.load()
+    
+    top_found = False
+    
+    for row in range(0,height-1,padding):
+        #scan for left edge:
+        for col in range(0,left,padding):
+            r2,g2,b2 = data[col,row]
+            distance = math.sqrt(((r2-r)**2) + ((g2-g)**2) + ((b2-**2))
+            if distance > threshold:
+                left=col
+                if debug:
+                    print "Pixel at %s,%s is %s,%s,%s. distance: %s. Left crop is %s" % (col,row,r2,g2,b2,distance,left)
+                break
+        
+        #scan for right edge:
+        for col in range(width-1,right,padding*-1):
+            r2,g2,b2 = data[col,row]
+            distance = math.sqrt(((r2-r)**2) + ((g2-g)**2) + ((b2-**2))
+            if distance > threshold:
+                right=col
+                if debug:
+                    print "Pixel at %s,%s is %s,%s,%s. distance: %s Right crop is: %s" % (col,row,r2,g2,b2,distance,right)
+                break
+                    
+        if (not top_found):
+            #scan the rest of the row to look for top edge:
+            for col in range(left,right,padding):
+                r2,g2,b2 = data[col,row]
+                distance = math.sqrt(((r2-r)**2) + ((g2-g)**2) + ((b2-**2))
+                if distance > threshold:
+                    if debug:
+                        print "Pixel at %s,%s is %s,%s,%s. distance: %s Top crop is: %s" % (col,row,r2,g2,b2,distance,right)
+                    top=row
+                    top_found = True
+                    break;
+        
+    #search for bottom edge:
+    found=False
+    for row in range(height-1,bottom,padding * -1):
+        for col in range(0,width-1,padding):
+            r2,g2,b2 = data[col,row]
+            distance = math.sqrt(((r2-r)**2) + ((g2-g)**2) + ((b2-**2))
+            if distance > threshold:
+                bottom=row
+                found=True
+                if debug:
+                    print "Pixel at %s,%s is %s,%s,%s. distance: %s Bottom crop is: %s" % (col,row,r2,g2,b2,distance,right)
+                break;
+        if found: break
+    
+    #add double the padding width to ensure that nothing is cropped
+    #    due to granularity
+    left=int(left-(padding*2))
+    right=int(right+(padding*2))
+    top=int(top-(padding*2))
+    bottom = int(bottom+(padding*2))
+    
+    #be sane:
+    if left<0: left = 0
+    if right>(width-1): right = width-1
+    if top<0: top = 0
+    if bottom > (height - 1): bottom=height-1
+    
+    if debug:
+        print "Smart Crop mode: Image is %5dx%5d. Border colour: %s,%s,%s. Crop %s %s %s %s" % (width,height,r,g,b,top,bottom,left,right)
+    
+    ret = pixbuf.subpixbuf(left, top, (right-left), (bottom-top))
+    
+    if (not ret): #this should maybe be "if ret is not a pixbuf"
+        #something broke! abort! abort!
+        ret = pixbuf
+    
+    return ret
 
 def get_most_common_edge_colour(pixbuf):
     """Return the most commonly occurring pixel value along the four edges
diff --git a/main.py b/main.py
index 09abae2..7209610 100644
--- a/main.py
+++ b/main.py
@@ -116,6 +116,9 @@ class MainWindow(gtk.Window):
             self.actiongroup.get_action('double_page').activate()
         if prefs['default fullscreen'] or fullscreen:
             self.actiongroup.get_action('fullscreen').activate()
+        if prefs['smart crop']:
+            prefs['smart crop'] = False
+            self.actiongroup.get_action('crop').activate()
         if prefs['default manga mode']:
             self.actiongroup.get_action('manga_mode').activate()
         if prefs['default zoom mode'] == preferences.ZOOM_MODE_BEST:
@@ -296,6 +299,9 @@ class MainWindow(gtk.Window):
             pixbuf = self.file_handler.get_pixbufs(single=True)
             unscaled_x = pixbuf.get_width()
             unscaled_y = pixbuf.get_height()
+            
+            if prefs['smart crop']:
+                pixbuf = image.auto_crop(pixbuf,prefs['smart crop threshold'])
 
             rotation = prefs['rotation']
             if prefs['auto rotate from exif']:
@@ -474,6 +480,10 @@ class MainWindow(gtk.Window):
 
     def change_keep_transformation(self, *args):
         prefs['keep transformation'] = not prefs['keep transformation']
+    
+    def change_smart_crop(self,*args):
+        prefs['smart crop'] = not prefs['smart crop']
+        self.draw_image()
 
     def manual_zoom_in(self, *args):
         new_zoom = self._manual_zoom * 1.15
diff --git a/preferences.py b/preferences.py
index feda595..44eee6f 100644
--- a/preferences.py
+++ b/preferences.py
@@ -40,6 +40,8 @@ prefs = {
     'smart space scroll': True,
     'flip with wheel': False,
     'smart bg': False,
+    'smart crop': True,
+    'smart crop threshold': 192,
     'store recent file info': True,
     'hide all': False,
     'hide all in fullscreen': True,
@@ -125,6 +127,14 @@ class _PreferencesDialog(gtk.Dialog):
             'show page numbers on thumbnails')
         page.add_row(thumb_number_button)
         
+        page.new_section(_('Smart Crop'))
+        label = gtk.Label('%s:' % _('Smart Crop colour threshold'))
+        adjustment = gtk.Adjustment(prefs['smart crop threshold'], 1, 256, 1, 10)
+        threshold_spinner = gtk.SpinButton(adjustment)
+        threshold_spinner.connect('value_changed', self._spinner_cb,
+            'smart crop threshold')
+        page.add_row(label, threshold_spinner)
+        
         page.new_section(_('Magnifying Glass'))
         label = gtk.Label('%s:' % _('Magnifying glass size (in pixels)'))
         adjustment = gtk.Adjustment(prefs['lens size'], 50, 400, 1, 10)
@@ -260,6 +270,7 @@ class _PreferencesDialog(gtk.Dialog):
         double_page_button.connect('toggled', self._check_button_cb,
             'default double page')
         page.add_row(double_page_button)
+        
         fullscreen_button = gtk.CheckButton(_('Use fullscreen by default.'))
         fullscreen_button.set_active(prefs['default fullscreen'])
         fullscreen_button.connect('toggled', self._check_button_cb,
@@ -350,6 +361,9 @@ class _PreferencesDialog(gtk.Dialog):
         value = spinbutton.get_value()
         if preference == 'lens size':
             prefs[preference] = int(value)
+        elif preference == "smart crop threshold":
+            prefs[preference] = int(value)
+            self._window.draw_image()
         elif preference == 'lens magnification':
             prefs[preference] = value
         elif preference == 'slideshow delay':
diff --git a/ui.py b/ui.py
index d4735d7..8a498ad 100644
--- a/ui.py
+++ b/ui.py
@@ -95,7 +95,9 @@ class MainUI(gtk.UIManager):
             ('slideshow', gtk.STOCK_MEDIA_PLAY, _('Run _slideshow'),
                 '<Control>S', None, window.slideshow.toggle),
             ('lens', 'comix-lens', _('Magnifying _glass'),
-                'g', None, window.glass.toggle)])
+                'g', None, window.glass.toggle),
+            ('crop', None, _('_Smart crop'),
+                's', None, window.change_smart_crop)])
         
         # Note: Don't change the default value for the radio buttons unless
         # also fixing the code for setting the correct one on start-up.
@@ -187,6 +189,8 @@ class MainUI(gtk.UIManager):
                     <separator />
                     <menuitem action="lens" />
                     <separator />
+                    <menuitem action="crop" />
+                    <separator />
                     <menu action="menu_transform">
                         <menuitem action="rotate_90" />
                         <menuitem action="rotate_270" />
@@ -298,6 +302,7 @@ class MainUI(gtk.UIManager):
             _('Double page mode'))
         self.get_widget('/Tool/manga_mode').set_tooltip_text(_('Manga mode'))
         self.get_widget('/Tool/lens').set_tooltip_text(_('Magnifying glass'))
+        #self.get_widget('/Tool/crop').set_tooltip_text(_('Toggle Smart Crop'))
 
     def set_sensitivities(self):
         """Sets the main UI's widget's sensitivities appropriately."""
 

_wb_

Microbe
Staff member
Joined
Apr 5, 2012
Messages
5,390
Age
41
Location
Brussels, Belgium
Should I implement something like this in PIV too?

If I get it right, you first compute the most common edge color, and then you scan the edges until you find a pixel that deviates more than a threshold from that. Should be simple enough to re-implement that in PIV (i.e. in C).

Little speed hint: avoid all those square roots, just compare the squared distance with the squared threshold.
 

Creature XL

Active Member
Joined
Jun 12, 2004
Messages
563
Age
45
Location
Hannover.de
Website
Visit site
wouldn't it be better to drop the sqrt in the distance calculation?


It îs only used to compare and threshold*threshold is faster (assuming integer) I suppose.
 

_wb_

Microbe
Staff member
Joined
Apr 5, 2012
Messages
5,390
Age
41
Location
Brussels, Belgium
wouldn't it be better to drop the sqrt in the distance calculation?


It îs only used to compare and threshold*threshold is faster (assuming integer) I suppose.
That's what I just said (and you only need to square the threshold once, so it's certainly faster):

Little speed hint: avoid all those square roots, just compare the squared distance with the squared threshold.
 
Top