モブ沢工房

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

Gdk(pygi)でマウスカーソルを任意位置に動かすには

感動のあまりカキコ!

改造版mypaintのsizechangemode.pyから一部抜粋〜

    def drag_start_cb(self, tdw, event):
        self._ensure_overlay_for_tdw(tdw)
        self._queue_draw_brush()
        # Storing original cursor position and some needed objects.
        dev = event.get_device()
        pos = dev.get_position()
        self._dev = dev
        self._scr = event.get_screen()
        self._ox = pos.x
        self._oy = pos.y
        super(SizechangeMode, self).drag_start_cb(tdw, event)

    def drag_update_cb(self, tdw, event, dx, dy):
        self._ensure_overlay_for_tdw(tdw)
            
        self._queue_draw_brush()
        adj = self.app.brush_adjustment['radius_logarithmic']
        cur_value = adj.get_value() + (dx / 120.0)
        adj.set_value(cur_value)
        self._queue_draw_brush()
        super(SizechangeMode, self).drag_update_cb(tdw, event, dx, dy)

    def drag_stop_cb(self, tdw):
        self._ensure_overlay_for_tdw(tdw)
        self._queue_draw_brush()
        self.start_drag = False
         
        self.base_x = None
        self.base_y = None

        # Reset cursor positon at the start position of dragging.
        Gdk.Device.warp(self._dev, self._scr, self._ox, self._oy)
        super(SizechangeMode, self).drag_stop_cb(tdw)

概要としては

  • event構造体のget_device(), get_screen()が必要。
  • mypaintのdrag_stop_cbにはeventがないので保存しておく
  • 位置はスクリーン座標なのだけど、deviceオブジェクトのget_position()でなんか謎のオブジェクトが得られてそいつの属性x,yがスクリーン座標なので、それを保存しておく。
  • ドラッグ終了時にGdk.Device.warpですっ飛ばす

これで、いままでシフトキー押下で発動するように設定していたオンキャンバスなブラシサイズ変更モード。

これが、サイズ変更でカーソル動いちゃって悲しい感じだったんですが (kritaを真似したのだけど、あっちはちゃんとカーソルが固定される)

ようやく思い通りになりました!

追記:

…と、マウスで実験してみた時は良かったのですが… ペンタブでやってみると、やはりペンタブを動かしてサイズを変更するとカーソルが移動してしまう。 ナンデ? と思ってよくよく考えてみると…

> ペンタブは絶対位置デバイスだから離した瞬間に別の絶対位置に移動している <

という、実にマヌケな結果に… じゃあ、Kritaはどうやってそれを防いでいるのか?

と思ってやってみたら(当たり前ですが)Kritaでも同じ挙動でした(>_<) 記憶が間違っていただけという悲しい結果に。

SWIG関数にctypesからアクセス

ついでにこれも思いついたシリーズ。

すなわち例えば、SWIG側の内部にstaticで構造体ポインタのリストを持っておき。 idx = register_hogehoge(void *ptr)でそのリストに登録。 その後はidxを使って(OpenGLのテクスチャの名前みたいな感じで)、例えば draw_bg(idx, 0, 0) みたいなことをすると。まぁ、これだとオブジェクトが削除されたのを知るのは難しそうなので、そのへん十分に気を使って、と。 どうにかすればctypesのオブジェクトにincref的なことができるのかもですが、まだよく分かっておりません。 あ、ていうかよく考えると表面はPyObjectだろうから普通に行けちゃったりするのかな…

ともかく、そういういうことを思いついたのでやってみました。 特に普通の共有ライブラリと違う点もなく、簡単にできました…ポイントは * ctypesの名前関係が難しくなるのでextern "C" でその場を凌ぐ * void*だと、構造体を引数に入れただけだと自動変換してくれないっぽいので、pointer(obj)を使う * つかcast(pointer(obj), c_void_p)が正しい? まぁLinuxだから簡単だったのかも知れないですけどね。

その他、ctypesの関数にSWIG内から…というのも妄想していましたが、よくよく考えると上記の「SWIGをctypesで呼ぶ」作戦のほうが柔軟性も自由度も高そうなので、とりあえずこれでいいかなと。

最初に思いついたのはctypesでポインタをlongにする関数を作って呼び出し、SWIGでそのlong値を受けてそれを強引に構造体ポインタにキャストする…というものでした。実際に動いたのですが、実になんかその…ダサいというか無理矢理感が強すぎたので上記の方法を考えました。

もしかするとswigに直接ctypesの構造体のアドレスをvoid*ポインタとして送り込めるのかもですが、そのへん全然わからないのでした。

PythonでSWIGのC++関数中でGdkPixbufにアクセスしてみた

Pythonで作ったGdkPixbufを、SWIGC++関数に送り込んでC++の中で最速描画ということを考えつきました。

さらに、そのpixbufはcairoから作ると、なお痛快でしょう。 と前々から思っていたのですが、遂に重い腰を上げて作ってみました。

そんなわけでそのサンプルをここに置いておきますね…(´・ω・`) 引数チェックは気分的にしたい部分以外はしていないですがテストなので…

testlib.i

SWIGのインターフェースファイルです。

%module testlib
// Inserting block
%{
#include "testlib.hpp"
%}
// Combining block
%include "testlibcore.hpp"
%init %{
%}

testlib.hpp

結合用のヘッダです。testlib.ccに埋め込まれるはず。

#include <Python.h>

// Write my own libraries
#include "testlibcore.hpp"

testlibcore.hpp

宣言だけなので書く必要無いですが一応

#ifndef __TESTLIBCORE_H__
#define __TESTLIBCORE_H__

#include <Python.h>

PyObject* testfunc(PyObject* pixbuf);

#endif 

testlibcore.cpp

#include "testlibcore.hpp"
#include <pygobject.h>
#include <stdio.h>
#include <gtk/gtk.h> //これでOK

PyObject* testfunc(PyObject* pixbuf) {

    GObject *obj = pygobject_get(pixbuf);
    if(obj != NULL) {
        printf("--- success to get gobject ---\n");
        GdkPixbuf* gpix = GDK_PIXBUF(obj);
        if(gpix != NULL) {
            printf(
                "the pixbuf is w:%d, h:%d, channnels:%d\n" ,
                gdk_pixbuf_get_width(gpix),
                gdk_pixbuf_get_height(gpix),
                gdk_pixbuf_get_n_channels(gpix)
            );
            // writing blue box
            int rowstride = gdk_pixbuf_get_rowstride (gpix);
            guchar *pixels = gdk_pixbuf_get_pixels (gpix);
            int n_channels = gdk_pixbuf_get_n_channels (gpix);

            int sx = 30;
            int sy = 30;
            int w = 48;
            int h = 20;

            for (int y=sy; y < sy+h; y++) {
                for (int x=sx; x < sx+w; x++) {
                    guchar *p = pixels + y * rowstride + x * n_channels;
                    p[0] = 0; //r
                    p[1] = 0;    //g
                    p[2] = 255;     //b
                    p[3] = 255;        //a
                }
            }
        }
        else {
            printf("--- too bad, failed gdkpixbuf ---\n");
        }
    }

    Py_RETURN_NONE;
}

ビルド用のSConstruct

いちおう、scons用のファイルも置いときます。いやべつにmakeでもいいんですが難しいので…

Target='_testlib.so' # 先頭に_が必要(超重要!)
srcs = "testlibcore.cpp testlib.i" # testlib.c is automatically generated.

e = Environment(SWIGFLAGS=['-c++', '-python'])
e.Append(CCFLAGS=['-Wall', '-g', '-O0', '-fPIC', '-fdiagnostics-color'])

e.ParseConfig('pkg-config pygobject-2.0 glib-2.0 gtk+-3.0 --cflags --libs')
e.ParseConfig('python-config --includes --ldflags')

e['SHLIBPREFIX'] = '' # To avoid add 'lib_' prefix to target library file.
e.SharedLibrary(Target, Split(srcs))

テスト実行用のcairo_pixbuf.py

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

import cairo
from gi.repository import Gdk, GdkPixbuf
import testlib

def main():
    w = 320
    h = 240
    surf = cairo.ImageSurface(cairo.FORMAT_A8, w, h)
    cr = cairo.Context(surf)
    cr.set_source_rgba(0, 0, 0, 1.0)
    cr.rectangle(8, 8, 100, 200)
    cr.fill()
    pixbuf = Gdk.pixbuf_get_from_surface(surf, 0, 0, w, h)
    print("---from python")
    print("sample bits:%d" % pixbuf.get_bits_per_sample())
    print("channels:%d" % pixbuf.get_n_channels())
    print("---calling c")
    testlib.testfunc(pixbuf)

    pixbuf.savev("/tmp/test_blued.png", "png", [], []) 

if __name__ == '__main__':
    main()

これを実行すると/tmp/test_blued.pngとして、 python側でcairoで四角を書かれたpixbufに青い四角が書かれたものが保存されるという。 まったくどうでもよい単なるテスト機能が発動するのでした。 なお、どうも上記のようにFORMAT_A8でグレースケールのcairoサーフェスに描画しても、その時はグレースケールで処理されますが、その後Gdk.pixbuf_get_from_surfaceでpixbuf変換すると32bitのRGBAになってしまうようです。まぁ構いませんが。

しかし、これは実に応用できそう… PygameサーフェスにCからアクセスしたりとか、前々から妄想していたのです。 これをやれば超高速BGタイル描画とか出来そうです(pythonは配列アクセスが遅いので…でも、どうせプロトタイプにしか使わないし、あんま意味ないかw) んでいろいろ調べましたが、コレも結構簡単に出来そうです。_pygame.hによるとPygameサーフェスはPySurfaceObjectで、PySurface_AsSurface(x)マクロがあるようです。

さらにPygameの実質後継と言われているらしいPySDL2も出来たら良いね、と思って調べましたが… PySDL2は、ctypes使ってるんですね。 PyOpenGL3もそうですが… つまりPySDL2とかでは同様にctypesで自作C関数にアクセスすれば、そのままぶっこめるという、実にしまらない落ちに(^^;

Xubuntu 16.04でCintiq 13HDを自動設定するの巻

ついつい、勢いでwacomの液タブCintiq 13HDを買ってしまいました(^^; 旧モデルなのは、安いだけでなくLinuxで使うには何も考えなくて良くて都合が良いというメリットもあるのです。 f:id:dothiko:20171027203651j:plain どーでもよいのですがこの絵、tegaki_dtで頑張って書いてたらボタンの誤爆で「画像が動く」状態になっちゃってどうにもならず、後で描き直してツイートしたのだけどやっぱ液タブなのでそのへんも速かったですな…

いやそれにしても中華液タブが割高に思えてしまう値段ですなコレは… これで一応最高レベル(あくまで廉価帯内での比較)の液タブを経験しておけば、後々中華品に手を出した時に 「ワコムだったらこの不快な現象は起きないのだろうか」という疑問などを一掃してくれるわけでまぁ買ってよかったかなと!

表題については、正直、現在のUbuntuLinuxであれば挿しただけでドライバが入って認識されるので何のネタにもならないのですが、デュアルモニタ(xinerama)だと横に超長いor縦に2つのモニタになっているので、全画面にマップされてしまいます。

これを以前、板タブについても避けようと思っていろいろやった挙句、面倒なので起動時に設定して挿しっぱということになりましたが、液タブはなんか光って電気もったいなさげなので使わない時は出来る限り消したいのですな。

というわけで再挑戦なのでありました。

まずは

  • udevにrulesファイルを書いて、RUN+=で一段目のシェルスクリプト実行。
  • 二段目のシェルスクリプトを実行、二段目でsleep 3ぐらいして、実際にX11にペンタブデバイスが認識されるのを待つ
  • 二段目スクリプトではsleepの後、export XAUTHORITY=/home/ユーザー/.Xauthority
  • あと、export DISPLAY=:0
  • んで、xsetwacom。

rulesファイルは/etc/udev/rules.d/99-cintiq.rulesとでもして

SUBSYSTEM!="input",ACTION!="add",GOTO="my_cintiq_end"
ENV{ID_MODEL}=="Wacom_Cintiq_13HD",ENV{ID_TYPE}=="hid",RUN+="/usr/local/lib/tablet/cintiq_setup_launcher.sh"
LABEL="my_cintiq_end"

という方法を取りました。まぁ試行錯誤の結果これで動くことは動く。…のですが、何度も呼び出されている模様… 実に気持ち悪いのでフラッグファイルを立てるなど試行錯誤してみたのですが、うまく行かない。何故か逆に設定がされなくなったりして…

そこで、考え方を変えて、udevとX11の組み合わせは筋が悪いのでとりあえずやめるとして 以前見失った「XFCE4の設定でペンタブの自動適用設定があったはず、あれをもう一度探そう」となりました。

以前はたしか「マウスとタッチパッド」にあったのですが、今は無い… んでようやく見つけましたよ、「リムーバブルとメディア」の中にその項目があった。 そんなん分かるか? f:id:dothiko:20171027203723j:plain これに、こんなふうに

/usr/local/bin/xfce4_detect_pentab.py --node %d

というように%dをつけると「/dev/input/event22」とか「/dev/input/mouse5」とかのデバイスノードを引数で送りつけてその都度呼んでくれます。だがしかし「その都度」なので、ペンタブ一つさすと3回ぐらいは呼ばれるというわけです。

それでもってデバイスノードが得られても、何のペンタブが挿されたのかは即座にはわかりません…

まぁ結局長くなるのでここには書きませんが、

  • cat /proc/bus/input/devices すると、その中に H: Handlers=mouse5 event22 みたいな情報があり、また、N: Name="Wacom Intuos 6x8 Pen"みたいなxsetwacomデバイス名も得られる。
  • pythonでこれを解析してデバイスノードからデバイス名を得るようなスクリプトを書き
  • ペンタブに対する設定はjsonファイル(config.json)にまとめて記述。
  • さらにsubprocessでxsetwacomを呼び出して、先ほどのjsonに書かれた設定を反映。
  • 一気にすべての設定を適用してしまう。xfce4のdaemonはおそらくdbusで捕捉してると思うので、udevとは違い呼ばれる時には既にxinputは全部揃ってるはず…
  • テンポラリファイルに情報を書いといて、同一デバイスに対するものは60秒以下なら設定せずに抜ける。これで重複する他の呼び出しを全部キャンセル

ということにしてスクリプトを書いたのだけど、ここに書くには余白が少なすぎる。 かなり自分専用でピーキーでチャチなものなので、githubに公開してもなぁ…

SWIG & python でundefined_symbolが出る時

何か超久々ですがとりあえず… とりあえず以下、例として、

  • Flagtileというクラスがあるとします
  • python用のtestlib2モジュールに組み込まれます。
  • SWIG, g++ともコンパイルは問題なく通る
  • C++内では普通に使えている
  • しかし、SWIGPythonモジュールに組み込むと、そのモジュールのimportが通らない。
In [1]: import testlib2
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
<ipython-input-1-e3832f46d55b> in <module>()
----> 1 import testlib2

/home/dothiko/python/test/mypainttest/fill_prog/testlib2.py in <module>()
     26                 fp.close()
     27             return _mod
---> 28     _testlib2 = swig_import_helper()
     29     del swig_import_helper
     30 else:

/home/dothiko/python/test/mypainttest/fill_prog/testlib2.py in swig_import_helper()
     22         if fp is not None:
     23             try:
---> 24                 _mod = imp.load_module('_testlib2', fp, pathname, description)
     25             finally:
     26                 fp.close()

ImportError: ./_testlib2.so: undefined symbol: _ZN9Flagtile25DIRTYE

こんなふうになる時ですね。

これは、私が経験した中では

  1. cppソースとhppで関数の引数が互換性があるが異なった型である (コンパイルエラーにならないので、見過ごしている)
  2. 「hppで宣言されているがcppに実体が存在しない」使用していない関数がある(警告を見過ごしている)
  3. SWIGが対応していない型を使っている

今回の場合は、SWIGが対応していない型、つまりFlagtile内にこのような宣言がある場合だったりしました。

    static const int32_t hogeflag = 0x00000001;

int32_tとかのなんて言うんでしょうか、正確にビットサイズを設定する型。これSWIGさんは嫌いらしいです。

ケツの座りは悪いですがintにすると通ります。*1 でも、intとかlongとか64ビットで何ビットだっけ?って考えるの嫌なんだよなぁ…

2018.9.10 追記

stackoverflow.com

SWIGのインターフェースファイル(.iファイル) で %import "stdint.i" とすれば良いようです!

ではでは!

*1:解決方法わかりました。追記見てください

PyGObject(PyGi)で「デフォルトのフォントサイズ」を取得する

PyGObject…でいいのでしょうか。PyGiなのか。未だによくわかりません。 とにかく新しい方のGtkpythonバインディングで、コンボボックスのカスタムレンダラーで使うデフォルトのフォントサイズを取得したくなりました。

これはMyPaintにOpenCVのgrabCut関数を使った新しいペイント方法(グラフカットを利用し穴の開きまくった線画を分離してペイントする)を実装した時、実験中は「真上のレイヤー」を線画レイヤーにしていたのですが、これは実際使いづらい。

そのため、コンボボックスで線画レイヤーを選べるようにしよう!とピコーンと思いついたんですが、カスタムレンダラーでフォントサイズを他のウィジェットと揃える方法が全く分からんのですね。

do_renderコールバックにおけるcairo contextでのtext_extents系関数でサイズを得ると、明らかに小さい文字が描画される…

しかしながら検索方法が悪いのか一向にそのものズバリにヒットしない…

そこでまぁ頑張っていろいろ調べ&試した結果がコレ

ただし、これが正しいのかどうかは不明、という…

from gi.repository import Gtk, Gdk, Pango

st = Gtk.ComboBox.get_default_style()
desc = st.font_desc
size_base = desc.get_size() / Pango.SCALE
if desc.get_size_is_absolute():
    # True == size is in device unit
    pass
else:
    # False == size is in Point.
    # pixel = Point * (DPI / 72.0)
    scr = Gdk.Screen.get_default()
    dpi = scr.get_resolution()
    if dpi < 0:
        dpi = 96.0
    size_base = math.floor(size_base * (dpi / 72.0))
  • スタイル構造体の中にfont_descがあるので、コンボボックスのデフォルトスタイルを得る。
  • font_descのget_size()でサイズを得られるが、これはPango.SCALEでスケーリングされた値であるため、割る。
  • それで得られた値はデバイス単位かポイント単位かに別れる。これはget_size_is_absolute()で区別できる。
  • ポイント単位であった場合、DPI設定 / 72.0 で得られた値を先ほどの値に掛けることでピクセル単位に変換できる。

これで変数size_baseにデフォルトのコンボボックスのフォントサイズ(ピクセル単位)が入る…はず

SWIGとnumpyについて(ImportErrorとセグメンテーションフォールト)

何か「ハリーポッターと魔法のナントカ」みたいなタイトル…

それはともかくSWIGとnumpyでハマってしまったので備忘録的にメモ。

現象としては

  • hppでNO_IMPORT_ARRAYを定義して#include <numpy/arrayobject.h>すると、.pyファイルでSWIGモジュールをインポートした時点でimporterror (undefined symbol: PyArray_API)が起きる。
  • NO_IMPORT_ARRAYを定義しないと、 cppでPyArray_ZEROS()を呼んだところでセグフォが起きる。

苦しんだのですが調べていてようやくわかりました。 基本的に

PY_ARRAY_UNIQUE_SYMBOL

が必要。

  • .iファイルのinit節でimport_array();呼び出し
%init %{
import_array();
%}
  • PY_ARRAY_UNIQUE_SYMBOLをdefineする共通のヘッダファイルを作成
#ifndef COMMON_HPP
#define COMMON_HPP

#define PY_ARRAY_UNIQUE_SYMBOL mytest_array_API

#endif

(↑基本的にmypaintのcommon.hppのパクリ)

  • このcommon.hppを、すべてのヘッダファイルで#include
  • ラッパー用ヘッダでのみNO_IMPORT_ARRAYを定義せず、その他の自作ヘッダではすべて#include <numpy/arrayobject.h>前にNO_IMPORT_ARRAYをdefineする。
  • NO_IMPORT_ARRAYを定義せずに#include “common.hpp"してあると、その都度シンボルが作られるらしくビルドエラーになる

これで、ImportErrorもビルドエラーもセグフォも起きなくなりました。

ところで統合開発環境のPyCharmを試してみたのですがこれは凄いですね。MyPaintも楽々走るしブレークポイントも普通に置けて(当然か…)これヤバイよ!と思いましたが、唯一悲しいのが起動速度の遅さ…まぁ、いいか。まだ、あんまり使ってないのですが。 ああ、あとちょっともっさり気味かもですが、これはまぁ機能の高さとおそらくpythonで書かれてる点からして仕方ないかも。