ディープラーニングを用いたオブジェクトの傾き補正

これはなに?

紙の本を裁断してスキャンしたデータを、綺麗に見やすく整える必要があり、そのための傾き補正について、まずは基本的な考えが間違っていないかをトイデータで検証してみた記録です。

やりたいこと

紙の本はわりとアバウトな作りになっていて、製本時から既に、紙面の長方形に対して、文面が傾いていたりします。本を読むときに、我々はこの傾きを無意識に補正していて、ほとんどの場合は気にならないようです。

しかし、その本をスキャンし、データ化したものを電子端末でいざ読もうとすると、この傾きが暴力的なまでに効いてきます。非常に気持ち悪く、意識が散漫となり、とても読み進めることができません。

幸いに半手動でこの傾きを補正するようなツール*1があり、これを使って手動で補正していたのですが、電子書籍の普及とともに、この作業の価値が低下し…、ようするに面倒くさくなったのです。AIにこの作業をこなしてもらえれば非常に助かります。

Fashion-MNIST

手元にはそこそこの量のデータがあり、これを使って学習することもできますが、いきなり大量のデータを使ってディープラーニングを行うのは無謀です。私が学習に使えるGPUはGTX 1070が一枚。それだけです。ほぼ徒手空拳であり、これでサクサク学習が進むデータセットといえばMNISTぐらいです。

MNISTといえば、28x28ピクセルの手書きの数字画像が70000枚(訓練60000枚, テスト10000枚)…なのですが、今回の用途には適しません。なぜかというと、手書き文字には個々人の癖が反映されていて、傾きという要素もその中に織り込まれているからです。

MNISTの例(うまくいくようには見えない)

f:id:ruby-U:20181003215622p:plain

そこでFashion-MNISTです。MNISTと同じフォーマットでありながら、衣服などからなる10クラスで構成されていて、素晴らしいことに、傾きが補正されています。

Fashion-MNISTの例(綺麗に補正されている)

f:id:ruby-U:20181003215629p:plain

これを使って、まずは本来のラベル(商品のジャンルからなる10クラス)を捨てて、359度・359クラス・1度単位の角度をラベルとして与えたデータを考えます。 このとき、画像はラベルの角度で回転操作をされています。つまり、オブジェクトの回転角度を推定するタスクです。

0-359度、ランダムに回転したオブジェクト

f:id:ruby-U:20181004231232p:plain

多層パーセプトロン

359度・359クラス・1度単位

まずは単純な多層パーセプトロンで試してみます。GTX 1070でも、5並列ぐらいでパラメータの違う学習を進めることができて捗ります。

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import backend as K

import numpy as np
import matplotlib.pyplot as plt

print("tf.version={0}".format(tf.__version__))

(train_images, train_labels), (test_images, test_labels) = keras.datasets.fashion_mnist.load_data()

num_classes = 359
classes = range(num_classes)

def generate_rotated_dataset(images):
    def _rotate(img, step):
        if step == 0: return img
        return rotate(img, angle=step, reshape=False, mode="constant", cval=0)
    new_images = []
    labels = []
    for img in images:
        label = np.random.choice(classes)
        new_images.append(_rotate(img, label))
        labels.append(label)
    return (np.array(new_images), np.array(labels))

train_images, train_labels = generate_rotated_dataset(train_images)
test_images, test_labels = generate_rotated_dataset(test_images)

train_images = train_images / 255.0
test_images = test_images / 255.0

from tensorflow.keras.layers import Flatten, Dense, Dropout, Activation
from tensorflow.keras.optimizers import Adam

def gpu_memory_lazy_allocate():
    config = tf.ConfigProto()
    config.gpu_options.allow_growth = True
    sess = tf.Session(config=config)
    K.set_session(sess)
gpu_memory_lazy_allocate()

def get_model():
    model = Sequential()
    model.add(Flatten(input_shape=(28, 28)))
    model.add(Dense(640))
    model.add(Activation('relu'))
    model.add(Dropout(0.2))
    model.add(Dense(640))
    model.add(Activation('relu'))
    model.add(Dropout(0.2))
    model.add(Dense(num_classes))
    model.add(Activation('softmax'))
    return model
model = get_model()

optimizer = tf.keras.optimizers.Adam(lr=0.0001)

model.compile(optimizer=optimizer, 
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(
    monitor='val_loss', 
    factor=0.5, 
    patience=20, 
    verbose=1, 
    mode='auto', 
    min_delta=0.0001, 
    cooldown=10, 
    min_lr=0)
        
history = model.fit_generator(
    x=train_images, 
    y=train_labels,
    validation_data=(test_images, test_labels),
    callbacks=[reduce_lr],
    epochs=100000)

…うまくいきません。

f:id:ruby-U:20181003134250p:plain

これは訓練データの角度が常に一定であるからだと考えて、以下のように動的にデータを生成するようにします。 それに加えて、オーグメンテーションとして、左右反転を入れています。

from tensorflow.keras.models import Sequential

batch_size = 32

def generate_rotated_dataset(images):
    def _rotate(img, step):
        if step == 0: return img
        return rotate(img, angle=step, reshape=False, mode="constant", cval=0)
    new_images = []
    labels = []
    for img in images:
        label = np.random.choice(classes)
        new_images.append(_rotate(img, label))
        labels.append(label)
    return np.array(new_images), np.array(labels)

class TrainImageSequence(Sequence):
    def __init__(self, images, batch_size=1):
        self.images = images
        self.batch_size = batch_size
    
    def on_epoch_end(self):
        np.random.shuffle(self.images)
        
    def _horizontal_flip(self, img):
        if np.random.choice([True, False]):
            return cv2.flip(img, 1)
        return img
    
    def __getitem__(self, idx):
        start = idx * self.batch_size
        end = start + self.batch_size
        batch_x = self.images[start:end]
        batch_x = [self._horizontal_flip(x) for x in batch_x]
        return generate_rotated_dataset(batch_x)

    def __len__(self):
        return math.ceil(len(self.images) / self.batch_size)

class TestImageSequence(Sequence):
    def __init__(self, images, batch_size=1):
        self.x, self.y = generate_rotated_dataset(images)
        self.batch_size = batch_size
    
    def __getitem__(self, idx):
        start = idx * self.batch_size
        end = start + self.batch_size
        batch_x = self.x[start:end]
        batch_y = self.y[start:end]
        return batch_x, batch_y

    def __len__(self):
        return math.ceil(len(self.x) / self.batch_size)

train_gen = TrainImageSequence(train_images, batch_size=batch_size)
test_gen = TestImageSequence(test_images, batch_size=batch_size)
history = model.fit_generator(
    generator=train_gen, 
    validation_data=test_gen, 
    callbacks=[reduce_lr],
    epochs=100000)

f:id:ruby-U:20181003134057p:plain

先頭の6件

f:id:ruby-U:20181003134107p:plain

うまくいったようです。ちょっと見にくいですが、左から推定角度、真の角度、確信度です。

15度・600クラス・0.025単位。

さて、次のステップです。本題に戻って考えると、スキャンされたドキュメントというものは、おおよそ方向が揃っています。359度を考える必要はなく、紙面がこのぐらいは傾いてるかもしれない、という部分で解像度を高めてみます。

15度・600クラス・0.025単位を試します。この値は特に意味のあるものではなく、フィーリングで選びました。私が見分けられる傾きが0.1度ぐらいなので、0.025単位でうまく精度が出せるなら、素晴らしいのではないでしょうか。

参考: 上から順に1, 0.5, 0.25, 0.1 の傾き。

f:id:ruby-U:20181003140628p:plain f:id:ruby-U:20181003140635p:plain f:id:ruby-U:20181003140633p:plain f:id:ruby-U:20181003140631p:plain

以下の設定でデータを生成します。モデルは前と同じです。なお、ここからは画像のサイズを縦横それぞれ2倍にしています。多少ですが、実際のシーンに近づけるためです。

batch_size = 32
num_classes = 600
image_shape = (56, 56)

classes = range(num_classes)
class_values = np.linspace(-7.5, 7.5, num_classes)

def generate_rotated_dataset(images):
    def _resize(img, size):
        return cv2.resize(img, size)
    def _rotate(img, step):
        if step == 0: return img
        return rotate(img, angle=step, reshape=False, mode="constant", cval=0)
    new_images = []
    labels = []
    for img in images:
        label = np.random.choice(classes)
        img = _resize(img, image_shape)
        img = _rotate(img, class_values[label])
        img = img.reshape(*image_shape, 1)
        new_images.append(img)
        labels.append(label)
    return np.array(new_images), np.array(labels)
Top 1: 0.0795
Top 2: 0.1577
Top 3: 0.2344
Top 4: 0.3077
Top 5: 0.3801
Mean error: 0.1690

TopNの精度がビシッと出ておらず、ピークがぼやけている感じです。

先頭の6件

f:id:ruby-U:20181004231755p:plain

このモデルだと、必ず600クラスのうちから候補を選ぶことになるので、わからないときはわからないと判断してもらったほうがよいかもしれません。 600クラスに、-7.5 ~ 7.5 以外の角度からランダムにチョイスした1クラスを追加してみます。

15度・600 + 1クラス・0.025単位。

batch_size = 32
num_classes = 600
image_shape = (56, 56)

classes = range(num_classes)
class_values = np.linspace(-7.5, 7.5, num_classes)

def generate_rotated_dataset(images):
    def _resize(img, size):
        return cv2.resize(img, size)
    def _rotate(img, step):
        if step == 0: return img
        return rotate(img, angle=step, reshape=False, mode="constant", cval=0)
    new_images = []
    labels = []
    for img in images:
        if np.random.uniform(0, 1) > 0.1:
            label = np.random.choice(classes)
            angle = class_values[label]
        else:
            # 範囲外のクラス
            label = num_classes
            angle = np.random.uniform(7.5 + K.epsilon(), 352.5 - K.epsilon())
        img = _resize(img, image_shape)
        img = _rotate(img, angle)
        img = img.reshape(*image_shape, 1)
        new_images.append(img)
        labels.append(label)
    return np.array(new_images), np.array(labels)
Top 1: 0.0915
Top 2: 0.1821
Top 3: 0.2681
Top 4: 0.3505
Top 5: 0.4240
Mean error: 0.1394
False positive: 70 (0.0078)
False negative: 24 (0.0240)

15度内の画像を誤って外れクラスだと識別する確率が0.0078あり、その場合はTopNの計算からは省いています。 若干ですが、TopNの精度と平均誤差が改善しました。

VGG

さて、GPU負荷が軽い多層パーセプトロンでおおよその目星がついたので、やや重いVGGモデルで精度を高めていきます。オリジナルのVGGにバッチノーマリゼーションを加えています。  

from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Flatten, Dense, Dropout, Activation, BatchNormalization
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.applications import VGG16

def gpu_memory_lazy_allocate():
    config = tf.ConfigProto()
    config.gpu_options.allow_growth = True
    sess = tf.Session(config=config)
    K.set_session(sess)
gpu_memory_lazy_allocate()

def get_model():
    mnist_input = Input(shape=(*image_shape, 1), name='mnist_input')
    vgg16 = VGG16(input_shape=(*image_shape, 1), weights=None, include_top=False)
    vgg16.summary()
    vgg16_output = vgg16(mnist_input)
    x = Flatten(name='flatten')(vgg16_output)
    x = Dropout(0.8)(x)
    x = Dense(1024, activation='relu', name='fc1')(x)
    x = BatchNormalization()(x)
    x = Dropout(0.8)(x)
    x = Dense(1024, activation='relu', name='fc2')(x)
    x = BatchNormalization()(x)
    x = Dropout(0.8)(x)
    x = Dense(num_classes + 1, activation='softmax', name='predictions')(x)
    return Model(inputs=mnist_input, outputs=x)
model = get_model()

optimizer = tf.keras.optimizers.Adam(lr=0.001)

model.compile(optimizer=optimizer, 
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(
    monitor='val_loss', 
    factor=0.5, 
    patience=20, 
    verbose=1, 
    mode='auto', 
    min_delta=0.0001, 
    min_lr=0)

history = model.fit_generator(
    generator=train_gen, 
    validation_data=test_gen, 
    max_queue_size=12*2, 
    workers=12, 
    use_multiprocessing=True,
    callbacks=[reduce_lr],
    epochs=100000)
Top 1: 0.4724
Top 2: 0.8104
Top 3: 0.9597
Top 4: 0.9906
Top 5: 0.9980
Mean error: 0.0148
False positive: 0 (0.0000)
False negative: 2 (0.1333)

途中で打ち切ったのですが、精度はやはり段違いです。 False-Positiveな要素がない(うまく識別できないものを外れクラスに追い出せていない)のが気になるので、この部分をロス関数で緩和してみます。

外れクラスへの誤答のペナルティを緩和したロス関数

def normalized_loss(y_true, y_pred):
    y_true_class = K.cast(y_true, dtype='float32')
    y_pred_class = K.cast(K.argmax(y_pred), dtype='float32')
    e = K.ones_like(y_true, dtype='float32') * num_classes
    a = K.equal(y_true_class, e) # y_true_class == error_class
    b = K.equal(y_pred_class, e) # y_pred_class == error_class
    a_eq_b = K.equal(a, b)
    a_ne_b = K.not_equal(a, b)
    
    case_0 = K.cast(a_eq_b, dtype='float32') # a == b
    case_1 = K.minimum(K.cast(a, dtype='float32'), K.cast(a_ne_b, dtype='float32')) # a AND (a != b)
    case_2 = K.minimum(K.cast(b, dtype='float32'), K.cast(a_ne_b, dtype='float32')) # b AND (a != b)
    
    w = 1 - y_pred[:, num_classes] * 0.2
    loss = K.sparse_categorical_crossentropy(y_true, y_pred)
    return loss * (case_0 + case_1 + case_2 * w)

model.compile(optimizer=optimizer, 
              loss=normalized_loss,
              metrics=['sparse_categorical_accuracy'])
Top 1: 0.7478
Top 2: 0.9483
Top 3: 0.9902
Top 4: 0.9972
Top 5: 0.9992
Mean error: 0.0066
False positive: 9 (0.0010)
False negative: 103 (0.1017)

誤差が0.1を超えたのは1個だけになりました。よさそうです。

先頭の6件

f:id:ruby-U:20181003145915p:plain

誤差の大きい順に6件

f:id:ruby-U:20181003145948p:plain

外れクラスだと誤識別された9件

f:id:ruby-U:20181003145953p:plain

一撃でウィンドウの内容をOCRする環境を構築した話

やったこと

もう大手各社のクラウドOCR APIの精度は十分実用的なレベルなので、単純にウィンドウの内容を投げればよい話なんだけれども、ウィンドウの内容全部をOCRしたかったり、あるいは矩形で選択したかったりするかもしれないので、スクリーンをキャプチャして、それら画像ファイルを生成するところまでは他のツールに任せることにして、「あるディレクトリ以下に画像ファイルが生成/移動されたら、それを自動でOCRする」作業をツールを書いて自動化しました。

成果物

github.com

インストール方法

zipファイルの中身を適当なところに解凍するだけです。

使い方

最小のコマンドは

cocr -d "PATH_TO_TARGET_DIRECTORY" -k "YOUR_GOOGLE_API_KEY"

-d で指定されたディレクトリを監視して、その中に画像ファイルが放り込まれたとき、自動で対応するjsonAPIを叩いたjsonレスポンスそのまま)、html(OCR結果を表示するHTMLファイル)を生成します。

-c オプションで結果をクリップボードに貼り付け。-s でhtmlファイルをブラウザで開く。-n で通知。他にもディレクトリ内の画像ファイルをまとめて処理する-b オプションなどがあります。

なお、使用しているGoogle Cloud Vision APIは月一定回数までは無料(2018/07 時点で最初の1000回は無料)ですが、-k オプションでのGoogle APIキーの指定は必須になります。APIキーを発行するときに、後で泣かないよう、適切な課金上限額を設定しておきましょう。

Authenticating to the Cloud Vision API  |  Cloud Vision API Documentation  |  Google Cloud

API キーの使用  |  ドキュメント  |  Google Cloud

イメージ

対象画像

f:id:ruby-U:20180713073812p:plain

クリップボード結果

f:id:ruby-U:20180713073741p:plain

HTMLを開いたところ

f:id:ruby-U:20180713074121p:plain

スクリーンキャプチャツールとの組み合わせ

SnapCrab www.fenrir-inc.com

GreenShot gigazine.net

などのツールの出力先を、cOCRの監視するディレクトリ以下に設定するだけです。あとはお好みに合わせて、スクリーンキャプチャツール側のキーバインドを設定すれば、「1ボタンで自動OCR、結果をクリップボードに保存」みたいなシステムが完成します。

PDFへの変換

cOCRは、Google Cloud Vision APIからのレスポンスをまるっとまるごとjsonファイルとして保存してるので、以下のツールとの組み合わせで、画像ファイルをOCRされたPDFに変換することも可能です。

github.com

github.com

Anki 2.1.0 (beta) で H.264 や MP3 の再生を可能にする方法

Anki のブラウザコンポーネントについて

まだ正式にリリースされていませんが、Anki 2.1.0 で Qt のバージョンが更新され、またブラウザ機能を提供するコンポーネントとして新しく QtWebEngine が導入されました。 Anki 2.1.0 beta42 では Qt, QtWebEngine のバージョンは 5.9.2 です。

Qt 5.9.2 Change Files - Qt Wiki

changes-5.9.2\dist - qt/qtwebengine.git - Qt WebEngine

 - Chromium Snapshot:
   * Security fixes from Chromium up to version 61.0.3163.79
    Including: CVE-2017-5092, CVE-2017-5093, CVE-2017-5095, CVE-2017-5097,
        CVE-2017-5099, CVE-2017-5102, CVE-2017-5103, CVE-2017-5107,
        CVE-2017-5112, CVE-2017-5114, CVE-2017-5117 and CVE-2017-5

とあるので、QtWebEngine 5.9.2 は Chromium 61 相当でしょうか。

一方で、Anki 2.0.x が使用しているブラウザコンポーネントは QtWebKit 2.2 で、これは 2011 年のリリース…。

QtWebKit 2.2.0 is released! - Qt Blog

QtWebKitFeatures22 – WebKit

QtWebKitRelease22 – WebKit

考えるのも嫌になるぐらい大昔の化石ですね。もう Anki 2.0.x のことは忘れてしまいましょう。

さて、Anki を最新のものにアップデートすれば、大抵のデッキはまず問題なくそのまま動作するのですが、以前から例えば Android 向けの実装である AnkiDroid では、より新しいブラウザコンポーネントが使用できていました。 極めて稀だとは思いますが、このような環境向けに、HTML5 Media API を使用する(Anki の [sound: xxx.mp3] タグではなく、JavaScriptでaudio/videoを直接操作している)デッキを作成し、運用している場合。または新しくこのようなデッキを導入する場合、最新のデスクトップ向け Anki でも、問題が生じるかもしれません。以下のような制限があるためです。

Qt WebEngine supports the MPEG-4 Part 14 (MP4) file format only if the required proprietary audio and video codecs, such as H.264 and MPEG layer-3 (MP3), have been enabled.

Qt WebEngine Features | Qt WebEngine 5.9

特定のメディア(H.264, MP3 など)について、ライセンス上の制限のため、デフォルトでは再生することができません。この問題を回避するには、-proprietary-codecs オプションを付加してコンパイルした QtWebEngine が必要です。

コンパイルしたものはこちら。商用利用にはライセンスが必要です

コンパイル

環境

>perl -v

This is perl 5, version 26, subversion 2 (v5.26.2) built for MSWin32-x64-multi-thread

~略~

>python --version
Python 2.7.13

ソース

http://download.qt.io/archive/qt/5.9/5.9.2/single/ から http://download.qt.io/archive/qt/5.9/5.9.2/single/qt-everywhere-opensource-src-5.9.2.tar.xz を拾ってきます。

依存ツール

手順

ソースを解凍後、生成された qtwebengine ディレクトリに、依存ツール(bison, flex, gperf)を放り込みます。 開発者コマンドプロンプト for VS2015 で qtwebengine を開き、-proprietary-codecs オプションを付けて qmake を実行します。結果は以下のようなものです。

C:\Qt\qt-everywhere-opensource-src-5.9.2\qtwebengine>"C:\Qt\5.9.2\msvc2015\bin\qmake.exe" -- -proprietary-codecs

Running configuration tests...
Done running configuration tests.

Configure summary:

Qt WebEngine:
  Embedded build ......................... no
  Pepper Plugins ......................... yes
  Printing and PDF ....................... yes
  Proprietary Codecs ..................... yes
  Spellchecker ........................... yes
  WebRTC ................................. yes
  Using system ninja ..................... no

Qt is now configured for building. Just run 'nmake'.
Once everything is built, Qt is installed.
You should NOT run 'nmake install'.
Note that this build cannot be deployed to other machines or devices.

Prior to reconfiguration, make sure you remove any leftovers from
the previous build.

Proprietary Codecs ..................... yes になっていることを確認してください。確認できたら nmake するだけです。数時間以上かかります。気長に待ちましょう。

Anki コンポーネントの上書き

C:\Program Files (x86)\AnkiQt5WebEngineCore.dll などがありますので、これらを成果物に置き換えます。

以上です。お疲れ様でした。

マウスジェスチャーツール Crevice 3.2.184 をリリースしました

カッコいいロゴを頂いたのでテンション上がってそのまま18時間コーディングしたらメジャーバージョンが1つ上がっていました。

f:id:ruby-U:20171223133526p:plain

どうですか、超クールなのでは?? イエーイ🎉🎉

Crevice 3.2.184 リリースノート

1月にリリースしたCrevice 2.5 から Crevice 3 へメジャーバージョンが1つ上がり、いくつかの大きな変更が加わっています。

  • 新しいクールなアイコン! アプリケーションの外観をブラッシュアップしました。

  • ユーザースクリプトのキャッシュ機構を追加。Crevice 2.5 と比較して起動速度が最大で50倍高速になりました。

  • コマンドラインインターフェイスの追加。ユーザースクリプトの場所、タスクトレイアイコン非表示、プロセス優先度、詳細なログ出力などの設定を引数として指定できるようになりました。

特に頑張ったところは起動速度の高速化です。CreviceではユーザースクリプトC# Scriptを使っていて、Crevice 2.5 ではRoslynで毎回コンパイルしていたのですが、このリソースが無駄だし起動時間は遅くなるしいいことなしだったので、ユーザースクリプトコンパイルした後のアセンブリをうまいことキャッシュできないかなーと試行錯誤してたら偶然うまく行きました。雰囲気でやっている。

技術資料

以下、できたてホヤホヤのソースから抜粋して、Roslynでユルい C# Script をコンパイルして、アセンブリからそれを評価する方法を書いておきます。アセンブリに GetType して、 GetMethod して Invoke という資料はよく見かけますが、 C# Script 特有のトップレベルに変数がくるようなコードでうまくそれをやる方法が見当たらなかったので貴重な資料だと思います(ホントか?)

Crevice 2 の実装(ユルいC# Script にglobalsを与えて評価する)

ユーザースクリプトから Script を作ります。

private Script ParseScript(string userScript)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Parsing UserScript...");
    stopwatch.Start();
    var script = CSharpScript.Create(
        userScript,
        ScriptOptions.Default
            .WithSourceResolver(ScriptSourceResolver.Default.WithBaseDirectory(UserDirectory))
            .WithMetadataResolver(ScriptMetadataResolver.Default.WithBaseDirectory(UserDirectory))
            .WithReferences("microlib")                   // microlib.dll
            .WithReferences("System")                     // System.dll
            .WithReferences("System.Core")                // System.Core.dll
            .WithReferences("Microsoft.CSharp")           // Microsoft.CSharp.dll
            .WithReferences(Assembly.GetEntryAssembly()), // CreviceApp.exe
                                                          // todo dynamic type
        globalsType: typeof(Core.UserScriptExecutionContext));
    stopwatch.Stop();
    Verbose.Print("UserScript parse finished. ({0})", stopwatch.Elapsed);
    return script;
}

そしてこの Script にコンテキスト ctx を globals として与えて評価します。ctxLeftButton, MiddleButton のような設定のためのトークン、 ジェスチャー定義に関する @when 関数や、Config のようなインスタンスTooltip, Balloon のようなユーティリティ関数などをメンバーとして持ち、それを globals として与えることで、ユーザースクリプト評価時のコンテキストとしています。

public class UserScriptExecutionContext
{
    public readonly DSL.Def.LeftButton   LeftButton   = DSL.Def.Constant.LeftButton;
    public readonly DSL.Def.MiddleButton MiddleButton = DSL.Def.Constant.MiddleButton;
// 略
    public Config.UserConfig Config
    {
      get { return Global.UserConfig; }
    }

    public DSL.WhenElement @when(DSL.Def.WhenFunc func)
    {
        return root.@when(func);
    }

    public void Tooltip(string text)
    {
        Tooltip(text, Global.UserConfig.UI.TooltipPositionBinding(WinAPI.Window.Window.GetPhysicalCursorPos()));
    }

    public void Balloon(string text)
    {
        Balloon(text, Global.UserConfig.UI.BalloonTimeout);
    }
// 略
}
private IEnumerable<Core.GestureDefinition> EvaluateUserScript(Script userScript, Core.UserScriptExecutionContext ctx)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Compiling UserScript...");
    stopwatch.Start();
    var diagnotstics = userScript.Compile();
    stopwatch.Stop();
    Verbose.Print("UserScript compilation finished. ({ 0})", stopwatch.Elapsed);
    foreach (var dg in diagnotstics.Select((v, i) => new { v, i }))
    {
        Verbose.Print("Diagnotstics[{0}]: {1}", dg.i, dg.v.ToString());
    }
    Verbose.Print("Evaluating UserScript...");
    stopwatch.Restart();
    userScript.RunAsync(ctx).Wait();
    stopwatch.Stop();
    Verbose.Print("UserScript evaluation finished. ({0})", stopwatch.Elapsed);
    return ctx.GetGestureDefinition();
}

評価後 ctx にはマウスジェスチャーの設定が保存されています。

Crevice 3 の実装(ユルい C# Script から生成したアセンブリをロードして globals を与えて評価する)

ユーザースクリプトから Script を作ります。

private Script ParseScript(string userScript)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Parsing UserScript...");
    stopwatch.Start();
    var script = CSharpScript.Create(
        userScript,
        ScriptOptions.Default
            .WithSourceResolver(ScriptSourceResolver.Default.WithBaseDirectory(UserDirectory))
            .WithMetadataResolver(ScriptMetadataResolver.Default.WithBaseDirectory(UserDirectory))
            .WithReferences("microlib")                   // microlib.dll
            .WithReferences("System")                     // System.dll
            .WithReferences("System.Core")                // System.Core.dll
            .WithReferences("Microsoft.CSharp")           // Microsoft.CSharp.dll
            .WithReferences(Assembly.GetEntryAssembly()), // CreviceApp.exe
                                                          // todo dynamic type
        globalsType: typeof(Core.UserScriptExecutionContext));
    stopwatch.Stop();
    Verbose.Print("UserScript parse finished. ({0})", stopwatch.Elapsed);
    return script;
}

ここまでは以前の実装と同じです。次にアセンブリのバイナリを作ります。次回起動時にアプリケーションのバージョンやユーザースクリプトに変更がなければ、これをキャッシュとして使うことで起動時間を大幅に短縮することができます。

private UserScriptAssembly.Cache CompileUserScript(UserScriptAssembly usa, string userScriptCode, Script userScript)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Compiling UserScript...");
    stopwatch.Start();
    var compilation = userScript.GetCompilation();
    stopwatch.Stop();
    Verbose.Print("UserScript compilation finished. ({0})", stopwatch.Elapsed);

    var peStream = new MemoryStream();
    var pdbStream = new MemoryStream();
    Verbose.Print("Genarating UserScriptAssembly...");
    stopwatch.Restart();
    compilation.Emit(peStream, pdbStream);
    stopwatch.Stop();
    Verbose.Print("UserScriptAssembly generation finished. ({0})", stopwatch.Elapsed);
    return usa.CreateCache(userScriptCode, peStream.GetBuffer(), pdbStream.GetBuffer());
}

キャッシュをロードして、以前と同様にコンテキスト ctx を globals として与えて評価します。アセンブリに GetType("Submission#0") して、 GetMethod("") して得られた関数を Invoke するのですが、この時に ctxnew object[] { new object[] { ctx, null } } のような形で2番めの引数として渡すのがポイントです。

private IEnumerable<Core.GestureDefinition> EvaluateUserScriptAssembly(UserScriptAssembly.Cache cache, Core.UserScriptExecutionContext ctx)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Loading UserScriptAssembly...");
    stopwatch.Start();
    var assembly = Assembly.Load(cache.pe, cache.pdb);
    stopwatch.Stop();
    Verbose.Print("UserScriptAssembly loading finished. ({0})", stopwatch.Elapsed);
    Verbose.Print("Evaluating UserScriptAssembly...");
    stopwatch.Restart();
    var type = assembly.GetType("Submission#0");
    var factory = type.GetMethod("<Factory>");
    var parameters = new object[] { new object[] { ctx, null } };
    var result = factory.Invoke(null, parameters);
    stopwatch.Stop();
    Verbose.Print("UserScriptAssembly evaluation finished. ({0})", stopwatch.Elapsed);
    return ctx.GetGestureDefinition();
}

評価後 ctx にはマウスジェスチャーの設定が保存されています。

仕組み的に Crevice 2 と Crevice 3 でのユーザースクリプトの実行は全く同様の結果になる…はずなのですが、いまいち確信が持てないので失敗時にはフォールバックするようには実装しています。

private IEnumerable<Core.GestureDefinition> GetGestureDef(Core.UserScriptExecutionContext ctx)
{
    var userScriptCode = GetUserScriptCode();
    var userScript = ParseScript(userScriptCode);
    if (!Global.CLIOption.NoCache)
    {
        try
        {
            var cache = GetUserScriptAssemblyCache(userScriptCode, userScript);
            return EvaluateUserScriptAssembly(cache, ctx);
        }
        catch (Exception ex)
        {
            Verbose.Print("Error occured when UserScript conpilation and save and restoration; fallback to the --nocache mode and continume");
            Verbose.Print(ex.ToString());
        }
    }
    return EvaluateUserScript(userScript, ctx);
}

うまく動かないよーという方がもしいらっしゃいましたらご連絡ください。

DisplayPortディスプレイには妖精が住んでいる(Windowsのディスプレイ構成をプロファイルごとに切り替える)

以前からWindowsのディスプレイ周りには困惑していて、特にDisplayPort(以下DP)が絡んだ場合には、DPの電源オフ時にはデバイス自体がOS上でオフ扱いになるという仕様からほんとにもうどうしようもない挙動をします。ディスプレイがオフ扱いになる、あるいは復帰するタイミング、またはデフォルトのモニター解像度の存在などが渾然一体となり、休憩から帰った後にウィンドウが元の位置にあることは決して保証されません。そのサイズすらもです。

この問題の対処法はいろいろあります。とりあえず簡単に紹介します。

DPディスプレイの電源を切らずに運用する

シンプルに因果律から導き出されるアイディアです。しばらく放っておくとDPディスプレイはスタンバイ状態に入り、この状態への移行、この状態からの復帰では特に問題は発生しません。 この解決策は良さそうに見えますが、席を立った後、あなたのディスプレイがまだ光を放っていることに気づいた親切な同僚の存在について考察してみましょう。

プログラムから即座にディスプレイをスタンバイ状態に移行する方法を以下に紹介します。あなたのディスプレイのインジケーターがスリープ時にモンスターたちを惹きつけないことを祈ります。

gist9167e2b52ab8033414fd70357079070e

モニターの解像度に関するレジストリ値を変更する

モニターを省電力から復帰させると解像度やウィンドウのサイズが小さくなるのを解決する : プログ 塵の雨日記: DisplayPort接続時のディスプレイのオンオフによるウィンドウの再配置について PC用4Kモニタでウインドウが小さく左上に偏る問題をレジストリ編集で解決

多くの方の直った!という声が寄せられていますが、この方法はマルチディスプレイ環境でウィンドウが別ウインドウに移動してしまう問題については効果がありません。

DPケーブルをハックする

DisplayPortの切断を回避するアダプター - Qiita DisplayPort接続でモニターの電源を切ると認識が外れるのを何とかできる? - Aquablews_apps.dump()

私はこの方法を試していませんが、コストと互換性問題について納得できれば試す価値はあるでしょう。

ウィンドウ位置を管理するツールを使う

あるディスプレイにウィンドウを集めるもの、ウィンドウの状態を保存・復元するものなどいろいろありますが、多くのツールが、異なるDPIを含むマルチディスプレイ環境で正常に動かず、ひとまず断念しました。いいツールがあったら教えてください。

ディスプレイ構成をプロファイルごとに切り替える

試行錯誤の後、最終的にこの方法にたどり着きました。要するに、悪いDPディスプレイは能動的に取り外して、DPディスプレイがオフ扱いになるタイミングを制御しようというものです。前述のレジストリ値を使う方法と組み合わせて、この問題を多少改善することができます。

1. Display Changer IIでWindowsのディスプレイ構成を保存・復元する

Display Changer II « 12noon

例えば以下のコマンドでProfile1.xmlにディスプレイの設定を保存し、復元することができます。

dc2.exe -create="Profile1.xml"
dc2.exe -configure="Profile1.xml"

このxmlファイルはその時点でのWindowsのディスプレイの設定を保存したものなので、グラフィックドライバの更新などでデバイス名などに大きな変更が加わった場合はその都度作り直す必要があります。

2. コマンドをタスクとして登録する

dc2.exe -configure="Profile1.xml" のようなコマンドを管理者権限のタスクとして、タスクスケジューラに登録しておきます。こうすることで、通常権限のAutoHotKeyアプリケーションからコマンドを実行できます。 ここでは例として、SwitchDisplayProfile1, SwitchDisplayProfile2, SwitchDisplayProfile3の3つのプロファイルを作成しました。

3. AutoHotKeyからディスプレイのプロファイルを切り替える

AutoHotKeyスクリプトには前述のDisplayStandBy.exeと管理者権限タスクSwitchDisplayProfile1, SwitchDisplayProfile2, SwitchDisplayProfile3を起動するフック、Ctrl+Shift+Alt+F9, Ctrl+Shift+Alt+F10, Ctrl+Shift+Alt+F11, Ctrl+Shift+Alt+F12を記述しました。

gist869012765c79d16d2604292c890c5f54

Amazonの過去の売れ筋商品データ(月間)を取得する

Amazonの商品コードを大量に集める必要があり、いろいろ方法を調べていると、次のようなものを見つけた。なかなか有用だと思うので取得方法を纏めておく。

過去の売れ筋商品データ(月間)

f:id:ruby-U:20171111085507p:plain

対象年月と商品カテゴリを選択してボタンを押せばデータへのリンクが表示される。

f:id:ruby-U:20171111085512p:plain

データのURLは固定されているようで、認証なども必要ない。好きなように取ってくればよさそうだ。

中身は普通のTSVで、1行のヘッダあり。なお、これは2011-01の本カテゴリのデータ。懐かしい…。 f:id:ruby-U:20171111090205p:plain

2011-01から2017-09までのデータを集めたものをダウンロードできるようにしておくので、面倒な人はこちら(Google Drive)

なお、注意点として、カメラカテゴリの他にも一部データに抜けがある。DVDカテゴリの2011-11, 2011-12など。恐らく元データが存在しない。

以下のスクリプトで作成した。

gist.github.com

GPUが使えて仮想環境を認識するJupyter環境のDockerfileを書いた

Jupyterが立ち上げるだけでプログラミング環境ができあがる各種の便利なDockerイメージを出してくれているが、GPUが使えない。ので、無理やり使えるようにしてみた。 Dockerfileの継承元のFROMを書き換える必要があったので強引な感じになった。 以下ではcudaのDockerイメージを元にしているが、jupyterを元にcudaをインストールしたほうが完了までの時間が短いはず。

なお、TensorflowがGPUの使えるJupyter入りのDockerイメージをリリースしてくれている。なので、Jupyter本家に拘らなければこんなことをする必要はほぼない…が、まあせっかく書いたので。

適当に継ぎ接ぎしてDockerfileを作る

mkdir ~/Documents/cuda-based-jupyter-datascience-notebook
cd ~/Documents/cuda-based-jupyter-datascience-notebook

wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/base-notebook/Dockerfile -O base-notebook
wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/minimal-notebook/Dockerfile -O minimal-notebook
wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/scipy-notebook/Dockerfile -O scipy-notebook
wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/datascience-notebook/Dockerfile -O datascience-notebook

cat << "EOF" > Dockerfile
# Merging nvidia/cuda and jupyter/datascience-notebook DockerFiles.
FROM nvidia/cuda:8.0-cudnn6-devel-ubuntu16.04

MAINTAINER rubyu

EOF
cat << "EOF" >> Dockerfile

# jupter/base-notebook
EOF
sed -e '1,8 s/^/# /g' base-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

# jupter/minimal-notebook
EOF
sed -e '1,6 s/^/# /g' minimal-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

# jupter/scipy-notebook
EOF
sed -e '1,5 s/^/# /g' scipy-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

# jupter/datascience-notebook
EOF
sed -e '1,5 s/^/# /g' datascience-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

USER $NB_USER

RUN jupyter notebook --generate-config && \
    jupyter serverextension enable --py jupyterlab --sys-prefix && \
    pip install environment_kernels && \
    echo "c.NotebookApp.kernel_spec_manager_class = 'environment_kernels.EnvironmentKernelSpecManager'" >> ~/.jupyter/jupyter_notebook_config.py
EOF

最後のRUNの部分がJupyter Labとconda createで作る仮想環境のための設定。

Dockerイメージ作成に必要なファイルをダウンロードしておく

wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/fix-permissions -O fix-permissions
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/jupyter_notebook_config.py -O jupyter_notebook_config.py
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/start-notebook.sh -O start-notebook.sh
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/start-singleuser.sh -O start-singleuser.sh
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/start.sh -O start.sh
sudo chmod ugo+x fix-permissions
sudo chmod ugo+x jupyter_notebook_config.py
sudo chmod ugo+x start-notebook.sh
sudo chmod ugo+x start-singleuser.sh
sudo chmod ugo+x start.sh

実行できる権限がないとダメ。ここではwgetしているが、普通はgit cloneするのでハマることはないはず。

コンテナを立ち上げる

sudo nvidia-docker build -t rubyu/datascience-notebook-gpu:1.0 .

sudo nvidia-docker run -d --name notebook-gpu -p 8888:8888 --restart=always -v /storage/samba:/home/jovyan rubyu/datascience-notebook-gpu:1.0 start.sh jupyter lab --NotebookApp.token='XXXX'

これで8888ポートでJupyter Labが起動している。

本題には関係ない話ですが、Jupyterのhome(/home/jovyan)をまるごとホストのディレクトリに割り当てて、かつそれをSamba経由でアクセスできるようにするのが好きです。Windowsで処理する必要がある時にいちいち転送する必要がなくなるので。なんやかんやでGUIWindowsが使いやすい(個人の感想です)

アタッチしてconda createで仮想環境を作る

docker exec -it notebook-gpu /bin/bash

conda create -n your-new-envronment python=3.5 jupyter tensorflow-gpu && conda clean -tipsy

作成された仮想環境はJupyterから見えて、カーネルとして選択することができる。

Dockerfile

# Merging nvidia/cuda and jupyter/datascience-notebook DockerFiles.
FROM nvidia/cuda:8.0-cudnn6-devel-ubuntu16.04

MAINTAINER rubyu


# jupter/base-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# 
# # Ubuntu 16.04 (xenial) from 2017-07-23
# # https://github.com/docker-library/official-images/commit/0ea9b38b835ffb656c497783321632ec7f87b60c
# FROM ubuntu@sha256:84c334414e2bfdcae99509a6add166bbb4fa4041dc3fa6af08046a66fed3005f
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# Install all OS dependencies for notebook server that starts but lacks all
# features (e.g., download as all possible file formats)
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get -yq dist-upgrade \
 && apt-get install -yq --no-install-recommends \
    wget \
    bzip2 \
    ca-certificates \
    sudo \
    locales \
    fonts-liberation \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
    locale-gen

# Install Tini
RUN wget --quiet https://github.com/krallin/tini/releases/download/v0.10.0/tini && \
    echo "1361527f39190a7338a0b434bd8c88ff7233ce7b9a4876f3315c22fce7eca1b0 *tini" | sha256sum -c - && \
    mv tini /usr/local/bin/tini && \
    chmod +x /usr/local/bin/tini

# Configure environment
ENV CONDA_DIR=/opt/conda \
    SHELL=/bin/bash \
    NB_USER=jovyan \
    NB_UID=1000 \
    NB_GID=100 \
    LC_ALL=en_US.UTF-8 \
    LANG=en_US.UTF-8 \
    LANGUAGE=en_US.UTF-8
ENV PATH=$CONDA_DIR/bin:$PATH \
    HOME=/home/$NB_USER

ADD fix-permissions /usr/local/bin/fix-permissions
# Create jovyan user with UID=1000 and in the 'users' group
# and make sure these dirs are writable by the `users` group.
RUN useradd -m -s /bin/bash -N -u $NB_UID $NB_USER && \
    mkdir -p $CONDA_DIR && \
    chown $NB_USER:$NB_GID $CONDA_DIR && \
    fix-permissions $HOME && \
    fix-permissions $CONDA_DIR

USER $NB_USER

# Setup work directory for backward-compatibility
RUN mkdir /home/$NB_USER/work && \
    fix-permissions /home/$NB_USER

# Install conda as jovyan and check the md5 sum provided on the download site
ENV MINICONDA_VERSION 4.3.21
RUN cd /tmp && \
    wget --quiet https://repo.continuum.io/miniconda/Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh && \
    echo "c1c15d3baba15bf50293ae963abef853 *Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh" | md5sum -c - && \
    /bin/bash Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh -f -b -p $CONDA_DIR && \
    rm Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh && \
    $CONDA_DIR/bin/conda config --system --prepend channels conda-forge && \
    $CONDA_DIR/bin/conda config --system --set auto_update_conda false && \
    $CONDA_DIR/bin/conda config --system --set show_channel_urls true && \
    $CONDA_DIR/bin/conda update --all --quiet --yes && \
    conda clean -tipsy && \
    fix-permissions $CONDA_DIR

# Install Jupyter Notebook and Hub
RUN conda install --quiet --yes \
    'notebook=5.2.*' \
    'jupyterhub=0.8.*' \
    'jupyterlab=0.28.*' \
    && conda clean -tipsy && \
    fix-permissions $CONDA_DIR

USER root

EXPOSE 8888
WORKDIR $HOME

# Configure container startup
ENTRYPOINT ["tini", "--"]
CMD ["start-notebook.sh"]

# Add local files as late as possible to avoid cache busting
COPY start.sh /usr/local/bin/
COPY start-notebook.sh /usr/local/bin/
COPY start-singleuser.sh /usr/local/bin/
COPY jupyter_notebook_config.py /etc/jupyter/
RUN fix-permissions /etc/jupyter/

# Switch back to jovyan to avoid accidental container runs as root
USER $NB_USER

# jupter/minimal-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# 
# FROM jupyter/base-notebook
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# Install all OS dependencies for fully functional notebook server
RUN apt-get update && apt-get install -yq --no-install-recommends \
    build-essential \
    emacs \
    git \
    inkscape \
    jed \
    libsm6 \
    libxext-dev \
    libxrender1 \
    lmodern \
    pandoc \
    python-dev \
    texlive-fonts-extra \
    texlive-fonts-recommended \
    texlive-generic-recommended \
    texlive-latex-base \
    texlive-latex-extra \
    texlive-xetex \
    vim \
    unzip \
    && apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Switch back to jovyan to avoid accidental container runs as root
USER $NB_USER

# jupter/scipy-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# FROM jupyter/minimal-notebook
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# libav-tools for matplotlib anim
RUN apt-get update && \
    apt-get install -y --no-install-recommends libav-tools && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

USER $NB_USER

# Install Python 3 packages
# Remove pyqt and qt pulled in for matplotlib since we're only ever going to
# use notebook-friendly backends in these images
RUN conda install --quiet --yes \
    'nomkl' \
    'ipywidgets=7.0*' \
    'pandas=0.19*' \
    'numexpr=2.6*' \
    'matplotlib=2.0*' \
    'scipy=0.19*' \
    'seaborn=0.7*' \
    'scikit-learn=0.18*' \
    'scikit-image=0.12*' \
    'sympy=1.0*' \
    'cython=0.25*' \
    'patsy=0.4*' \
    'statsmodels=0.8*' \
    'cloudpickle=0.2*' \
    'dill=0.2*' \
    'numba=0.31*' \
    'bokeh=0.12*' \
    'sqlalchemy=1.1*' \
    'hdf5=1.8.17' \
    'h5py=2.6*' \
    'vincent=0.4.*' \
    'beautifulsoup4=4.5.*' \
    'protobuf=3.*' \
    'xlrd'  && \
    conda remove --quiet --yes --force qt pyqt && \
    conda clean -tipsy && \
    # Activate ipywidgets extension in the environment that runs the notebook server
    jupyter nbextension enable --py widgetsnbextension --sys-prefix && \
    fix-permissions $CONDA_DIR

# Install facets which does not have a pip or conda package at the moment
RUN cd /tmp && \
    git clone https://github.com/PAIR-code/facets.git && \
    cd facets && \
    jupyter nbextension install facets-dist/ --sys-prefix && \
    rm -rf facets && \
    fix-permissions $CONDA_DIR

# Import matplotlib the first time to build the font cache.
ENV XDG_CACHE_HOME /home/$NB_USER/.cache/
RUN MPLBACKEND=Agg python -c "import matplotlib.pyplot" && \
    fix-permissions /home/$NB_USER

USER $NB_USER

# jupter/datascience-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# FROM jupyter/scipy-notebook
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# R pre-requisites
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    fonts-dejavu \
    gfortran \
    gcc && apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Julia dependencies
# install Julia packages in /opt/julia instead of $HOME
ENV JULIA_PKGDIR=/opt/julia

RUN . /etc/os-release && \
    echo "deb http://ppa.launchpad.net/staticfloat/juliareleases/ubuntu $VERSION_CODENAME main" > /etc/apt/sources.list.d/julia.list && \
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3D3D3ACC && \
    apt-get update && \
    apt-get install -y --no-install-recommends \
    julia && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    # Show Julia where conda libraries are \
    echo "push!(Libdl.DL_LOAD_PATH, \"$CONDA_DIR/lib\")" >> /usr/etc/julia/juliarc.jl && \
    # Create JULIA_PKGDIR \
    mkdir $JULIA_PKGDIR && \
    chown $NB_USER $JULIA_PKGDIR && \
    fix-permissions $JULIA_PKGDIR

USER $NB_USER

# R packages including IRKernel which gets installed globally.
RUN conda config --system --add channels r && \
    conda install --quiet --yes \
    'rpy2=2.8*' \
    'r-base=3.3.2' \
    'r-irkernel=0.7*' \
    'r-plyr=1.8*' \
    'r-devtools=1.12*' \
    'r-tidyverse=1.0*' \
    'r-shiny=0.14*' \
    'r-rmarkdown=1.2*' \
    'r-forecast=7.3*' \
    'r-rsqlite=1.1*' \
    'r-reshape2=1.4*' \
    'r-nycflights13=0.2*' \
    'r-caret=6.0*' \
    'r-rcurl=1.95*' \
    'r-crayon=1.3*' \
    'r-randomforest=4.6*' && \
    conda clean -tipsy && \
    fix-permissions $CONDA_DIR

# Add Julia packages
# Install IJulia as jovyan and then move the kernelspec out
# to the system share location. Avoids problems with runtime UID change not
# taking effect properly on the .local folder in the jovyan home dir.
RUN julia -e 'Pkg.init()' && \
    julia -e 'Pkg.update()' && \
    julia -e 'Pkg.add("HDF5")' && \
    julia -e 'Pkg.add("Gadfly")' && \
    julia -e 'Pkg.add("RDatasets")' && \
    julia -e 'Pkg.add("IJulia")' && \
    # Precompile Julia packages \
    julia -e 'using HDF5' && \
    julia -e 'using Gadfly' && \
    julia -e 'using RDatasets' && \
    julia -e 'using IJulia' && \
    # move kernelspec out of home \
    mv $HOME/.local/share/jupyter/kernels/julia* $CONDA_DIR/share/jupyter/kernels/ && \
    chmod -R go+rx $CONDA_DIR/share/jupyter && \
    rm -rf $HOME/.local && \
    fix-permissions $JULIA_PKGDIR $CONDA_DIR/share/jupyter


USER $NB_USER

RUN jupyter notebook --generate-config && \
    jupyter serverextension enable --py jupyterlab --sys-prefix && \
    pip install environment_kernels && \
    echo "c.NotebookApp.kernel_spec_manager_class = 'environment_kernels.EnvironmentKernelSpecManager'" >> ~/.jupyter/jupyter_notebook_config.py