空飛ぶ気まぐれ雑記帳

主に趣味とかプログラミングについて扱います。

PythonでMarkdownをHTMLに変換する

4月になってから環境が変わってクソ忙しくなったせいか、録にブログを更新できず。
なんだかんだで、もう5月も終わり。プログラムを書いてないわけじゃないけど、githubの履歴を見る限り一ヶ月でたったの6000行しか書いてないので、全然ですね。
それはさておき、流石に月間更新数0はいただけないので、何か書こうと思っていたら、ネタはあったので投下します。

# なぜにPythonで?
ただMarkdownを変換するだけならpandocなりを使えば容易に実現可能なのですが、table要素にはclassを追加して、相対パスの画像をサーバにアップロードしたURLに変換してという作業を自動化するのはそう簡単ではありません。
それでペチペチとGoogle先生にご教示願ったら、それっぽいのがちゃんとあったんですよね。


Python Markdown

# 見付けたは良いが…
ただ、コイツ。ドキュメントを読んでもさっぱり使い方がわからない。
いや、少なくとも、Markdown->HTMLに変換する方法はよく分かるのだけれども、それ以上のことをしようとすると何をする必要があるのかさっぱりわからない。
実際Githubのissueを見ると山のような質問。あっ察しというわけで、再びGoogle先生にご教示願うと無事それっぽいのを見つけることができました。
ana-balica.github.io

一通りの使い方はなんとなく書いてあったので、実装してみました。
以下が実際のコード

#encoding:UTF-8

import sys
import glob
import os
import os.path
import markdown
import codecs
from markdown import Extension
from markdown.util import etree
from markdown.treeprocessors import Treeprocessor
from markdown.extensions import tables, toc

def main(argv):
  if len(argv) < 3:
    print('convert.py <Input directory> <output directory>')
    return

  md = markdown.Markdown(extensions = [ElementTreeExtension(), 'markdown.extensions.tables', 'markdown.extensions.toc'])

  input_dir = argv[1]
  output_dir = argv[2]

  try:
    os.mkdir(output_dir)
  except OSError as e:
    if not os.path.exists(output_dir):
      rethrow()

  for file in glob.iglob(os.path.join(input_dir, "**/*.md"), recursive=True):
    print(file)
    output_filename = get_output_filename(file, input_dir, output_dir)
    with codecs.open(file, 'r', 'UTF-8') as fp, codecs.open(output_filename, 'w+', 'UTF-8') as ofp :    
      text = fp.read()
      markdown_text = md.convert(text)
      ofp.write(markdown_text)

class ElementTreeExtension(Extension):
  def extendMarkdown(self, md, md_globals):
    md.treeprocessors.add('elementtreeprocessor', ElementTreeProcessor(), '_end')
    md.registerExtension(self)
class ElementTreeProcessor(Treeprocessor):
  def run(self, root):
    h1s = root.getiterator("h1")
    for h1 in h1s:
      h1.set("class", "h")
    
def get_output_filename(input_filename, input_root_dir, output_dir):
  input_dir, filename = os.path.split(input_filename)
  base, ext = os.path.splitext(filename)
  relative_root_to_file = os.path.relpath(input_dir, input_root_dir)
  output_full_path = os.path.join(output_dir, relative_root_to_file)
  if not os.path.isdir(output_full_path) and not os.path.exists(output_full_path):
    os.mkdir(output_full_path)

  return os.path.join(output_full_path, base + ".html")

if __name__ == '__main__':
  main(sys.argv)

第1引数にMarkdown形式のファイルが保存されたディレクトリ。
第2引数に出力ファイルが保存されるディレクトリを設定すれば良い。
これで変換したhtmlのh1要素にhというclassが自動で追加されるようになっている。

スクリプトとDLLの間に生きる時のデバッグ

はじめに

4月になって環境が変わって色々と忙しい今日このごろ。
気づけば4月も末日、そろそろGW前だというのに未だに慣れず。

そう、4月末なのに今月はまだ1記事も書いてないのです。
マズイ…

というわけで、今回はちょっとしたデバッグのテクニックをご紹介。
他の人がCでDLL作ったけど、Pythonから関数呼び出して使うとなぜか動かない…でも、他人のソースを全部読むのはダルい…という場合にオススメです。

DLLを作る

とりあえず、今回は最小限の構成でDLLを作るのでヘッダーファイルなしのcppファイルのみです。

CMakeLists.txt

cmake_minimum_required(VERSION 3.3)

project(DLL)

add_library(${PROJECT_NAME} SHARED DLLFunc.cpp)

DLLFunc.cpp

#include <iostream>
#include <random>

#define DLL_EXPORT __declspec(dllexport)

extern "C" DLL_EXPORT float add(int x, int y)
{
  float value = std::random_device()();
  return x + y + value;
}

extern "C" DLL_EXPORT void print(const char* s)
{
  std::cout << "DLL:" << s << std::endl;
}


これをcmakeしてからビルドすればとりあえず、DLLが出来上がります。
この時、DLLのデバッグをしたいので、DLLのビルド時のConfigはReleaseではなく、DebugかRelWithDebInfoを選択してください。
このRelWithDebInfoというのは最適化は掛けるけど、デバッグシンボルを埋め込むため、ブレークポイントを挿入したときにちゃんと止まってくれるようになります。(実際は一部最適化の関係でブレークポイントを挿入できない場所もあります)

DLLのテスト

最初はとりあえず、C++からテスト。というわけで、下記のコードを使います。コマンドライン引数にDLLのパス(Debugビルドのもの推奨)を与えてください
なお、もちろんですが下記のファイルは別のディレクトリに保存してくださいね。

CMakeLists.txt

cmake_minimum_required(VERSION 3.3)

project(Main)

add_executable(${PROJECT_NAME} main.cpp)

main.cpp

#include <iostream>
#include <string>
#include <Windows.h>

int main(int argc, char** argv)
{
  if (argc < 2)
  {
    std::cerr << "Wrong arguments!\nMain.exe <Input dll path>" << std::endl;
    return -1;
  }

  auto dll = LoadLibraryA(argv[1]);

  auto print_fn = GetProcAddress(dll, "print");
  auto add_fn = GetProcAddress(dll, "add");

  auto value = reinterpret_cast<float(*)(int x, int y)>(add_fn)(10, 20);
  reinterpret_cast<void(*)(const char*)>(print_fn)(std::to_string(value).c_str());

  FreeLibrary(dll);

  return 0;
}


これを実行すると何か標準出力に出るはずです。
このとき、Mainソリューションを開いた状態でDLLFunc.cppをVisualStudioで開き、ブレークポイントを設定すればちゃんとブレークしてくれるはずです。

Pythonで使う

ここまでだとぶっちゃけあまり意味がないのでPythonスクリプトから呼び出します。
実際に使ったのは下記コード。

import ctypes
import sys

print('Type input dll')
input_dll = input()
print('--------------------------------------')
dll = ctypes.cdll.LoadLibrary(input_dll)

dll.print.argtypes = [ctypes.c_char_p]
dll.add.restype = ctypes.c_float
dll.add.argtypes = [ctypes.c_int, ctypes.c_int]

value = dll.add(10, 20)
dll.print(ctypes.c_char_p(str(value).encode('utf-8')))


基本的には先程のテストと同様ですが今回は実行時にちょっとした処理を必要とするため、標準入力からdllファイルのパスを受け取るようにしています。
それから実際に実行して、DLLのパスを入力する前にブレークポイントを入れるためにちょっとした処理を行います。

まず、Visual Studioを起動します。次に「デバッグ>プロセスにアタッチ」もしくはctrl+alt+pで表示されるダイアログの選択可能なプロセスから「python.exe」を探します。pとか押せばpのとこまで飛べるんでそれを活用しつつ選んでから「アタッチ」ボタンを押せばアタッチ完了です。
この状態でDLLFunc.cppを開いてブレークポイントを入れれば、pythonから該当する関数を呼び出すとちゃんとブレークポイントで止まります。

まとめ

DLLデバッグしたいと思ったらとりあえずctrl+alt+pでプロセスにアタッチと覚えとけばいいと思います。
今回は簡単な例でしたが、DLL内で例外が発生して落ちてる場合もVisual Studioがキャッチしてくれて、その時のコールスタックが見れるので、それを活用してデバッグしましょう。
それとPython以外でもmatlabやRなんかでも使えるので(matlabの場合javaで動いている影響かガンガン例外が飛び交うので少々知識が必要ですが)是非お試しあれ

OpenCVのDeformable Part Modelを試してみた

はじめに

今回は、故あってOpenCVのDPM(Deformable Part Model)を試してみたので、それについて紹介したいと思います。

DPMって?

DPMとは物体を検出するアルゴリズムの1つで、入力画像からHOG特徴を抽出し、事前に学習しておいた物体全体のモデルと物体をパーツ毎に分けた変形可能なモデルの2種類を併用することで、ロバストに検出することができるアルゴリズムです。

結構古いアルゴリズムで、原著論文が2007年位なのですが、R-CNN(Region-based Convolutional Neural Network)と比較すると高速で、実時間で利用可能なためかロボティクス系の分野では未だにチラチラ見かけます。

まあ、R-CNNも段々と改良されて、高速になりつつあって、実時間で利用可能なアルゴリズムもあった気がするので、近いうちに撲滅されかねませんが。

なお、アルゴリズムは下記が詳しいです。

www.slideshare.net

OpeCVのDPM

OpenCV3.0以降ではOpenCV Contribに入っているのでそれを使います。
公式ドキュメントとサンプルコードを参考に試してました。

http://docs.opencv.org/trunk/df/dba/classcv_1_1dpm_1_1DPMDetector.htmldocs.opencv.org
github.com

サンプルコードでは、動画を前提としてフレームレートの計算とかしていますが、私は動画を用意する余裕が無かったので、該当部分の削除等行ったソースコードを用意しました。
なお、Windows環境の場合、OpenCV公式のサンプルコードだと改行コードでポシャるので、std::ifstreamの引数に注意が必要です。(下記サンプルでは修正済みです)

#include <opencv2/dpm.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

#include <filesystem>

#include <iostream>
#include <fstream>

#if defined(DEBUG) || defined(_DEBUG)
# pragma comment(lib, "opencv_core310d.lib")
# pragma comment(lib, "opencv_dpm310d.lib")
# pragma comment(lib, "opencv_highgui310d.lib")
# pragma comment(lib, "opencv_imgproc310d.lib")
# pragma comment(lib, "opencv_imgcodecs310d.lib")
#else
# pragma comment(lib, "opencv_core310.lib")
# pragma comment(lib, "opencv_dpm310.lib")
# pragma comment(lib, "opencv_highgui310.lib")
# pragma comment(lib, "opencv_imgproc310.lib")
# pragma comment(lib, "opencv_imgcodecs310.lib")
#endif

void help();
bool readImageLists(const std::string &file, std::vector<std::string> &imgFileList);
void drawBoxes(cv::Mat &frame,
  std::vector<cv::dpm::DPMDetector::ObjectDetection> ds,
  cv::Scalar color);

int main(int argc, char** argv) try
{
  const char* keys =
  {
    "{@model_path    | | Path of the DPM cascade model}"
    "{@image_dir     | | Directory of the images      }"
  };

  cv::CommandLineParser parser(argc, argv, keys);
  std::string model_path(parser.get<std::string>(0));
  std::string image_dir(parser.get<std::string>(1));
  std::string image_list = image_dir + "/files.txt";

  if (model_path.empty() || image_dir.empty())
  {
    help();
    return -1;
  }

  std::vector<std::string> imgFileList;
  if (!readImageLists(image_list, imgFileList))
    return -1;

#ifdef HAVE_TBB
  std::cout << "Running with TBB" << std::endl;
#else
#ifdef _OPENMP
  std::cout << "Running with OpenMP" << std::endl;
#else
  std::cout << "Running without OpenMP and without TBB" << std::endl;
#endif
#endif

  cv::Ptr<cv::dpm::DPMDetector> detector = cv::dpm::DPMDetector::create(std::vector<std::string>(1, model_path));

  cv::namedWindow("DPM Cascade Detection", 1);
  // the color of the rectangle
  cv::Scalar color(0, 255, 255); // yellow
  cv::Mat frame;

  const int MAX_LONGSIDE = 600;
  for (size_t i = 0; i < imgFileList.size(); i++)
  {
    std::vector<cv::dpm::DPMDetector::ObjectDetection> ds;

    cv::Mat image = cv::imread(image_dir + "/" + imgFileList[i]);
    if (image.cols > MAX_LONGSIDE)
    {
      cv::resize(image, image, cv::Size(MAX_LONGSIDE, static_cast<float>(MAX_LONGSIDE) * image.rows / image.cols));
    }
    else if (image.rows > MAX_LONGSIDE)
    {
      cv::resize(image, image, cv::Size(static_cast<float>(MAX_LONGSIDE) * image.cols / image.rows, MAX_LONGSIDE));
    }

    frame = image.clone();

    if (image.empty()) {
      std::cerr << "\nInvalid image:\n" << imgFileList[i] << std::endl;
      return -1;
    }

    // detection
    detector->detect(image, ds);
    
    // draw boxes
    drawBoxes(frame, ds, color);
    std::cout << "Frame:" << i << std::endl;

    // show detections
    imshow("DPM Cascade Detection", frame);

    cv::waitKey(0);

    cv::imwrite(image_dir + "/s" + std::to_string(i + 1) + ".jpg", frame);
  }

  return 0;
}
catch (std::exception& e)
{
  std::cerr << e.what() << std::endl;
  throw;
}

void help()
{
  std::cout << "\nThis example shows object detection on image sequences using \"Deformable Part-based Model (DPM) cascade detection API\n"
    "Call:\n"
    "./example_dpm_cascade_detect_sequence <model_path> <image_dir>\n"
    "The image names has to be provided in \"files.txt\" under <image_dir>.\n"
    << std::endl;
}


bool readImageLists(const std::string &file, std::vector<std::string> &imgFileList)
{
  std::ifstream in(file.c_str(), std::ios::in);

  if (in.is_open())
  {
    while (in)
    {
      std::string line;
      std::getline(in, line);
      if (line.empty())
      {
        continue;
      }

      imgFileList.push_back(line);
    }
    return true;
  }
  else
  {
    std::cerr << "Invalid image index file: " << file << std::endl;
    return false;
  }
}


void drawBoxes(cv::Mat &frame, std::vector<cv::dpm::DPMDetector::ObjectDetection> ds, cv::Scalar color)
{
  if (ds.empty())
  {
    return;
  }

  cv::rectangle(frame, ds[0].rect, cv::Scalar(0, 0, 255), 2);
  for (unsigned int i = 1; i < ds.size(); i++)
  {
    cv::rectangle(frame, ds[i].rect, color, 2);
  }
}

また、今回は手間を省くために学習済のモデルを使うので、下記リンクのmotorbike.xmlを用います。

github.com

実践

今回紹介しているサンプルでは、下記のように引数を与える必要があります。

TestDPM motorbike.xml <画像が保存されているディレクトリ> 

また、画像が保存されているディレクトリに、files.txtというファイル名の画像ファイルを改行区切りで列挙する必要があります。
なお、画像ファイルのパスはfiles.txtからの相対パスでOKです。

実際にバイクに対して適応した結果は下記の通りです。赤色が最も評価値が高い矩形で、黄色がその他の候補
4枚目は、さすがに斜めから撮影した画像を学習していないせいか、明らかに失敗していますが、他は結構色が似ていて難しいものもあるのですが、検出精度は良好。
ただ、メモリの使用量が尋常ではなく、600✕450の画像ファイルを処理するのに1GBほどのメモリを必要とします。

f:id:elda27:20170329005714j:plainf:id:elda27:20170329005719j:plainf:id:elda27:20170329005723j:plainf:id:elda27:20170329005727j:plainf:id:elda27:20170329005732j:plainf:id:elda27:20170329005736j:plainf:id:elda27:20170329005743j:plain

おわりに

結構いい感じに動いたので、万々歳ですね。
その割に、日本語でDPMについて検索しても使ってみたみたいな話が非常に少ない(論文はちらほら見るのですが、ブログで取り上げてる例が少ない)のが不思議なところです。
次回は自力でモデルを作りたいと思います。

古き悪しき全プロジェクト共通のVC++ディレクトリ的なSomethingを設定する方法

むか~し、むかし。Visual Studio 2008では設定>プロジェクトとソリューションからVC++ディレクトリの設定を行うことが出来た。
この設定では、全てのプロジェクトに共通した設定を書き込むことが出来、大変重宝しておった。
その一方で、色々な問題からVisual Studio 2010の頃には廃止された。

それ以来、プロジェクトに個別で設定するか、ユーザープロパティシートに書き込んで無理矢理に再現するかの2択になっている。
それで、先日Visual Studio 2017にアップデートした際に、再設定する必要があったので、書くネタにも困っていたところだし、メモとして残しておく。

ユーザプロパティシートは"%USERPROFILE%\Local\Microsoft\MSBuild\v4.0"にあるので、自分の環境に合わせて、Win32(x86)、Win64(x64)を編集する。
なお、今回紹介する設定方法はx86、x64共用なので、どちらでも同じ内容を書けば良い。

以下は実際の設定内容。

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ImportGroup Label="PropertySheets">
  </ImportGroup>
  <PropertyGroup Label="UserMacros" />
  <ItemDefinitionGroup />
  <ItemGroup />
  <PropertyGroup>
    <!--プラットフォーム名。x86かx64か-->
    <ShortPlatform Condition="'$(Platform)' == 'Win32'">x86</ShortPlatform>
    <ShortPlatform Condition="'$(Platform)' != 'Win32'">x64</ShortPlatform>
    <!--ツールセット名。v141がVisual Studio 2017-->
    <Toolset Condition="'$(PlatformToolset)' == 'v120'">vc12</Toolset>
    <Toolset Condition="'$(PlatformToolset)' == 'v140'">vc14</Toolset>
    <Toolset Condition="'$(PlatformToolset)' == 'v141'">vc14</Toolset>
  </PropertyGroup>
  <Choose>
    <When Condition="'$(PlatformToolset)' == 'v120' ">
      <PropertyGroup>
        <LibraryPath>D:\library\lib;$(LibraryPath)</LibraryPath>
        <IncludePath>D:\library\include;$(IncludePath)</IncludePath>
      </PropertyGroup>
    </When>
    <When Condition="'$(PlatformToolset)' == 'v140' AND '$(Platform)' == 'Win32'">
      <PropertyGroup>
        <LibraryPath>D:\vs14library\lib;D:\vs14library\$(ShortPlatform)\$(Toolset)\lib;D:\vs14library\lib\vc_lib;$(LibraryPath)</LibraryPath>
        <IncludePath>D:\vs14library\include;$(IncludePath)</IncludePath>
      </PropertyGroup>
    </When>
    <When Condition="'$(PlatformToolset)' == 'v140' AND '$(Platform)' != 'Win32'">
      <PropertyGroup>
        <LibraryPath>D:\vs14library\lib;D:\vs14library\$(ShortPlatform)\$(Toolset)\lib;D:\vs14library\lib\vc_x64_lib;$(LibraryPath)</LibraryPath>
        <IncludePath>D:\vs14library\include;$(IncludePath)</IncludePath>
      </PropertyGroup>
    </When>
    <When Condition="'$(PlatformToolset)' == 'v141' AND '$(Platform)' == 'Win32'">
      <PropertyGroup>
        <LibraryPath>D:\vs14library\lib;D:\vs14library\$(ShortPlatform)\$(Toolset)\lib;D:\vs14library\lib\vc_lib;$(LibraryPath)</LibraryPath>
        <IncludePath>D:\vs14library\include;$(IncludePath)</IncludePath>
      </PropertyGroup>
    </When>
    <When Condition="'$(PlatformToolset)' == 'v141' AND '$(Platform)' != 'Win32'">
      <PropertyGroup>
        <LibraryPath>D:\vs14library\lib;D:\vs14library\$(ShortPlatform)\$(Toolset)\lib;D:\vs14library\lib\vc_x64_lib;$(LibraryPath)</LibraryPath>
        <IncludePath>D:\vs14library\include;$(IncludePath)</IncludePath>
      </PropertyGroup>
    </When>
  </Choose>
</Project>

以上なのでが、この設定ファイル、記述内容から何となくお察しかもしれないが、Visual Studio 2012の頃から使いまわしている。
そのため、中々にカオスなことになっているが悪しからず。

一応各部の解説をすると、Chooseタグが所謂if文にあたるタグで、WhenタグのCondition属性に式を設定する。このときif文内で使える変数はPropertyGroup内で宣言?した変数のみとなっている。
また、というのがライブラリのディレクトリで、*.libファイルが保存されているディレクトリを足せば良い。同様に、にはヘッダファイルのディレクトリを設定すれば良い。
つまるところ"D:\vs14library"とか"D:\library"とかなってる部分を自分の環境に合わせて変えれば良い。また、私の環境の場合、cmakeを使ってインストールしたライブラリをすべて1つのディレクトリに集約しているので1つないし、2、3個しかパスを足していないが、各ライブラリを別個のディレクトリにインストールしている場合、各ディレクトリを設定する必要がある。

ぶっちゃけ、Microsoftが公開している情報にたどり着けた試しがないせいか、実に5年間この説明があっているのかすらよくわからない。ただ、動いているという事実だけはあるので、まあいいかと捨て置いているので、誰か詳しい人いたらコメントください。

Visual Studio 2017を試してみた

はじめに

そもそもVisual Studio 2017の正式版がリリースされたのは3/7のことで、リリースノートによるとVisual C++の更新は軽微なもので、CMakeのサポートがメインになっている模様。
実際、ツールチェーンのバージョンも141とVisual C++14のマイナーアップデートにあたるらしい。
個人の開発だとC++しか使わない私としては、あまり代わり映えしない内容では有るが、一応実際に使った雑感について述べていく。

www.visualstudio.com

主要な新機能

概要

主要な新機能は下記の通り。
IDE全体で見ると結構変更点もあるんだなと思います。
あ、あくまでも、C++er視点なので、それ以外の変更点は山ほどあると思いますが悪しからず。

  • Light Weight Solution Load
  • CMakeのサポート
  • Linuxを対象としたビルド

Light Weight Solution Load

Light Weight Solution Loadとは、その名の通り高速なSolution読み込み機能のことで、Solution読み込み時に全てのファイルの読み込みおよびInteli Senseによる解析を行わず、ユーザが開いたファイルなどを優先的に行うそうです。
これにより、これまで膨大な数のプロジェクトを読み込む際にかなり長い時間を待たされていたのですが、それがマシになる機能です。
実際に79プロジェクトから構成されるSolutionを読み込んで見ましたが、体感で3倍程でしょうか。
本当に早いです。

blogs.msdn.microsoft.com

しかし、この機能はデフォルトではSolution毎に右クリックで表示されるメニューからEnableする必要があります。
これを全てのSolutionに対して有効にする場合、Tools>Options>Projects and Solutions>General の順に選択し、ページ下部のLightweight Solution loadのチェックボックスをチェックすれば良いそうです。

CMakeのサポート

「フォルダを開く」機能が追加されており、CMakeLists.txtを配置することで、Visual Studioが自動で他のディレクトリ(AppData配下に)を作ってCMakeを実行してくれるようになりました。
これにより、従来一度他のエディタでCMakeLists.txtを書いてから、CMakeを実行して、やっとVisual Studioからファイルの編集を行うことができる。というよう感じだったのが、Visual Studioのみで完結してCMakeを用いたプロジェクトを構築できるようになりました。

marketplace.visualstudio.com

また、CMakeSettins.jsonという名前のファイルを配置することで、Visual Studioのバージョン、ビルド対象のアーキテクチャ毎に個別に詳細なオプションを設定することができるようなっています。

ただ、追加されたばかりの機能というだけあって、不満なところが結構あります。
まず、補完機能についてです。IDEなので補完機能も一応付いていますが、CMake tools for Visual Studioに比べても若干貧弱かなと思う所があります。
つぎに、CMakeの内部バージョンについてです。Visual Studioが呼び出すCMakeはVisual Studioが内包しているバイナリなのですがそのバージョンが3.6.0となっている点です。現在、CMakeの最新バージョンが3.8.0なので、それ比べると2バージョン前で若干古いですね。まあcontinue文がサポートされているバージョンなのでまだましかも知れませんが。
そして、最後にCMakeが実行されるタイミングについてです。CMakeが実行されるタイミングはフォルダ配下のCMakeに関係するファイル(CMakeLists.txtだけでなくソース中でincludeされているファイルも含まれている模様)を保存した際に毎回実行されるため、複数のCMakeに関係するファイルを変更する場合、毎回CMakeを実行する必要があって結構厄介です。

Linuxを対象としたビルド

つまるところVisual GDBのような機能が実装されているらしく、これで有料のVisual GDBを買う必要は無いなーって話ですかね。
現状稼働していLinux PCがないので、動作確認はしていませんが、どうなんですかね?
また試したら記事を書こうと思います。

はじめてWebアプリケーションを作った話

はじめに

プログラミングを初めて早6年と3ヶ月。
これまでに、C++でアプリケーションを作ったことは数多くあれど、Webアプリケーションは作ったことがありませんでした。
ただ、某FEの試験を受けたり、QtからGoogleAPIをJavascript経由でバインディグしたりと色々やっていた影響でHTMLやJavascriptはそこそこ書けたのでぶっつけ本番でもなんとかなるかと思って実際に作ってみました。
が…思ってた異常に大変で躓いたところも多かったので、herokuを使ってPythonでWebアプリを作るまでに躓いたところを紹介したいと思います。
下記は実際に作ったアプリケーションへのリンクです

http://ppt-scripter.herokuapp.com/ppt-scripter.herokuapp.com

何つくったの

ざっくりと説明するとMicrosoftのPower Pointで作ったスライドからノートのみを抽出するアプリケーションです。
実は前回記事のGUIアプリケーションをやめて、Webアプリケーションにしてみました。

elda27.hatenablog.com

Webアプリの構成について

今回はherokuの公式が紹介しているdjangoで作るのではなく、もう少しlightにアプリを作れるらしいflaskを使って作ってみました。
また、pythonは2系ではなく、3系を使っています。

躓いたところ

以下は実際に躓いたところについて紹介していきます。
flaskの絡みはググれば出てくるので、多くのことで悩むことが無かったのですが、herokuの仕様が?すぎて辛かったです。
なお、私は初心者なので、もしその解決方法は不味いよってところがあったら教えてくれると嬉しいです。

Javascriptで躓いたところ。

Postでファイルをアップロードする時、input type="file"とDrag&DropAPIの両方を使う場合

Google Chromeの場合input type="file"にDrag&Dropしてやるとそのままアップロードできるのだけど、Edgeだとそんなことが無かったりする。
そのため、Drag&Dropでファイルを受け取る場合、JavascriptのDrag&DropAPIを使えば良いらしいのだけど、Formによる送信と共存させる方法が最初わからなかった。
ただ、まあ良くよく調べてみると簡単な話で、input type="file"の時とDrag&Drop APIで取得したファイルそれぞれを自前でnewしたFormDataに設定してやればいいそうです。

  function uploadFiles(files)
  {
    filelist_dom.children().remove()
    var fd = new FormData()
    for(file of files)
    {
      fd.append('fileinputs', file, file.name)
    }

    $.ajax({
      type:'POST',
      contentType: false,
      processData: false,
      url: '/convert',
      data: fd,
      dataType: 'json'
    })
  }

  $('#file-upload').on('click', function(event) {
    event.preventDefault()
    $('#hidden-file-upload').click()
  })

  $('#hidden-file-upload').change(function(event) {
    console.log('File upload via hidden input')
    event.preventDefault()
    uploadFiles(this.files)
  })

herokuで躓いた所

必要なファイルについて

前述の通り公式で紹介されているPythonによるWebアプリケーションはdjangoを用いたアプリケーションで、flaskを用いたアプリケーションではありません。
そのため、必要なファイルに若干の違いがあります。

github.com

結局Webアプリケーションを実行するまでに必要な最低限のファイルは下記の3つです。

  • requirements.txt:必要なパッケージをリストするファイル
  • runtime.txt:Pythonのバージョンを指定するファイル
  • Procfile:Webアプリ実行のためのコマンドを書くファイル
requirements.txt

サーバサイドでPythonのパッケージをインストールする際に使われるファイル。
pipを使って出力すればいいらしい。

pip freeze > requirements.txt 

Anacondaを使っているなら新しく環境を作成して必要なパッケージだけをインストールして実行すれば良いと思う。

runtime.txt

herokuではPython2系がデフォルトになっているので、Pythonのバージョンを指定してやる必要があります。下記リンクを見ると使用できるPythonのバージョンが書かれているので、今回はPython3.6.0を設定しました。

devcenter.heroku.com

実際に設定ファイルに書いた内容は下記のとおりです。

python-3.6.0
Procfile

そもそもProcfileってなんぞや。という所から始まったのですが、どうやらProcfileというのは、foremanという1アプリケーションを複数プロセス管理するツールのためのファイルだそうです。
それで、ファイルフォーマットとしては、Webアプリケーションを実行する際のコマンドを列挙するファイルだそうで、下記のようなフォーマットで書くそうです。

<Process type>: <Command>

herokuでこのファイルを書く場合、Http通信のやりとりをするコマンドのに「web」を使えば良いそうです。実際にgunicornを使ってwsgi対応のWebアプリケーションとして実行する場合、下記のように書けば良いそうです。

web: gunicorn <mainにあたるスクリプトファイル(拡張子なし)>:app --preload --log-file=-

ここで、ポイントなのが--preloadオプション。herokuのtutorialでは無くても動くようなことが書いてあったのですが、私の環境だと動かなかったので、追加しました。

おわりに

今回は、herokuでWebアプリケーションを作るまでに躓いたところをまとめました。
デザインとかはお察しですが、そのうちもう少しマシにしようと思っています。

PythonでGUIアプリケーションを作ろうとした話

はじめに

今回の話はタイトルどおり、Pythonを使ってGUIアプリケーションを作ろうとした話です。
これまでに、C++でアプリケーションを作ったことは数多くあれど、他の言語でアプリケーションを作った経験は少ないものです。
特に、スクリプト言語に絞ると無いと言っても過言ではないでしょう。

というのも、スクリプト言語というと、研究で何かしらの実験をして、その実験結果を整理するためのもの。
というイメージが強く、どうしても書捨てなイメージが強かったからです。

その一方で、C++でアプリケーションを作ると常に膨大な行数のソースコードとファイル数で、1つ作り上げるのに結構な時間を要するという至極一般的な問題がありました。
その上、実装中に「この書き方はできるだろ」と思ってたら実は駄目ですということがママあり*1、急遽設計を変更する羽目に会うことがあります。

そのため、あまりC++でアプリケーションを書ききったことがないという現実があります*2

いくらなんでもそれは…と思い至り、少しでも完成率を上げるために、それならスクリプト言語で書けば良いんじゃねと結論に至りました。

そんなわけで、今回紹介するのはPythonGUIアプリケーションを作る過程の話を紹介したいと思います。

GUIライブラリの選定

tkinter

言わずと知れた、Python付属のGUIライブラリ。
比較的簡単に使えるらしいのですが、どうも完成しても見た目があまり良くないそうで、ユーザー数が少ないそうです。
実際Googleで検索してもあまり込み入った話を書いたブログが見つからなかったので、実際少ないのでしょう。
というわけで、今回はボツ案で。

wxPython

wxWidgetsPythonバインディング
C++版に比べて使いやすいらしいのですが、wxWidgetsWindows版はとにかくバグが多い*3上に、かなりお作法に厳しいので、あまり好きではありません。
というわけでボツですね。

PyQt5

Qt5のPythonバインディング
文字列を標準のstring型ではなく、QString型をバインディングしたクラスを使ったりと結構面白いバインディングの仕方をしているライブラリです。
イベントの扱いにC++版と若干違いがあって、connectでSIGNALとSLOTというマクロを使って接続していたのですが、signal型のconnectを使ってslotを直接接続する感じになっています。
書いていてよくわからないので、例をあげます。実際にボタンのクリックイベントを使う時は下記のようにします。

button.clicked.connect(self.onClicked)

ただ、このライブラリ。
色々あって、ライセンスがLGPLではなく、GPLでしかライセンスされていません。
そのため、開発したアプリケーションにもGPLでライセンスをする必要があるため、注意が必要です。

そのため、今回はできればGPLでライセンスしたく無かったのでボツにしました。

PySide

PyQt5の作者が作ったLGPLでライセンス可能なQt5のPythonバインディング
PyQt5との違いは文字列型にPython標準のstringを使っているなどなど。
詳しくはググってください。

基本的にはC++版とのQtと使い方に違いは無いので、慣れていることもあって、今回はPySideにしました。

アプリケーションのデプロイ

Pythonで作ったアプリケーションのデプロイについてです。
通常Pythonスクリプト言語ですので、実行にインタプリタを必要とします。
しかし、それだと、自分が作ったアプリケーションをプログラマでないユーザに使ってもらおうとすると、ユーザにもPythonインタプリタをインストールすることを強いる必要があります。
それを回避するためのデプロイ用のアプリケーションを選定します。

今回作成したアプリケーションはPython3系で書いたので、Python3系で問題なく動作するアプリケーションが必要です。

下記で紹介しているアプリケーション全てpipからインストール可能なのでインストールも楽に済みます。

py2exe

多分一番有名なデプロイアプリケーション。
py2appなど、Windwos環境以外に向けた実装も用意されているため一応マルチプラットフォームで活用することが出来ます。
しかし、私の環境(Python3.5.2)ではどうもうまく動作せず。
実行中にエラーメッセージを吐いて異常終了してしまったので、うまくデプロイすることが出来ませんでした。

PyInstaller

py2exeでは、アプリケーションのデプロイに設定ファイルとしてのPythonスクリプトが必要なのですが、こいつはそれを必要としません。
ソースコードを解析して、必要なDLLを決定しているそうなのですが、その解析があまり賢くないため、過剰にDLLを集めてしまうそうです。
ただ、こいつの場合、entryポイントエラー(だったかな)DLL不足ではない謎の例外を吐いて、私の環境では上手く動いてくれなかったので、諦めました。

cx_Freeze

前2つと何が違うかと言われると「なんだろう?」ってなるくらいに、よく調べていません。
それはさておき、こいつは、py2exeと同様、設定ファイルを必要とするタイプですが、結構安定して動作するそうです。
それで実際に試してみると、一部DLLが不足してEXE起動時にDLL無いよって例外吐いて落ちてしまいました。
なんだか、進歩があったようなので、もう少し調べてみると、そういう場合はpackagesに含まれなかったライブラリを手動で足せば良いそうです。

実際に書いたsetup用のスクリプトは下記の通りです。
いまいちpackagesとincludesの違いはわかりませんが…とりあえず、こう書くそうです。

import sys
from cx_Freeze import setup, Executable

base = None
if sys.platform == 'win32':
	base = 'Win32GUI'

packages = ['lxml']      # これがデプロイされなかったライブラリ
includes = ['PySide']    # デプロイ時に含めて欲しいライブラリ 
excludes = ['PyQt4']     # デプロイ時に含めて欲しくないライブラリ

setup(  
	name = 'Scripter',  # アプリケーション名
	version = '0.1',    # アプリケーションのバージョン
	description = '',
	options = { 'build_exe' : {'includes':includes, 'excludes':excludes, 'packages': packages} },
	executables = [Executable('main.py', base=base, icon='ICON.ico', targetName='Scripter.exe')] 
	#出力するアプリケーション名やアイコンファイルを設定
)

インストーラの作成

インストーラの作成というとアプリケーションを本格的にユーザにデプロイする場合にしか必要としないため、あまり需要がない or そういうことをする人は自分で調べられるからなのかはわかりませんが、このあたりは日本語資料があまりないので、とりあえず書いておこうと思いました。

Install Shield

いわずと知れた、インストーラの代名詞的存在。いろんなソフトで見かけるかと思います。
しかし、こいつ、ライセンス条項が結構怪しいらしく、使用するPCをInstallShieldの開発元に監視されるらしく結構やばい。
というわけで、今回は候補から外します。

WiX

Microsoftが現在オープンソースで開発しているアプリケーションで、XMLベースのマークアップを記述することでインストーラを作成することができます。
ただ、その仕様とXMLの言語仕様が相まって書くのが非常にめんどくさい。
その上、資料と言える資料が英語Wikiのみで学習コストも高いですし、そもそもファイル数が増えると書くのがめんどくさい。

NSIS

NSIS Wiki
というわけで、今回の本命。
こいつはWiXと同じくスクリプト形式のインストーラ作成アプリケーションなのですが、文法がスペース区切りのbashの設定ファイルのような形式になっていて、WiXとくらべて書きやすい文法になっています。
また、こいつの優れている点としてzipファイルからインストーラを自動生成できる点があります。
その設定も非常に簡単で、zipファイルを選択して、あとはOKボタンを押すだけでできます。

おわりに

今回はPythonアプリケーションをデプロイしてインストーラを作成するところまで紹介しました。特にインストーラを作る部分は初めてやってみましたが非常に簡単でした。
が、作ったアプリケーションをいかに公開するかという難題がまだ残っています。
それについては少々悩んでいるので、また今度。

*1:え、文法を覚えて無いのかって?いいえ、そういう訳ではなく。Qtお前のことだよ。

*2:だいたい10個に3個ぐらいの割合でしか完成までたどり着きません

*3:OpenGLのコンテキストが解法されずメモリリークするとか、Sliderオブジェクトの再描画がされないとか