おさかなせいざ

おさかなせいざ

プログラミングメモや日記がわりに

Clojure * duct * opencv で高専時計(美人時計)を作った

この記事は Clojure Advent Calendar 2017 12日目の記事です。

※注:半分くらい日記、技術内容は下のほうなので、そこだけ見られたい方は本題までホイールをギュインとして下さい。

高専

先月11月、私が通う高専では一般にいう文化祭である高専祭が行われました。

初日は雨が降ったり、そのせいであまり人が来なかったり、子どもの台パンで機材が壊れないかハラハラしておりました。 二日目は天候にも恵まれて、公私共々楽しめるものになったのではないかと思います。

さて、この高専祭には伝統的に高専祭期間中だけに活動する高専祭部活というものがあります。 その中に E project (通称:Eプロ)という本校の4学科ある中の電気情報工学科に所属する学生だけで構成される部活があり、私はここに所属しています。 学科にちなんで、回路の体験コーナや、VR、去年度から続けて音ゲーなんてものも作って展示していました。

もちろん、情報とつくからにはインターネットでの展示も必要ということで、数年前からWebページも作り公開していたわけです。 ここでようやく題名がでてきます。

高専時計

公開しているWebページ、これが高専時計です。

ようこそ高専時計へ | Welcame to Kosen Clock

高専時計は、高専祭に向けて準備している学生や学校関係者の姿を写真に収め、それらに時刻を表示するようにして公開するというものです。 少し古いかもしれませんが、美人時計というと想像がつきやすいのではないでしょうか。 上のリンクから見られます。

今年も高専時計をやるとなり、気になっていた私は参加することにしました。 参加するからには新しいことに取り組みたいと考え、こんな提案をしました。

「当日の来校者にも高専度計に参加してもらおう」

これがアホみたいに自分を苦しめることになります。

今まで通り、学生の姿を収めることはもちろん、当日来て頂いた方たちにもスマートフォンから画像をアップロードしていただいて、それも時計にして表示する。 新しい取り組みとしては良かったと思っています。

ですが、突然他の部活に引き抜かれてデザイン担当が消えたり、他の展示の準備を手伝って時間がなくなったり、 そもそも参加人数が考えていたよりも少なく参加者に配る予定だったお菓子が余りまくったり、画像に時刻を埋め込む位置が全然違う場所になって

と言われたり、これらの片鱗はサイトからも解ると思います。 それでも、二日間で合計1500PVと100枚以上の画像をアップロードをして頂き、高専祭当日の雰囲気も感じられるサイトになりました(主観)。

本題

といったことをするために、考えなければいけなったのは環境です。 使用言語には現在もはまりにはまっている Clojure を使用しました。 理由は使いたかったから。

PHP でやればもっと簡単にできるやん。」

と言われたこともありましたし、実際そうすればよかったと作製中何度も思いましたが、結果的に Clojure で書ききりました。 夏休み前に少し書き動くようにし、💪を構成する基本単位達のインターンでこの話をしたのですが、残念ながら落ちてしまいました。 インターン行きたかった…。 次はもう少し力を付けて行きます。

プログラムの全体は大きく3つに分けられます。

  • 各URLの処理管理を行うルーティング部
  • html の生成を行うテンプレート部
  • 時刻を埋め込む画像処理部

テンプレート部とパーサ部はごちゃ混ぜになって routing.clj にあります。 画像処理部は img.clj になります。 使用した主なライブラリは

ルーティング部に duct

GitHub - duct-framework/duct: Server-side application framework for Clojure

テンプレート部にSelmer

GitHub - yogthos/Selmer: A fast, Django inspired template system in Clojure.

画像処理部にOpencCV

OpenCV library

の3つです。

ディレクトリの構成は以下の通りです。duct のテンプレートを使用しました。

.
├── dev
│   ├── resources
│   │   ├── dev.edn
│   │   └── local.edn
│   └── src
│       ├── dev.clj
│       ├── local.clj
│       └── user.clj
├── img
│   ├── local
│   └── show
├── lib
│   ├── Imshow.jar
│   ├── libopencv_java320.dylib
│   ├── libopencv_java320.so
│   └── opencv-320.jar
├── logs
│   └── dev.log
├── Procfile
├── profiles.clj
├── project.clj
├── README.md
├── resources
│   └── kosen_clock
│       ├── config.edn
│       ├── private
│       │   └── img
│       │       ├── fes52ndmarkA.png
│       │       ├── fes52ndmarkB.jpg
│       │       └── fes52ndmarkC.jpg
│       ├── public
│       │   ├── css
│       │   │   ├── bootstrap.css
│       │   │   ├── bootstrap.css.map
│       │   │   ├── bootstrap.min.css
│       │   │   ├── bootstrap.min.css.map
│       │   │   ├── bootstrap-theme.css
│       │   │   ├── bootstrap-theme.css.map
│       │   │   ├── bootstrap-theme.min.css
│       │   │   ├── bootstrap-theme.min.css.map
│       │   │   └── style.css
│       │   ├── fonts
│       │   │   ├── glyphicons-halflings-regular.eot
│       │   │   ├── glyphicons-halflings-regular.svg
│       │   │   ├── glyphicons-halflings-regular.ttf
│       │   │   ├── glyphicons-halflings-regular.woff
│       │   │   └── glyphicons-halflings-regular.woff2
│       │   ├── img
│       │   │   └── no-image.jpg
│       │   ├── js
│       │   │   ├── bootstrap.js
│       │   │   ├── bootstrap.min.js
│       │   │   └── npm.js
│       │   └── test
│       │       └── test.html
│       └── template
│           ├── base.html
│           ├── list.html
│           ├── non-upload.html
│           ├── not-found.html
│           ├── root.html
│           └── upload.html
└── src
    └── kosen_clock
        ├── boundary
        ├── handler
        │   ├── not_found.clj
        │   └── routing.clj
        ├── img.clj
        ├── info.clj
        ├── main.clj
        └── port.clj

以下ではそれぞれのライブラリの使用した内容や気づいたことについて述べます。

ルーティング部

ルーティング部には duct を使用しました。 正確に言えば duct はルーティングライブラリではありません。 duct は Clojure でhttp の処理を行う際に使われる ring 、 compojure を integrant のデータ駆動アーキテクチャ(よくわかってない)という方法を用いて データの定義や管理、integrant-replを用いて repl 駆動開発を行う際に使用するライブラリです。

duct に関して現在日本語で公開されている情報は、全体的に少し古い情報が多いです。 最新のバージョンで使用する際には、公式の readme や Wiki を参考にするほうがよいと思います。

(defmethod ig/init-key :kosen-clock.handler/routing [_ _]
  (context @access-root []
    (wrap-cookies
     (wrap-multipart-params
      (routes
       (GET "/" [] (parser/render-file "template/root.html"
                                       {:access-root @access-root
                                        :global-last-upload-image @global-last-upload-image}))
        (GET "/upload" {cookies :cookies}
             {:cookies {"markA" {:max-age 604800
                                 :value (let [v (:value (cookies "markA"))] (if-let [tmp (some #(when (= v %) %) (into @making-img @root-imgs))]
                                                                              tmp
                                                                              ""))}
                        "markB" {:max-age 604800
                                 :value (let [v (:value (cookies "markB"))] (if-let [tmp (some #(when (= v %) %) (into @making-img @root-imgs))]
                                                                      tmp
                                                                      ""))}
                        "markC" {:max-age 604800
                                 :value (let [v (:value (cookies "markC"))] (if-let [tmp (some #(when (= v %) %) (into @making-img @root-imgs))]
                                                                      tmp
                                                                      ""))}}
              :headers {"Content-Type" "text/html; charset=utf-8"}
              :body (upload-html {:last-markA (let [v (:value (cookies "markA"))] (some #(when (= v %) %) (into @making-img @root-imgs)))
                                  :last-markB (let [v (:value (cookies "markB"))] (some #(when (= v %) %) (into @making-img @root-imgs)))
                                  :last-markC (let [v (:value (cookies "markC"))] (some #(when (= v %) %) (into @making-img @root-imgs)))})})
        (POST "/upload" {params :params cookies :cookies}
              (upload-image (get params :mark) (get params "file") cookies))
        (GET "/list" {params :params}
             (make-list (get params :page)))

        (GET "/festival" [] (res/redirect (str "http://tokei.maizuru-ct.ac.jp") :moved-permanently))
        (context "/festival/" []
                 (GET "*" [] (res/redirect (str "http://tokei.maizuru-ct.ac.jp") :moved-permanently)))

        (context "/img" []
                 (route/files "/" {:root @img-root})
                 (route/resources "/" {:root "kosen_clock/public/img"})
                 (route/not-found {:headers {"Content-Type" "image/jpeg"}
                                   :body no-image}))
        (route/resources "/public/" {:root "kosen_clock/public"})
        (route/resources "/fonts/" {:root "kosen_clock/public/fonts"})
        (route/resources "/test/" {:root "kosen_clock/public/test"})
        (route/resources "/css/" {:root "kosen_clock/public/css"})
        (route/resources "/js/" {:root "kosen_clock/public/js"}))))))

これが実際に使用したルーティング部です。 duct は ring のラッパの側面もあるので、ここはまんま ring です。 最初の defmethod ig/init-key :kosen-clock.handler/routing ig/init-key が integrant に関数の登録を行う部分です。 config.edn で役割を定義しています。 これらを定義することで、コードの変更を行っても直ぐに repl 上から反映することができます。

以下が config.edn の中身です。

{:duct.core/project-ns  kosen-clock
 :duct.core/environment :development

 :duct.module/logging {:environment :development}
 :duct.module/web {}

 :duct.router/cascading
 [#ig/ref :kosen-clock.handler/routing]

 :duct.handler.static/not-found {:response "This page cannot be found"
                                 :headers {"Content-Type" "text/html; charset=utf-8"}
                                 :body #ig/ref :kosen-clock.handler/not-found}

 :kosen-clock.handler/routing {}
 :kosen-clock.handler/not-found {}
 :kosen-clock/port {}
 }

使用するプラグインなどもここで指定します。

使用してみて、変更をすぐに反映したり、複雑な設定が必要な部分を担ってくれて大変ありがたかったです。

テンプレート部

テンプレート部には Selmer を使用しました。 Selmer はテンプレート元となるhtmlデータに処理を行い、コードを返すというシンプルなライブラリです。

例えば、使用するヘッダは同様ということは多いです。その時に、

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
    <body>
    {{body|safe}}
    </body>
</html>

というテンプレートファイルを用意しておき、

(selmer.parser/render-file "template.html" {:body "<p>hello</p>"})

とすると

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
    <body>
    <p>hello</p>
    </body>
</html>

と返ってきます。もちろん<>がそのまま埋め込まれるのは危険なので、通常は &lt &gt に置換されます。 今回であれば、必要であるのでテンプレートにsafe オプションを入れることでそのままタグとして使用しています。

Selmer 以外にも テンプレートライブラリはありますが、個人的に一番シンプルで分かりやすかったので使用しました。

画像処理部

ほとんどjavaです。もちろん Clojure で書いたのですが、画像処理に使用した Opencv が参照を使って中を直接弄りまくるせいで Clojure っぽさがほとんどありません。 java のラッパを使っているのですから(そもそも C++ ですし)当たり前です。

例えば以下を見て下さい。

(dorun (map (fn [r]
    (let [sm (.submat dst r)
           smc (.submat dst-clone r)
           tmp (Mat.)]
       (Imgproc/GaussianBlur smc smc (Size. 31 31) 8 6)
         (.release sm)
         (.release smc)
         (.release tmp)))
       @roiRects))

これは時刻を表示する位置にぼかし処理を行う部分なのですが、なんだよ release って! python のラッパのように戻り値を採用してほしかったです。 このような処理がたーくさんあり、今読み返しても何を書いているのかわからないところが多いです。

使用する言語選択を失敗した悪い例ではないでしょうか。 目的にあった言語を選びましょう。

まとめ

どのライブラリも使いやすく、期限ギリギリではありましたが高専時計を完成させることが出来ました。 html や css の知識が足りず少し物足りない見た目にはなってしまいましたが、 自分の中で、webサービスとしてここまで大きな物を完成されられたことはいい経験となりました。

教訓:人の管理はちゃんとしよう。

追記

最後に高専時計のソースコードは以下にありますので、よろしければご覧ください。

github.com

明日は anolivetree さんの AndroidClojureを使う話 です。 楽しみ。