モブ沢工房

プログラミングとかLinux関連(特にOSSのグラフィックツール関連)とかレトロゲームとか3Dプリンタやら日曜大工等、色々。

mypaint 1.2にkrita風stabilizer(の簡易版)をつけてみるテスツ

(追記あり)

mypaintにどうしてもKritaのstablizerを付けたかったのですが、とりあえずKritaのソースも読まず適当なものをつけてみたら思いの外、中々よいスタビライズ感があったのでここにメモしておきます。

なのであくまでkritaなのですが、使ってみた感触は中々kritaっぽい。

UIの作り方が良くわからないのとテストなので、使われていないらしい「d」キーを押している間、スタビライザーが発動するというふうにしました。 github版では消しゴムボタンとフリーハンドボタンの間に新設したスタビライザートグルボタンを押すことで発動します。キーボード割り当ても可能。なので、押している間ではなくトグルになりました。まぁそのほうが使いやすいかも。

単に現在もmypaintにある「手ブレ補正」を強めたのとどう違うのか?実はオイラも分かってません(^^;

当初はキーを押しているあいだ「手ブレ補正」を最強にするコードだったのですが、今ひとつ効果が弱い気がしたのでコードを追って見ると、どうも殆どはbrushlibで行われていて敷居が高い。

そこで適当に、単に最近の32サンプルを平均化するというだけのコードを書いてみたら、割といい感じ?になってしまいました。ただ、仕方ない話ですがカーソルから離れた位置に線が描かれるのが難点かな〜と。真面目にやるならKrita読まないと…orz

これがあるとどう言う局面で役立つのかというと

  • どうでも良い線→そのままサクッと
  • ちょっと難しい線→stabilizerでサクッと
  • かなり難しい線(長いカーブなど)→inktoolでじっくりと

このようにサクサク書けるのではないか?と。

stabilizerとinktoolを合わせれば炎になる!炎となったmypaintは…無敵だ!!

ような気がするのですが、まだそれほど使っていません(^^;

コード

gui/freehand.pyをいじります。 めんどうくさいので、この時点のmasterとワタクシが使っておりますテスト用ブランチの差分で…

2016/01/30 18:24:08 追記 少し修正しました。eventhackと言われる部分(多分、キューに溜まったデータ?)も対応するように。

2016/02/03 23:46:24 追記 やけくそでgithubのリポジトリで公開することにしました。自分の頭の悪さと英語力のなさには愕然ですがinktoolを使いやすくしたい一心なのです…ってか誰か良いコードを書いてくれ比較的マジに (本当はそんな事が起きないかな〜と淡い期待を抱いていたのですが、どうも意味の分からん英語を書く変なやつと思われている模様っていうか正確な認識です、ハイ(^^; ))

diff --git a/master:gui/freehand.py b/my_build:gui/freehand.py
index 7b56268..2dad4a7 100644
--- a/master:gui/freehand.py
+++ b/my_build:gui/freehand.py
@@ -22,6 +22,7 @@ from gettext import gettext as _
 import gobject
 import gtk
 from gtk import gdk
+from gtk import keysyms
 from libmypaint import brushsettings
 
 import gui.mode
@@ -102,6 +103,10 @@ class FreehandMode (gui.mode.BrushworkModeMixin,
     # gives us, but position them at the x and y that Xi2 gave us.
     # Pressure and tilt fidelities matter less than positional accuracy.
 
+    # Stablizer ring buffer
+    _stabilize_max = 32
+    _stabilize_src_pos = [None] * _stabilize_max 
+
     ## Initialization
 
     def __init__(self, ignore_modifiers=True, **args):
@@ -110,6 +115,11 @@ class FreehandMode (gui.mode.BrushworkModeMixin,
         self._cursor_hidden_tdws = set()
         self._cursor_hidden = None
 
+        # Stablizer init
+        self._stabilize_init()
+        self._stabilized=False
+
+
     ## Metadata
 
     pointer_behavior = gui.mode.Behavior.PAINT_FREEHAND
@@ -394,6 +404,8 @@ class FreehandMode (gui.mode.BrushworkModeMixin,
             # Hide the cursor if configured to
             self._hide_drawing_cursor(tdw)
 
+
+
             result = True
         return (super(FreehandMode, self).button_press_cb(tdw, event)
                 or result)
@@ -418,6 +430,11 @@ class FreehandMode (gui.mode.BrushworkModeMixin,
             self._reinstate_drawing_cursor(tdw)
 
             result = True
+
+        # Stablize
+        self._stabilize_reset()
+
+        
         return (super(FreehandMode, self).button_release_cb(tdw, event)
                 or result)
 
@@ -467,9 +484,19 @@ class FreehandMode (gui.mode.BrushworkModeMixin,
             if not same_device:
                 tdw.doc.brush.reset()
 
-        # Extract the raw readings for this event
-        x = event.x
-        y = event.y
+        # Stabilizer cursor position fetch
+        self._set_stabilize_point(event.x, event.y)
+        if self._stabilized:
+            pos = self._get_stabilize_point()
+            if not pos:
+                return
+            else:
+                x, y = pos
+        else:
+            # Extract the raw readings for this event
+            x = event.x
+            y = event.y
+
         time = event.time
         pressure = event.get_axis(gdk.AXIS_PRESSURE)
         xtilt = event.get_axis(gdk.AXIS_XTILT)
@@ -573,18 +600,35 @@ class FreehandMode (gui.mode.BrushworkModeMixin,
             # Remove the last item: it should be the one corresponding
             # to the current motion-notify-event.
             hx0, hy0, ht0 = drawstate.evhack_positions.pop(-1)
-            # Check that we can use the eventhack data uncorrected
-            if (hx0, hy0, ht0) == (x, y, time):
-                for hx, hy, ht in drawstate.evhack_positions:
-                    hx, hy = tdw.display_to_model(hx, hy)
-                    event_data = (ht, hx, hy, None, None, None)
-                    drawstate.queue_motion(event_data)
+            if self._stabilized:
+                if (hx0, hy0, ht0) == (event.x, event.y, time):
+                    for hx, hy, ht in drawstate.evhack_positions:
+                        self._set_stabilize_point(hx,hy)
+                        pos = self._get_stabilize_point()
+                        if pos:
+                            hx, hy = tdw.display_to_model(pos[0], pos[1])
+                            event_data = (ht, hx, hy, None, None, None)
+                            drawstate.queue_motion(event_data)
+                else:
+                    # FIXME code duplication
+                    logger.warning(
+                        "Final evhack event (%0.2f, %0.2f, %d) doesn't match its "
+                        "corresponding motion-notify-event (%0.2f, %0.2f, %d). "
+                        "This can be ignored if it's just a one-off occurrence.",
+                        hx0, hy0, ht0, event.x, event.y, time)
             else:
-                logger.warning(
-                    "Final evhack event (%0.2f, %0.2f, %d) doesn't match its "
-                    "corresponding motion-notify-event (%0.2f, %0.2f, %d). "
-                    "This can be ignored if it's just a one-off occurrence.",
-                    hx0, hy0, ht0, x, y, time)
+                # Check that we can use the eventhack data uncorrected
+                if (hx0, hy0, ht0) == (x, y, time):
+                    for hx, hy, ht in drawstate.evhack_positions:
+                        hx, hy = tdw.display_to_model(hx, hy)
+                        event_data = (ht, hx, hy, None, None, None)
+                        drawstate.queue_motion(event_data)
+                else:
+                    logger.warning(
+                        "Final evhack event (%0.2f, %0.2f, %d) doesn't match its "
+                        "corresponding motion-notify-event (%0.2f, %0.2f, %d). "
+                        "This can be ignored if it's just a one-off occurrence.",
+                        hx0, hy0, ht0, x, y, time)
         # Reset the eventhack queue
         if len(drawstate.evhack_positions) > 0:
             drawstate.evhack_positions = []
@@ -599,6 +643,51 @@ class FreehandMode (gui.mode.BrushworkModeMixin,
                                     priority=self.MOTION_QUEUE_PRIORITY)
             drawstate.motion_processing_cbid = cbid
 
+    def key_press_cb(self, win, tdw, event):
+        if event.keyval == keysyms.d and not self._stabilized:
+            self._stabilize_init()
+
+    def key_release_cb(self, win, tdw, event):
+        if self._stabilized:
+            self._stabilize_reset()
+
+    ## Stabilize related
+    def _stabilize_init(self):
+        self._stabilized_index=0
+        self._stabilized_cnt=0
+        self._stabilized=True
+
+    def _stabilize_reset(self):
+        self._stabilized_index=0
+        self._stabilized_cnt=0
+        self._stabilized=False
+
+    def _set_stabilize_point(self,x,y):
+        self._stabilize_src_pos[self._stabilized_index]=(x,y)
+        self._stabilized_index+=1
+        self._stabilized_index%=self._stabilize_max
+        self._stabilized_cnt+=1
+
+    def _get_stabilize_point(self):
+        if self._stabilized_cnt < self._stabilize_max:
+            return None
+
+        ox=0
+        oy=0
+        idx=0
+        while idx < self._stabilize_max:
+            cx,cy=self._get_stabilize_element(idx)
+            ox+=cx
+            oy+=cy
+            idx+=1
+
+        return (ox/self._stabilize_max,oy/self._stabilize_max)
+
+    def _get_stabilize_element(self,idx):
+        return self._stabilize_src_pos[(self._stabilized_index + idx) % self._stabilize_max]
+    
+        
+
     ## Motion queue processing
 
     def _motion_queue_idle_cb(self, tdw):