milk_spoonのブログ

さじの情報科学的活動、考えたこと、その他を雑記するためのブログです。

boostnote(Boost Note)がリニューアルしてほぼ新アプリ

なにかって言うと、かなりリニューアルしたし、実質新しく作り直したようなので、markdownエディタとしての今までの評価を見て、これからダウンロードする人は気をつけて。
旧版もダウンロードできるよ。

という話。

公式サイトから素直にダウンロードできるのはリニューアル版のみになってた

https://boostnote.io/
ここから目ぼしいダウンロードボタンを押しても、リニューアル版しかダウンロードできない。
最近旧版を使い慣れてきていいな…と思った自分は若干裏切られた感じがしなくはない。

旧版のリンクもある

f:id:milk_spoon:20200115180759p:plain
まあその、ページ下にちゃんと旧版ダウンロードリンクがあるので。

そもそもgitHubのリリースページにいけばありますね。そりゃそうだ。
github.com
(ただのダウンロードリンクならしれっと取り下げられちゃうかと思ったけどgitHub上ならまず消えないでしょう)

boostnoteの評価が指してるのは今のところこの旧版でしょう。
もちろん時間が経てばリニューアル版の評価が出るでしょうけど。
ちなみに旧版はboostnoteって読んでたのをリニューアルを期にBoost Noteとしたようなので、これで新版旧版呼び分ける感じがする。

リニューアル版でよくなったところ

と言っても、リニューアル版使ったわけじゃないんですが
リニューアル版でクラウドスマホ対応したようなので、これは結構機能として大きいんじゃないかなと。
むしろ自分のようにローカルにメモ帳的に使えればいいや、的な方が少数派かな?

まとめ

リニューアル版はあんまりこなれてない感じがするので、アーリーアダプターさんたちによってもうちょっと磨かれるまで旧版使おうかなというただのものぐさ!
でも旧版を今の所かなり気に入ってるので、リニューアル版がいい感じになったらぜひつかいたい。なんだかんだクラウドなら安心だし。

おまけ

旧版でhtml出力する時、デフォルトだとダークテーマ的なcssはついてこない(うまく反映されてない?)けど
うまくいく方法を見つけたのでプルリクでも…と思ったらなんか新版が出ちゃったので
結局ワークアラウンドをここに雑書きする大変非社会還元的なことになりそう。

別PCからgit pushできない

結局

↓のようにremote originのurlにユーザー名とパスワードを入れた。

git remote set-url https://Hoge:Password@github.com/Hoge/repo.git

ら、push成功。
そのままは嫌だったので

git remote set-url origin https://Hoge@github.com/Hoge/repo.git

でパスワード部分は削除
でもこれでもpushうまくいったので、一回認証通すのが必要だった?
できたにはできたけどあまりスッキリ解決せず。

検索したら他の人が5億回くらい同じ問題に当たってたけど、「パスワードもurlに入れました^^」で終わってるところが多かったので
一応あとから削除してもOKよってのを書いときます。

追記

多分Windows資格情報マネージャーに別のアカウントの情報が残ってたせいかも?
コンパネから開いて、gitHub.comに対する資格情報を削除してあげれば、次回git pushであらためてユーザーIDとパスワードを聞かれる…はず。
詳しくは↓で。

詳細

メイン機で作成して色々やってgitHubにpushしたリポジトリを、サブ機でcloneしてから編集してcommitしてpushしようとした。

$ git push
remote: Permission to Hoge/repo.git denied to Fuga.
fatal: unable to access 'https://github.com/Hoge/repo.git/': The requested URL returned error: 403

実はHogeとFuga2つのgitHubアカウントを持ってる。
でもgit configの--globalも--localもついでに--systemも見たけどFugaの情報はない。
これでもかと--globalと--localでuser.nameをHogeに、user.emailをHogeのnoreplyメールアドレスに設定
あとremote originのurlにユーザー名も入れてhttps://Hoge@github.com/Hoge/repo.git
に。
それでもエラーメッセージ変わらず…
ユーザー名入れたんだから改めてパスワード聞いてくれればいいのに!
SSH使うなり、(同じ問題の回答としてよく出てくる)パスワードもremote originのurlに入れてしまえばいいのかもしれないけど
Fugaの情報をどこから持ってきてるのか謎…

でも結局冒頭のようにパスワードをurlに入れて解決。
パスワード入れておくのはイヤなのであとから削除。
削除したあともpushできたので一回認証通ればOKっぽい。

追記

ごめんこれ多分Windowsの資格情報マネージャーのせいでは。
f:id:milk_spoon:20191128063012p:plain

gitがcredential.helperとしてリモートリポジトリの認証情報を記憶管理するオプションが色々あるけど
Windowsの場合wincredという設定があって、これが資格情報マネージャーを使うらしい。
いや当然といえば当然感…
Git - 認証情報の保存

多分上の資格情報マネージャーの画面で、github.comに対する情報を消してあげれば
gitでpushする最初のときにユーザー名とパスワードを聞かれるはず。
で、以降は記憶されるのでパスワード入力しなくて済む。
でも結果として同じように解決するなら、やっぱり↑のように一回ユーザーIDとパスワード入れてpushしてからURL変えるほうがラクかも。

サブ機も前に色々開発で使ってたので、多分Fugaアカウントの情報が残っててそっちを見に行ってたっぽい。
なんでgitがFugaの情報を吐くのか(知ってるのか)怖かったけど出どころがわかって安心。

PySide2+Pillowでコード-1073740771 (0xC000041D)エラー

前の記事ではwxPythonが~PyQtが~って言ってたんですが結局PySide2やってます。
ほとんどPyQt5と違いは無いらしいけど、ライセンスがLGPLなのでちょっと安心かなと
(あまり皆気にしてないような気もする)

本題

で、タイトルの通り…
Pillowを使って画像をいじり、QPixmapでPySide2のGUI画面上に表示しようとしたら
コード-1073740771 (0xC000041D)でエラーになりました。
f:id:milk_spoon:20191123143653p:plain

結論から言うと、QPixmapを作るときにcopyするとOKでした。

こうしていたのを

#image: PIL.Image
qtimg = ImageQt(image)
pixmap = QPixmap.fromImage(qtimg)

こう。

#image: PIL.Image
qtimg = ImageQt(image)
pixmap = QPixmap.fromImage(qtimg).copy()  #★copy

ここの回答ほぼまんまで解決しましたが、エラー内容は違うかな…?
stackoverflow.com
下で書きますが、QGraphicsViewあたりもいじってたので原因特定がちょっと大変でした…

具体例(半分雑記)

PillowのImageGrabでスクリーンショットを撮って
PySide2のQGraphicsViewに表示させよう~としたらハマりました。
再現できるコードはコチラ。

#main_form_ui.py
#UI部分のみ
from PySide2 import QtCore, QtGui, QtWidgets

class Ui_MainForm(object):
    def setupUi(self, MainForm):
        MainForm.setObjectName("MainForm")
        MainForm.resize(400, 300)
        self.horizontalLayout = QtWidgets.QHBoxLayout(MainForm)
        self.horizontalLayout.setObjectName("horizontalLayout")
        #QGraphicsViewを配置
        self.viewMain = QtWidgets.QGraphicsView(MainForm)
        self.viewMain.setObjectName("viewMain")
        self.horizontalLayout.addWidget(self.viewMain)

        self.retranslateUi(MainForm)
        QtCore.QMetaObject.connectSlotsByName(MainForm)

    def retranslateUi(self, MainForm):
        MainForm.setWindowTitle(QtWidgets.QApplication.translate("MainForm", "Dialog", None, -1))
#main_form.py
#フォームの実装とmain
from PySide2.QtWidgets import QApplication, QMainWindow, QDialog, QGraphicsScene,QGraphicsPixmapItem
from PySide2.QtGui import QPixmap
from PIL import Image, ImageGrab
from PIL.ImageQt import ImageQt
from main_form_ui import Ui_MainForm
import sys

class MainForm(QDialog):
    def __init__(self,parent=None):
        super().__init__(parent)
        self.ui = Ui_MainForm()
        self.ui.setupUi(self)

        scene = QGraphicsScene(None)
        #スクリーンショットを取得、QGraphicsPixmapItemになるまで変換してsceneに追加
        image = ImageGrab.grab()
        imgqt = ImageQt(image)
        #★ココ!
        pixmap = QPixmap.fromImage(imgqt)#.copy()
        pixmap_item = QGraphicsPixmapItem(pixmap)
        scene.addItem(pixmap_item)

        self.ui.viewMain.setScene(scene)

if __name__ == "__main__":
    app = QApplication(sys.argv)

    window = MainForm()
    window.show()

    sys.exit(app.exec_())

ウィンドウにQGraphicsViewが貼ってあって、そこに起動時にImageGrabで撮ったスクショを貼るだけのコードです。

問題の箇所は★ココ!の部分で、ImageGrabで取得したスクショ画像をQPixmapにするところですね。
ちなみにImageGrabではなく、Image.openで画像ファイルを開いたものを渡したときはQPixmapをcopyしなくても動作していました。
StackOverflowの回答にあるように、特定状況でリソースが解放されてしまう感じなんでしょうか。

こんな感じで。
PySide2楽しいです。皆どんどんGUIpythonで書こう~

wxPythonで画像が切れたり余分に表示されてしまうときの対処

PythonGUI書きたいよね、と思ってwxPythonやってます。
QtはC++で書いたことあるし、PyQtのほうが良いんじゃ…と思いつつ。
正直書きやすく分かりやすくはないので人にオススメするかと思うとうーん
定数でレイアウト指定とかして、Pythonっぽくない…

でもまあ頑張ればそれなりに書けるので簡単なツールを手元で作ってます。
画像ビューア+αみたいな感じだけど、一つ詰まったことが…

下のようなサイズ違いの2つの画像を切り替えてGUIに表示すると
f:id:milk_spoon:20180108221240p:plain
f:id:milk_spoon:20180108221244p:plain

こんな風にめちゃくちゃな表示に(´;ω;`)
f:id:milk_spoon:20180108224707p:plain
f:id:milk_spoon:20180108224713p:plain

原因は、画像データを更新した後に画面のRefreshメソッドを呼んでいなかったことでした。
実際使うものを作ったときには画像が欠けてたりもして結構解決に時間をとりましたが、分かってみるとそりゃそうか、という感じです…
抜き出しでわかりづらいですがGUI上の画像変更のコードを以下のようにするとうまくいきます。

    #wx.Frameを継承したメイン画面クラスのメンバ
    def UpdateDisplay(self):
        #画像ファイルパスを指定して読み込み
        img = wx.Image(self.imagePaths[self.index])
        #StaticBitmapコントロールにセットする
        self.image.SetBitmap(img.ConvertToBitmap())
        #画面を更新する←これが抜けていた
        self.Refresh()

実際作ってるものはメインのwx.Frame継承クラスの子にさらにwx.Panelが何重かネストしてその中に画像があったりするんですが、一番上のself.Refreshだけでちゃんとなるので
あまりコントロール間の事は考えずself.Refreshが楽そうです
(wx.Window継承クラスならRefresh呼べるので一部Panelだけ更新ももちろんできるはず)

wxPythonで画像が欠けたり変な表示になってたらself.Refresh()!ということで
同じところで詰まる人がいませんよーにと祈ります。

OpenCV+Siv3Dで超簡単な画像補正ソフトを作った

spoonblog.hatenablog.com

これでOpenCVとSiv3D一緒に使えるよ!といったものの実際の使用例がなくては話にならないので
ちょうど自分で試したかったことも合わせて、デモ的ではあるけど2つほど作りました

ソース↓
https://github.com/saji-spoon/OpenCV-Siv3DSamplegithub.com

動くやつ↓
https://github.com/saji-spoon/OpenCV-Siv3DSample/releases/tag/1.00

以下、問題設定とかSiv3Dと合わせた表示結果とかについて説明するのでOpenCVの画像処理そのものの深いところの説明はあまりしません。
輪郭検出とか個別のことは気が向いたら書くかもしれないけど詳しくはソースで(

1. アンケート用紙の枠検出

(Form-Siv3D(August2016v2)プロジェクト)

こんな感じ↓のアンケート用紙からOpenCVで入力枠を検出したい
f:id:milk_spoon:20171208012038j:plain

個人的にはこの記事↓めっちゃ参考になった…
画像から四角いものを検出する、ってときにだいたい必要なものが解説されてる気がする
dev.classmethod.jp

画像から四角いものを検出するには結構色々手順がいるけども、とりあえず「輪郭検出」→「ゴミ取り」(一定以下の面積の輪郭を取り除く)というところを考えると、「一定以下」ってどれくらいの数値ならいいのかがよくわからない。

ということで、スライダーで値を調整しながら、指定された値より面積が大きい輪郭しか表示しないようにSiv3Dで表示させてみた。
f:id:milk_spoon:20171208012536p:plain
デフォルトだとこの状態で、アンケート用紙に書かれた文字まで輪郭検出してしまっている。

f:id:milk_spoon:20171208012608p:plain
スライダーを少し右に動かすと、文字から検出した輪郭は表示されなくなる。5000を超えたあたりで一番小さい入力欄も表示されなくなってしまうこともわかる。

ゴミ取っただけだとタイトルとかも入ってしまっているけど、ここから切り出したい画像だけクリックで指定…とかも割と簡単に発展できそう。

2. アンケート用紙のスキュー補正

(SkewCorrect-Siv3D(August2016v2)プロジェクト)

またアンケート用紙。
白紙のと、記入済みだけど曲がってしまっているのがある。
スキャナで取るとき下手くそだと曲がりますね。

f:id:milk_spoon:20171208013037j:plainf:id:milk_spoon:20171208013040j:plain

ただ、アンケート用紙には一番外側にふっとい枠線がある。これを検出して曲がっているのを補正できる…はず。
スキャンしたときとか曲がってしまったり歪んでしまったりして結果画像がズレてるのをスキューと言います。これを直すのでスキュー補正。
f:id:milk_spoon:20171208013449p:plain
これが白紙画像。これの補正枠を基準として、これに合わせて直す。

f:id:milk_spoon:20171208013458p:plain
曲がってしまってる記入済みの画像。でも補正枠は検出できてるので、これを白紙画像の補正枠と合わせるように変形すればいい。

f:id:milk_spoon:20171208013455p:plain
できました。

輪郭検出と直線近似で補正枠の四角形はとれた上で、
cv::getPerspectiveTransform
cv::warpPerspective
の2つの関数を使うと、一方を基準として、もう一方をそこにあわせる、みたいな画像の変形ができる(ふんわり)。
正確にはcv::getPerspectiveTransformで、4点から4点への変換行列を作り、cv::warpPerspectiveで作った行列を用いて実際に画像変形を行う。
まあでも、今回は単純に歪んでる方の補正枠を正しい補正枠に合わせるように変形しているだけといえばそれだけ。

Siv3D単体でも結構な加工とか画像処理(輪郭検出すら)できるので
OpenCVだけでできるのはなんじゃろなーと考えてたけど、自分の知ってる範囲ではcv::warpPerspectiveかな…と。
予想以上にきれいに補正できて(画像がもともときれいなのはあるけど)びっくり。
真っ直ぐにした画像を予め用意してるとかそういうわけではないぞ!

まとめ

やっぱり値を調整しつつリアルタイムで見れるのとか、座標クリックとかで操作しつつ次の処理を決めたりとかするにはSiv3Dの機能が強力。
簡単な操作で大量の画像の切り出しとか加工を自動化できると色々役に立つかも。
特に最近機械学習がブームで大真面目に大手がソースコードの画像を機械学習にぶっこんだりしているので、やっぱり画像処理できると強いと思います(雑)

OpenCV←→Siv3Dで画像のデータ変換をする

これはSiv3D Advent Calendar 2017 8日目の記事です。

最近OpenCVC++)で色々と画像処理をしてるけども、デフォルトのウィンドウとかキー入力受付が結構使うのが辛く、せっかくなので表示とか操作インターフェースを使い慣れたSiv3Dでやりたい。
Siv3Dの画像クラスであるs3d::Imageと、OpenCVの画像クラスであるcv::Matの変換ができれば、OpenCVで加工した画像をSiv3Dで表示する、ってことが普通にできそう。
やっぱり先駆者さんもいるもので、このコードを参考にちょいちょいしてみたらできた。

Siv3D+OpenCV環境

ちょろっと環境構築の話もしておく。
まず当たり前だけどVisual Studio 2015とSiv3D とOpenCV(3.0.0)のインストールは済ませておく。
作るプロジェクトはSiv3Dプロジェクト。
そのプロジェクトに、OpenCV3.0.0のincludeディレクトリを「追加のインクルードディレクトリ」として設定。終わり。
f:id:milk_spoon:20171207205940p:plain
追加のライブラリディレクトリなどの設定はいらず、Siv3D側で導入済みのOpenCVのライブラリを参照して動く。
簡単だけどcv::imreadやcv::imshowが通常通り動かない点など、通常の導入とちょっと違う点も出てくる。
例えば、cv::imreadはbmpしか読み込まないので、基本的に画像読み込みはs3d::Imageから行ったほうがいいと思う。
他、cv::imshow等ウィンドウ系関数が使えない。そもそもインターフェースをSiv3Dにするという話なので使わないのだけど…

コンパイルした時、未解決の外部シンボル "public: void __cdecl cv::Mat::copyTo~とかって怒られる場合は、OpenCV3.0.0以外のバージョンのincludeディレクトリが指定されているかも。
2017年12月現在の最新はOpenCV3.3.1なので、インストールもディレクトリの指定も間違わないように注意。

画像の変換

ということでcv::Mat←→s3d::Image。
以下のコードは
1. s3d::Imageでファイル読込→cv::Matに変換して加工→s3d::Imageを出力
2. cv::imreadでファイル読込→s3d::Imageに変換して加工→s3d::Imageを出力
をする。最後のループで両方の結果を表示。

下記コードを動かす前に、Siv3DデフォルトでExampleディレクトリに入ってる「Windmill.png」をbmpに変換した「Windmill.bmp」を同じディレクトリに置いておいてください。
ペイントとかで開いたのを保存しなおすとかでOKです。

#define NO_S3D_USING

# include <opencv2/opencv.hpp>// OpenCV3.0.0
# include <Siv3D.hpp>//Siv3D 2016Augustv2

void Main()
{
        //1. s3d::Image to cv::Mat
        s3d::Image image1(L"Example/Windmill.png");

        //s3d::Image内の画素データを参照するcv::Matを作成
        cv::Mat_<cv::Vec4b> mat1(image1.height, image1.width, static_cast<cv::Vec4b*>(image1.data()), image1.stride);

        //OpenCVの関数で加工 今回はネガポジ反転
        mat1 = ~mat1;

        for (auto it = mat1.begin(); it != mat1.end(); ++it)
        {
                //透明度も反転で0になってしまっているので255に
                (*it)[3] = 255;
        }

        //表示用        
        s3d::Texture texture1(image1);

        //2. cv::Mat to s3d::Image

        //cv::imreadはbmpしか読み込めない
        cv::Mat mat2 = cv::imread("Example/Windmill.bmp", cv::IMREAD_UNCHANGED);

        //channelが4かつ画素はRGBAの並びでないといけないので変換
        //(imreadのデフォルトはBGR)
        cv::cvtColor(mat2, mat2, cv::COLOR_BGR2RGBA);

        s3d::Image image2(mat2.cols, mat2.rows);

        //cv::Mat内の画素データをs3d::Image内へコピー
        std::memcpy(image2.data(), mat2.data, 4 * mat2.cols * mat2.rows);

        //加工
        image2.scale(0.3);

        //表示用
        s3d::Texture texture2(image2);

        while (s3d::System::Update())
        {
                texture1.draw();
                texture2.draw(texture1.width, 0);

        }
}

f:id:milk_spoon:20171112144050p:plain

1.でcv::Matを加工した後、s3d::Imageへの変換処理をしなくていいの?ってなるけど、今回はなくて大丈夫。
s3d::Image→cv::Matは「変換」と書いてるけど、s3d::Imageが持ってるデータをcv::Matが指すようになるだけなので。
当然、cv::Matを更新すると、s3d::Imageも更新されている。

初めに読み込んだイメージから別のcv::Matをcloneなどで作った場合は、cv::Mat→s3d::Imageの処理を挟む必要がある。

cv::Mat→s3d::Imageする際は、cv::Mat側がチャンネル数4で、画素がRGBAの並びでないといけない。
上記のようにmemcpyを使うと、cv::Matがメモリ上で連続データでないといけない。(ROIなど、連続データでないこともある。)
例では適当に変換しているが、本来変換元のcv::Matにどのようなものが来るかは色々あると思っていて、

  • s3d::Imageから変換したもの(チャンネル数4, RGBA)
  • cv::imreadでファイル読込したもの(チャンネル数2 or 3 or 4, グレイスケール or BGR)
  • 他のcv::Matを加工したもの(チャンネル数等不明、不連続データの可能性あり)

これらを適宜RGBAに変換などする必要がある。

変換関数

ここまで来て結構めんどくさいなーと思いました。cv::Mat→s3d::Imageするのに↑のような条件気にしたりとかはやってられない感。
なのでわりと思考停止で変換できるんじゃないかなーという変換関数たちを作った↓

github.com

↑の条件を気にしないでcv::Mat→s3d::Imageに変換したいって場合はcvsiv::MatToImageForceを呼んで渡せばだいたい変換できる。
(cv::MatがRGBかBGRかは判定できないので指定する必要アリ)
↑の条件に合っていることがわかっていればcvsiv::MatToImageを呼んだほうがコピーが少ないので良い。
cvsiv::IsConvertibleByMatToImage/IsConvertibleByMatToImageForceでそれぞれ適用可能かも判定できる。

上の例のようにs3d::Imageを参照するcv::Matを作りたいときはcvsiv::GetMatLinkedToImage。
むしろデータ共有してるとか嫌だからコピーしてcv::Mat作りたいときはcvsiv::ImageToMat。
ただ画像データは基本コピーしないほうがパフォーマンスいいと思うので…

まとめ

cv::Matの表現できる画像データはs3d::Imageより多様なので、任意のcv::Matをs3d::Imageへ変換しようとするのは意外と色々あって大変だけどもう変換関数があるので解決(?)
次はこれを使ってOpenCVで処理した画像をSiv3Dで見てみるようなものを作ってみたい。
→作りました
spoonblog.hatenablog.com


Siv3D Advent Calendar 2017 8日目でした。
次はhota1024さんです。

jupyter-themesでダーク系のテーマでもgrade3みたいな感じにする

Windows上でJupyter Notebookを使い始めた。
早速Vim-bindingにしてみたり、jupyter-themesで画面をダーク系にしてみたりと楽しんでいる(?)

ところが、このjupyter-themesのテーマ、不満な点が一つだけあった。
以下のコマンドでダーク系のmonokaiテーマへ切り替える。
ちなみに-vimVim-bindingを入れてる人用のオプション、-N -Tでファイル名やツールバー表示。

> jt -f inconsolata -t monokai -vim -N -T

結果がこの通り。

f:id:milk_spoon:20170820182559p:plain

うーむ。

デフォルトのgrade3(↓)みたいに、コードセルだけ違う色にしてあとは背景に沈ませてほしい…

f:id:milk_spoon:20170820183038p:plain

今のままだとセルが全部光っていてどこを見ればいいかわかり辛い。

コレを解決するには、テーマ切り替え時のオプションに-altmdを追加してやればよい。

> jt -f inconsolata -t monokai -vim -N -T -altmd

適用するとこんな感じ。

f:id:milk_spoon:20170820183517p:plain

まだ出力セルが少し明るいが、これはスタイルファイルの編集でなんとかなる。
スタイルファイル(.less)が
(Anacondaインストールディレクトリ)\Anaconda3\Lib\site-packages\jupyterthemes\styles\
にあるので編集する。
出力セルの背景色は@cc-output-bgの値を変更すればOK。
スタイルファイルを増やせばちゃんとjtコマンドで設定できるので自作テーマももちろん作れる。

以上。
grade3がデフォルトで背景にテキストやmdが沈むようになっているのはstylefx.pyでgrade3だけ特別処理されてるとか、-altmdオプションは公式readmeでも説明がないとか色々悩んだけどなんとかなった…