puppeteerでスクレイピングしてブログの投稿を検知する

こんにちは、イメージ・マジックの池田です。
今回はタイトルにもある通り、puppeteerのスクレイピング(とついでにブログの投稿検知)についてやっていきます。なぜなら最近プライベートで触ったからという、とても個人的な理由です。

puppeteerとは?

puppeteerというのは、Node.jsを用いてスクレイピングをするライブラリです。
はい、死ぬほど雑な説明をしました。違うわ!とまではいかないまでもお叱りを受けそうですね。詳しくは公式サイトをご覧ください。公式の英語サイトに投げる暴挙。とにかくNode.jsでスクレイピングのできるライブラリです。しかもChromeを作っているGoogle製。使わない理由はありませんね。

どうやって使うの?

  1. Node.jsの最新版をインストールします
  2. 好きな場所にフォルダを作り、npm init -yと打ち、npm i puppeteerします
用意はこれだけです。試しに公式サイトに載っているコードを実行してみましょう。同じフォルダにexample.jsを作成し、以下のコードを打ち込み、PowerShellなどからnode example.jsで実行してみてください。
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'example.png' });

  await browser.close();
})();
同じフォルダにexample.pngというファイルが作成されたと思います。ファイルには見知らぬサイトのスクリーンショットが。puppeteerがブラウザを画面に出したりせずに撮ってきてくれたスクリーンショットです。実際にこのページにアクセスしてみると、画像そのままのサイトが表示されたと思います。
これを応用することで、ブログの投稿を検知することが可能となります。RSSがあればこういうことをしなくても大丈夫だったりしますが、そもそもRSSが下火だったり、RSSを提供するようなサイトじゃなかったりもしますからね。

プログラムの流れ

前提条件:昨日や一週間前に取ってきたタイトル一覧を手元に持っている(持っていなければ、下記手順の1~2、5だけを行います)
  1. ブログにアクセスする
  2. タイトル一覧を取得する
  3. 手元で持っているタイトル一覧と照合する
  4. 手元にないタイトルがあれば、そのタイトルを通知する
  5. 最新のタイトル一覧を手元に保存する
  6. おわり
簡単ですね。これをTypeScriptで書いたコードが下記です。私はVisual Studio Codeと組み合わせて使うTypeScriptが大好きなので、本来ならばこれの宣伝もしたいのですが、TypeScriptの素晴らしさは恐らく全ての生物が既に知っているので、ここでは取り扱いません。
import puppeteer, { Page } from "puppeteer";
import { readFileSync, writeFileSync } from "fs";
import csvParse = require("csv-parse/lib/sync")
import csvStringify = require("csv-stringify/lib/sync")
import { difference } from "./setUtils";

const CURRENT_FILE_NAME = 'current.csv';
const TITLE_SELECTOR = '.entry-title';

const readCurrentCsv = (): Set<string> | null => {
  try {
    const file = readFileSync(CURRENT_FILE_NAME);
    return new Set((csvParse(file) as Array<Array<string>>).flat());
  } catch (error) {
    // Error NO ENTryらしいです
    if (error.code === 'ENOENT') {
      return null;
    } else {
      throw new Error("知らんエラー。君誰?");
    }
  }
}

const fetchTitles = async (page: Page): Promise<Set<string>> => {
  const elements = await page.$$(TITLE_SELECTOR);
  return new Set(await Promise.all(elements.map(async ele => (await ele.getProperty('textContent'))!.jsonValue())) as Array<string>);
}

const writeCsv = (newTitles: Set<string>) => {
  writeFileSync(CURRENT_FILE_NAME, csvStringify([...newTitles].map(title => [title])));
}

const notify = (diffTitles: Set<string>) => {
  console.info(`
「${[...diffTitles].join('、')}」が開発者ブログに投稿されました。見てみましょう!
https://techblog.imagemagic.jp/
`.trim());
  // みたいのを通知したりしたいよね
}

const main = async (page: Page) => {
  const currentTitles = readCurrentCsv();
  const newTitles = await fetchTitles(page);

  if (currentTitles === null) {
    writeCsv(newTitles);
    return;
  }

  const diffTitles = difference(newTitles, currentTitles);
  if (!diffTitles.size) {
    return;
  }

  notify(diffTitles);
  writeCsv(newTitles);
}

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://techblog.imagemagic.jp/');

  try {
    await main(page);
  } finally {
    await browser.close();
  }
})();
main関数の中身を見れば、先程のプログラムの流れと大体同じなことがわかると思います。このプログラムをたとえばAWSのLambdaに乗せて、CloudWatchで定期的に叩くなどすれば、差分検知通知システムの完成です。
細かいコードの解説は省きます。皆さんも興味のあるサイト(たとえば本テックブログ!)をスクレイピングしたりして、実際に触ってみてください。

※スクレイピングは少し油断するとアクセス負荷を相手のサーバーにかけてしまったり、そもそもスクレイピングを許可していないサイトもあります。使い方を間違えれば違法行為です。この辺りのサイトを見つつ、用法用量を守って正しくお使いください。

Markdownについて

はじめに

Suzukiです。
今回はmarkdownについて書きます。

Markdown記法とは

Markdown記法とは軽量マークアップ言語の1つです。
記法については以下の公式ドキュメントを参考にしてください。
参考:mermaid公式ドキュメント -> https://mermaid-js.github.io/mermaid/#/

図表を描くならmermaid

簡単な図を表すのに便利です。
試しに書いてみると、   日本の地方別人口の円グラフ
```mermaid
pie title 日本の地域別人口(万人)(平成 22 年)
    "関東" : 4175
    "近畿" : 2250
    "中部" : 2157
    "九州" : 1467
    "東北" : 942
    "中国" : 758
    "北海道" : 552
    "四国" : 404
```
というような円グラフが描けます。
ライブエディタは以下にあります。
参考:Mermaid Live Editor -> https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0
 

メリット

HTMLで書くより簡単に記述できます。
またHTMLのタグも使用できるので便利です。
PDFに変換することで配布しやすくなります。

実際の使用法

markdownで文章を書いてmermaidで図を描きこみ、PDFに変換して配りやすくして使っています。
最近では、業務の作業手順書をmarkdownで書きPDFに変換して関係部署に配布しました。

余談

実は今書いているのもmarkdownエディタを使って書いています(笑)
右にプレビューを出しながら書くと、見栄えを確認しながら書けるので便利ですね。

キーマクロを作ってみた話

こんにちは。イメージマジック三浦です。昼食のために外に出たある日、風から感じる香りが変わったなと感じます。もうすぐ春ですね。
今回はnginxのログを解析するために、サクラエディタのキーマクロを作った話です。

キーマクロを作った経緯

システムのパフォーマンスが悪い時期があり、ボトルネック調査の一環でnginxのログを解析していました。 解析時はExcelやGoogleスプレッドシートでフィルタリングできるように、テキスト置換を繰り返してきましたが、次第にパターンが決まってきて、置換自体を手動リプレイできる状態になってきました。そしてはたと気づきました。
 

ここまでパターン化できているならキーマクロで自動化しよう!

キーマクロの記録方法

ここで、キーマクロの記録について触れておきます。
キーマクロは「ツール」>「キーマクロの記録開始」とクリックすることで、記録開始できます。タイトルバーに「キーマクロの記録中」と出てきます。
上の状態で「ツール」>「キーマクロの記録終了」とクリックすると、キーマクロ記録を終了できます。

簡単なキーマクロを作ってみる

まずは簡単なキーマクロを作ってみます。

(1,2,3,4,5)
を正規表現置換で
(1)
(2)
(3)
(4)
(5)
のようにする操作をキーマクロで保存してみます。 正規表現は以下の通りです。
「ツール」>「キーマクロの読込」とすると、以下のファイルが出てきます。
このパス内にある「RecKey.mac」が、先ほど記録したキーマクロの実体ファイルです。このファイルをデスクトップなどにコピーします(元の場所におくと次回のキーマクロ記録で消えてしまうため)。

キーマクロ機能拡張

先のキーマクロの中身はこのようになっています。
S_ReplaceAll(',', '\\)\\r\\n\\(', 1068);  // すべて置換
S_ReDraw(0);    // 再描画
Excelできれいに貼り付けられるようにしたいという目的を踏まえ、キーマクロを拡張していきます。今回は以下のような方針で拡張しました。
  • S_ReplaceAll のパターンを増やして、異なる方法での置換を実行する
  • S_ReDrow(0)を最後に呼び出すことで、画面再描画は最小限にする
  • 1行をタブで区切られた時の要素数が均一かつ、Excelに貼り付けた時に情報がきれいに出る。
拡張したキーマクロの一部を紹介します。
// HTTPメソッドの分割
S_ReplaceAll('\"(GET|POST|HEAD) ', '\t$1\t', 1068);
// HTTP Status の分割
S_ReplaceAll(' ([2-5][0-9][0-9]) ', '\t$1\t', 1068);
// 再描画
S_ReDraw(0);
拡張したキーマクロは、共通設定の「マクロ」タブで登録し、メニューから実行できるようにします。そこから先は置換結果やExcelに貼りつけた結果を確認しながら、正規表現を調整していきます。

仕上げ

Excelへの貼り付けを繰り返すうちに、以下の点も入れたいという気持ちが出てきたので、拡張中のキーマクロに組み込みます。
  • 先頭に項目行を入れたい
  • 置換後は保存しておいて開いたらすぐに貼り付けられるようにしたい
コード自体はキーマクロの記録で生成されたものをコピペします。
仕上げまで実施し、最終的には以下の通りになりました。
// ファイルの先頭に移動
S_GoFileTop(0);
// 項目行を追加
S_InsText(′項目1\t項目2\…');

// HTTPメソッドの分割
S_ReplaceAll('\"(GET|POST|HEAD) ', '\t$1\t', 1068);
// HTTP Status の分割
S_ReplaceAll(' ([2-5][0-9][0-9]) ', '\t$1\t', 1068);
(他いろいろ)
// 再描画
S_ReDraw(0);

// 保存
FileSave();
これで、アクセスログを整形しExcelに貼り付ける直前まで整形する操作のキーマクロができました。作ってみると非常に便利で、心の中でドヤ顔しています。

最後に

冒頭に記載したボトルネックについては、部員の皆様の活躍により、現在は改善されています。

UWPアプリからOpenCVしてみる(その2)

矢野です。UWPアプリからOpenCVを使う話の決着をつけておきます。 Visual Studioを起動して、メインページにボタンを追加します。 
NuGetを使って、OpenCvSharp(OpenCvSharp4, OpenCvSharp4.runtime.uwp)をインストールします。 ボタンのクリックイベントハンドラ(find_Click)に、カメラからのキャプチャ、データ変換、ARマーカーの検出、結果を表示という流れでコードを書いていきます。
private async void find_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
    ImageEncodingProperties encProp = ImageEncodingProperties.CreateBmp();

    var source = captureElement.Source;

    SoftwareBitmap bitmap = null;
    using (var captureStream = new InMemoryRandomAccessStream())
    {
        await source.CapturePhotoToStreamAsync(encProp, captureStream);
        await captureStream.FlushAsync();
        captureStream.Seek(0);

        var decoder = await BitmapDecoder.CreateAsync(captureStream);
        bitmap = await decoder.GetSoftwareBitmapAsync(
            BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
    }

    Mat mat = null;
    using (var stream = new InMemoryRandomAccessStream())
    {
        var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, stream);
        encoder.SetSoftwareBitmap(bitmap);
        await encoder.FlushAsync();

        mat = Mat.FromStream(stream.AsStreamForRead(), ImreadModes.Color);
    }

    var grayMat = new Mat();
    Cv2.CvtColor(mat, grayMat, ColorConversionCodes.BGR2GRAY);

    CvAruco.DetectMarkers(grayMat,
        CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict4X4_50),
        out Point2f[][] corners, out int[] ids, DetectorParameters.Create(), out Point2f[][] rejectedImgPoints);

    CvAruco.DrawDetectedMarkers(mat, corners, ids);

    using (var stream = new MemoryStream())
    {
        mat.WriteToStream(stream);
        using (var raStream = stream.AsRandomAccessStream())
        {
            var decoder = await BitmapDecoder.CreateAsync(raStream);
            bitmap = await decoder.GetSoftwareBitmapAsync(
                BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
        }
    }

    var imageSource = new SoftwareBitmapSource();
    await imageSource.SetBitmapAsync(bitmap);
    image.Source = imageSource;
}
OpenCvSharpは優れたライブラリで、.NETアプリケーションからOpenCVを使いやすくラップしてくれています。おかげでOpenCVでのARマーカー検出、描画は難しくありませんでした。 ところが、それ以前にUWPのSoftwareBitmapとOpenCVのMat型の相互の変換についてはなかなか消化することができず、あちこちのオンラインドキュメントやブログやフォーラムを何度も何度も巡回しつつ、試行錯誤しつつ行ったり来たりを繰り返してようやくそれらしい形にたどり着きました。 あまりそんなことばっかりやっているわけにはいきませんが、ある程度余裕を持て、藻掻いて、悩む時間も時には大事ですね。
これでもって改めて前回の記事をカメラで撮ってみる(横着)と、無事にARマーカーとそのID(3)が認識されていることが確認できました。

PHP7.4移行作業の苦労小話

はじめに

こんにちは、イメージ・マジックのもあいです。 今回は直近で作業をしていたPHP7.0からPHP7.4への移行作業について、手動での対応ではまった点についての備忘録です。

プロパティの型が指定できることによって・・・

PHP7.4の大きな違いの一つとしてはプロパティの型が明示できるようになったことがあると思いますが、これにより移行作業が思った以上に手間だったという印象がありました。自分が体験した内容を記していきます。 サンプルとして下記コードを用意しました。

<?php
declare(strict_types=1);

class Hoge {
        private $prop1;
        private string $prop2;
        public function getProp1() {
                return $this->prop1;
        }
        public function getProp2():string {
                return $this->prop2;
        }
}
$a = new Hoge();
var_dump($a->getProp1());
var_dump($a->getProp2());

サンプルとして用意したコードですが、このコードは16行目でエラーとなり実行が止まります。Hogeクラスのプロパティprop2ですが、string型を明示しているため、初期化しないでアクセスすると下記のようなエラーとなります。

PHP Fatal error:  Uncaught Error: Typed property Hoge::$prop2 must not be accessed before initialization in /home/moai/sample/test01.php:11

昔からPHPコードを書いていた人間としては、ここまでチェックされるとPHPの簡単なことが簡単なコードで実現できるという手軽さが無くなってきている印象を受けます。Javaのような言語に比べれば全然マシですけれども。それだけPHPが使われており単純なミスを抑止できるように型チェックをするようになっているとも言えます。 上記のエラーですが対応方法はいくつかあって、コンストラクタで初期化する、nullで初期化する、プロパティを参照するところで\Errorをcatchする、といったことができます。さすがに\Errorをcatchはやり過ぎな気もします。nullで初期化するのが妥当な気がします。

最後に

そのほかに、PHPが用意している文字列操作の関数引数にnullが入るとエラーになるという地味に面倒な物もありましたが、null合体演算子を入れれば比較的簡単に解決できますが、使用箇所が多いと地味に面倒でした。

行方不明の要素の捜索

株式会社イメージ・マジックの技術ブログ、先週の担当のsoenoです。
正月付近の投稿ということでそれっぽい何かを考えていたのですがもう2月だそうです。
流石に正月気分でもないので当初の予定はやめ、今回は行方不明の要素の捜索について書きます。

HTML上で消えた要素を探す。

html上で色々要素を置いていきふと気が付くと、html上は存在するあの要素がない!
そんなときのよくやる手口の紹介です。普通にやっている人もいるかと思いますが…

方法

開発者ツールなり、cssへのクラスの追加なりで以下の指定を追加。
.search{
    position: absolute;
    top: 0;
    left: 0;
    height: 100px;
    width: 100px;
    border: solid 5px red;
    background: blue;
    display: block;
  z-index: 9999999999;
  opacity: 1;
  color: green;
}
まず上の指定を捜索対象の要素に指定を足します。
クラスごと貼り付けるなり、ディベロッパーツールで追加するなりしてください。
この時打ち消されて効いていない箇所があった場合は衝突箇所を確認します。

次に表示が確認出来たら消えた原因を調べます。
(そのまま指定を足したままにするわけにはいきませんので)
付けた指定を一個づつ落とすなどし、怪しいところを絞り込みます。

最後に絞り込んだ箇所を問題のない指定に変えて終了です。
(疑似要素の場合はcontentsへの指定を追加するなど逐次書き換え。)

結論

今回この記事を書こうと思ったのは修正中のページの中で行方不明要素が出たからでした。
原因はdivの閉じタグが編集中に消えhtmlが崩れていたことでした。
まずhtmlをきれいに書き、行方不明の要素を出さないのが一番ということでしょうか。

RectorでSymfony4への変換

こんにちは、岡野です。

最近、Symfony2.8/PHP7.0で実装されたサービスをSymfony4.4/PHP7.4へバージョンアップしました。その際ソースコードの自動変換に使用したRectorというソフトウェアを紹介します。
詳しくは https://getrector.org/ を見ていただくとし、実際の変換結果を挙げていきます。    

変換結果

Symfony2.8 -> 2.8

元々Symfony2.8で実装していますが念のためRectorを実行します。

最初に設定ファイルを準備します。
$ vi rector.php
...
$parameters->set(Option::PATHS, [
    __DIR__.'/app',
    __DIR__.'/src',
]);
$parameters->set(Option::EXCLUDE_PATHS, [
    __DIR__.'/app/SymfonyRequirements.php',
    __DIR__.'/app/cache/',
    __DIR__.'/src/AppBundle/Tests/',
]);
$parameters->set(Option::PHP_VERSION_FEATURES, '7.0'); // 適宜変更
$parameters->set(
    Option::SYMFONY_CONTAINER_XML_PATH_PARAMETER,
    __DIR__.'/appDevDebugProjectContainer.xml'
);
そしてRectorを実行します。
$ vendor/bin/rector process --set symfony28
以下変換されました(以降、変換結果の一部を抜粋します)。
/**
 * @Route(defaults={"foo": ""}, ...)
 */
↓ ↓ ↓
/**
 * @Route(defaults={"foo"= ""}, ...)
 */
 

Symfony2.8 -> 3.0

$ vendor/bin/rector process --set symfony30
$form = $this->createForm(new Foo());
↓ ↓ ↓
$form = $this->createForm(\AppBundle\Form\Foo::class);
 

Symfony3.0 -> 3.4

(symfony31~symfony33は差分が発生しませんでした)
$ vendor/bin/rector process --set symfony34
/**
 * @Route(...)
 * @Method("POST")
 */
↓ ↓ ↓
/**
 * @Route(..., methods={"POST"})
 */
 

PHP7.0 -> 7.0

$ vendor/bin/rector process --set php70
isset($foo[$bar]) ? $foo[$bar]: 0
↓ ↓ ↓
$foo[$bar] ?? 0

rand();
↓ ↓ ↓
random_int(0, mt_getrandmax());
 

PHP7.0 -> 7.1

$ vendor/bin/rector process --set php71
list($a, $b) = $this->foo();
↓ ↓ ↓
[$a, $b] = $this->foo();

count($foo)
↓ ↓ ↓
is_array($foo) || $foo instanceof \Countable ? count($foo) : 0 // php73実行時に改善される
 

Symfony3.4 -> 4.2

(symfony40,symfony41は差分が発生しませんでした)
$ vendor/bin/rector process --set symfony42
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller
↓ ↓ ↓
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class FooController extends AbstractController
 

PHP7.1 -> 7.3

(php72は差分が発生しませんでした)
$ vendor/bin/rector process --set php73
is_array($foo) || $foo instanceof \Countable ? count($foo) : 0
↓ ↓ ↓
is_countable($foo) ? count($foo) : 0

json_encode($foo)
↓ ↓ ↓
json_encode($foo, JSON_THROW_ON_ERROR)

json_decode($foo)
↓ ↓ ↓
json_decode($foo, false, 512, JSON_THROW_ON_ERROR)
 

Symfony4.2 -> 4.3

$ vendor/bin/rector process --set symfony43
$event->getDispatcher()->dispatch(
    FOSUserEvents::SECURITY_IMPLICIT_LOGIN,
    new UserEvent($event->getUser(), $event->getRequest()));
↓ ↓ ↓
$event->getDispatcher()->dispatch(
    new UserEvent($event->getUser(), $event->getRequest()),
    FOSUserEvents::SECURITY_IMPLICIT_LOGIN);

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
public function foo(GetResponseEvent $event)
↓ ↓ ↓
use Symfony\Component\HttpKernel\Event\RequestEvent;
public function foo(RequestEvent $event)
 

Symfony4.3 -> 4.4

$ vendor/bin/rector process --set symfony44
// Command class
public final function execute(InputInterface $input, OutputInterface $output)
↓ ↓ ↓
public final function execute(InputInterface $input, OutputInterface $output): int
 

PHP7.3 -> 7.4

$ vendor/bin/rector process --set php74
/**
 * @var integer
 * @ORM\Column(name="id", type="integer")
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="AUTO")
 */
private $id;
↓ ↓ ↓
/**
 * @ORM\Column(name="id", type="integer")
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="AUTO")
 */
private int $id;

/**
 * @var integer
 * @ORM\Column(name="foo", type="integer", nullable=true)
 */
private $foo;
↓ ↓ ↓
/**
 * @ORM\Column(name="foo", type="integer", nullable=true)
 */
private ?int $foo = null;

/** @var EntityManager */
private $em;
↓ ↓ ↓
private \Doctrine\ORM\EntityManager $em; // use形式への変更は手作業が必要

1024
↓ ↓ ↓
1_024 // この変換は微妙
 

Twig1.x -> 2.0

$ vendor/bin/rector process --set twig20
new \Twig_SimpleFilter(...)
↓ ↓ ↓
new \Twig_Filter(...)
 

Twig2.0 -> 2.4.0

$ vendor/bin/rector process --set twig240
new \Twig_Filter(...)
↓ ↓ ↓
new \Twig\TwigFilter(...)
 

まとめ

正規表現による置換よりもソフトウェアで一括変換した方が抜けも誤りも少ないでしょう。

だし誤変換もごく一部あります。未定義の配列へ要素追加しているコードが $foo = (array)$foo; $foo[] = $bar; と変換されるなどです(元々未定義なことが良くないのですが)。

って変換結果にざっと目を通すことは必要ですが、ほとんどはPHP/Symfony文法エラーのため容易に気付けます。変換後にPHPStormのInspect Code機能で確認することも有効でした。

当然
ながらRectorで変換できない点もありますので手動対応は必要です(src/AppBundle -> src/など)。

その他

­ 上記以外にもRectorのルールセットは多数ありますので興味ありましたら以下をご覧ください。
https://github.com/rectorphp/rector/tree/master/config/set

convertコマンド1回での画像処理【Imagemagick】

こんにちは。入社して約半年経ちました、ふくまです。
この時期は寒くてエアコン暖房必須です。少し前にSNSで「寝るとき暖房つけるか、つけないか」の質問をしたところ、30弱の票が集まり「暖房つけない」のほうが若干多かったです。 私は寒い時にエアコンも加湿器もガンガンに「つける」ので、個人的に衝撃の結果でした。
最近Imagemagickのコマンドと少しお友達になれたので、その内容について書いていきます。  

convertコマンドを1回で実行する理由

その方が処理が早いためです。 ですが、ネットで参考にさせていただいた記事だと、convertコマンドで画像を何回かに分けて生成し、最終的な画像を生成しているものが多かったです。

生成する画像

複数の画像処理を一括で行うため、今回はこれら2つの画像を使用することにします。(「いらすとや」さん、ありがとうございます。https://www.irasutoya.com/)  

すごい勢いで土下座をする人(syazai.png)  

とても怒っているカバ(kaba.png)

カンの良い方はもうお気づきかと思いますが、「怒ったカバに向けて全力で謝罪をする人」という構図の画像を生成します。
作業場所は上記2枚の画像を置いたディレクトリ内で、画像の生成先も同じ場所です。
コマンド作成手順は、1. kaba.pngを左右反転、2. syazai.pngを縮小、3. キャンバスの上に1,2を配置(コマンド1回で実行)、です。

convertコマンドを1回で実行する

1.kaba.pngを左右反転
convert kaba.png -flop output1.png
2.syazai.pngを縮小
convert syazai.png -resize 50% output2.png
3.キャンバスの上に1,2を配置(コマンド1回で実行)
convert \( -size 1255x705 xc:white -gravity west \( kaba.png -flop \) -composite -gravity southeast \( syazai.png -resize 50% \) -composite \) output.png

3の実行結果

今後見ることは無いであろう構図です。

コマンドの説明

3のコマンドを、外側から順を追って説明します。
convert \( A \) output.png
上記は、Aの処理の結果をoutput.pngに出力します。 次にAの中身を見ます。
-size 1255x705 xc:white -gravity west \( B \) -composite -gravity southeast \( C \) -composite
-size 1255×705 xc:whiteで、出力する画像サイズと背景白を定義しています。
そして-gravity west ( B ) -compositeで、キャンバスに左寄せでBの画像を出力することを指定しています(-compositeは複数画像を合成する際に使うオプションです)。
-gravity southeast ( C ) -compositeで、キャンバスに右下寄せでCの画像を出力します。
そして、Bは反転したカバ、Cは謝罪してる人の縮小した画像をそれぞれ返します。

最後に

今回はconvertコマンド1回で複数処理を実行する方法を紹介しました。
Imagemagickのコマンドは奥が深そうなので、今後も使い方を探索していきたいです。

Electronを触ってみた件について

●はじめに

こんにちは、イメージマジックのSuzukiです。
最近かなり寒くなってきました。
電気毛布を2枚フル活用しながら寒さを凌いでいます。
最近は「electron」を使っています。
その為、今回は「electron」について書いていこうと思います。

●electronとは

・特徴
HTML + CSS + Node.js でアプリが作れる。
これ1つで Windows, Mac, Linux 向けのアプリが作れる。
他のものに比べて敷居が低い。

●インストール

1.nodeのインストール
Windowsの場合、 https://nodejs.org/download/ からmsiをダウンロードしてインストール。
2.Electronのインストール
$ npm -g install electron-prebuilt
3.プロジェクトの作成
アプリケーション用のディレクトリを作成し、その下で npm init します。
アプリケーションのエントリポイントは「index.js」になっています。

●参考

https://qiita.com/nyanchu/items/15d514d9b9f87e5c0a29

●最後に

electronの特徴や簡単な使い方について紹介しました。
Webの技術(HTML5やJavaScript)を使っているので、習得しやすいメリットがあります。

Imagemagickを使った減色処理

こんにちわ。今週のテックブログ、担当のURAです。
最近めっきりと寒くなりましたね。私は毎朝子供を保育園に連れて行くのですが、なぜか保育園は暑いのです。外は肌寒いのに、保育園の中で子供を教室に連れて行ったり荷物をセットしているだけで、ちょっと汗をかくくらい暑いのです。これ保育園あるあるだと思うのですが、世のお父さん方どうでしょうか?

ですよね?保育園、暑いですよね?
はい、全力で共感してもらえて満足したので、今回のテーマ「減色」の話に移ります。

Why 減色?

最近、とあるプロジェクトで「減色」の実装をしていました。
弊社は様々なアイテムにオンデマンドでプリントする会社です。
プリント方法にも色々あり、インクジェットプリントであれば写真のようなフルカラーの画像をプリントできますが、プリントする色数が1色のみであったり、2色のみであったりと限られているプリント方法もあります。
そして、注文時に指定された加工方法の色数と、お客様がアップロードしたデザインデータの色数は必ずしも一致しません。
そこで、デザインデータを決まった色数に減らす処理、つまり「減色」が自動的にできると、デザインデータを補正できるというわけです。

Imagemagickで減色してみる

減色処理にはいくつかアルゴリズムがあります。
今回は、Imagemagickを使って減色する方法をご紹介します。
説明のため、とある画像を8色に減色することを考えていきます。

色のビット深度を指定して減色する

「ビット深度」とは、画像の各チャネルを何ビットで表現しているかという数値です。
例えば、ビット深度が8ビットのRGB画像とは、Red / Green / Blueチャネルのそれぞれが8ビット(256色)で表現されている画像であり、1ピクセルはRed / Green / Blueを組み合わせた情報ですので、
256 * 256 * 256 ≒ 1678万
約1678万パターンの色表現が可能な画像という意味です。

Imagemagickの「depth」コマンドを使うと、ビット深度を指定して減らすことができます。
お題は8色に減色することですので、ビット深度を1ビット(Red:2 * Green:2 * Blue:2 = 8色)に減色します。
convert dance.png -depth 1 dance-depth1.png
単純に画像のビット数を減らすと、肌色が消えてしまいました。

パレットを指定して減色する

Imagemagickの「remap」コマンドを使うと、指定したパレットに使われている色のみになるよう、画像を再構成することができます。
今回は、パレットの色は元画像を見ながら大まかに決定し、以下のようなコマンドでパレットを作成しました。
convert -size 60x60 xc:"rgb(0,0,0)" xc:"rgb(255,255,255)" xc:"rgb(255,0,0)" xc:"rgb(0,255,0)" xc:"rgb(0,255,255)" xc:"rgb(0,0,255)" xc:"rgb(255,0,255)" xc:"rgb(254,220,189)" +append palette8.png
remapコマンドは以下です。
convert dance.png +dither -remap palette8.png dance-remap.png
上記のコマンドに「+dither」とありますが、これは色を組み合わせて中間色を表現する「ディザ」処理を入れないという意味です。
減色する8色以外の中間色が表現されてしまうと見づらいので、今回はディザ処理を省いています。

パレットを自身で作成したため肌色は出ていますが、細かくパレットを調整したわけではないので、例えば洋服の水色は色に差が出ています。
また、パレットの8色にしか減色できないため、様々な画像を自動的に減色する用途には向かなそうです。

画像の色分布から似た色を減色する

Imagemagickの「colors」コマンドを使うと、画像の色分布を元に似た色を減らすことができます。
アルゴリズムについて知りたい方は以下をご覧ください。
https://imagemagick.org/script/quantize.php
convert dance.png +dither -colors 8 dance-colors8.png
元画像の主要な色が再現できていそうです。

k-means法を使って減色する

k-means法とは、k個のクラスタに平均を使ってクラスタリングするアルゴリズムです。
機械学習界隈でも登場するアルゴリズムの一つですので、興味のある方は知らべてみてください。
https://ja.wikipedia.org/wiki/K%E5%B9%B3%E5%9D%87%E6%B3%95

Imagemagickでのコマンドは「kmeans」を使います。
magick dance.png -kmeans 8 dance-kmeans8.png
これも元画像の主要な色が再現できていそうです。
「colors」コマンドと比較すると、顔色が良くなった(頬の色がより再現できている)ような…?

まとめ

Imagemagickを使った減色処理について紹介しました。
紹介しておいてなんなのですが、とあるプロジェクトでは減色処理にImagemagickは採用せず、独自実装する道を選びました。
Imagemagickは誰でも使えますし、独自実装することでより直感的な減色の結果が得られるなら、ユーザビリティーの面で差別化できますしね。