物理のバス停 by salt22g

とある物理学見習いの備忘録。

自前のMask画像からCOCO format jsonを作成

手作業でAnnotationなんてやってられるか!!!

ということで、画像処理でcoco formatのjsonを作るscriptを書きました。
簡易的なのでぜひ改造して使ってください。

ただしMask情報が二値化画像で取得できている前提です。
そもそも二値化できるなら物体検出いらないというツッコミはさておき…

f:id:salt22g:20201220204846p:plain
概念図

Mask R-CNN

機械学習において最も注目されている分野の一つが物体検出です。
自動運転や監視カメラなど、画像・映像情報から注目したい物体を抽出する技術で直感的かつ応用の幅が広い技術です。

その中でも有名かつ、開発が盛んなのがMask R-CNNと呼ばれる手法です。
論文はこちら
https://arxiv.org/abs/1703.06870

こんな感じで画像中から物体を検出し、それぞれがなんであるかを判断しています。

f:id:salt22g:20201220194651p:plain
https://github.com/matterport/Mask_RCNN/blob/master/assets/street.png

開発の歴史はこちらの記事によくまとまっています。
qiita.com

非常に優れた手法ですが、最初からこんな結果が得られるわけではなく、学習が必要です。
機械学習modelは正解の情報を持っている教師データを元に、最適な内部の重みを決定しています。
(機械学習について詳しく知りたい人はこちら。私も超初心者で勉強中です。)

ゼロから作るDeep LearningPythonで学ぶディープラーニングの理論と実装 斎藤 康毅
https://www.amazon.co.jp/dp/4873117585/ref=cm_sw_r_tw_dp_x_42Y3Fb1GA2SF8

機械学習スタートアップシリーズ これならわかる深層学習入門 (KS情報科学専門書) 瀧 雅人
https://www.amazon.co.jp/dp/4061538284/ref=cm_sw_r_tw_dp_x_e4Y3Fb8MTHZK4


先程の画像のように様々な物体を精度よく検出するためには大量の画像とそれぞれに正解の情報が必要です。
そこでデータセットとして学習および性能検証に用いられるベンチマークがCOCO datasetと呼ばれる巨大なdatasetです。

COCO dataset

cocodataset.org

"Common Objects in Context" 略してCOCO。
下の画像で示すように様々な画像と物体、それぞれが何であるか記述された正解情報をもっています。

f:id:salt22g:20201220200350p:plain
https://cocodataset.org/images/coco-examples.jpg

正解情報を持つjsonはこんな感じ。
https://docs.trainingdata.io/v1.0/Export%20Format/COCO/

{
  "info" : info, 
  "images" : [image], 
  "annotations" : [annotation], 
  "licenses" : [license],
}

info: {
  "year" : int, 
  "version" : str, 
  "description" : str, 
  "contributor" : str, 
  "url" : str, 
  "date_created" : datetime,
}

image: {
  "id" : int, 
  "width" : int, 
  "height" : int, 
  "file_name" : str, 
  "license" : int, 
  "flickr_url" : str, 
  "coco_url" : str, 
  "date_captured" : datetime,
}

license: {
  "id" : int, 
  "name" : str, 
  "url" : str,
}

このデータセットが基準となるformatをもっていることによって異なるmodelでも性能が比較できるようになっています。
またjsonのformatを揃えることで様々なデータを入れ替えても同じプログラム上で動くようになっています。

しかし、実際自分で使う際には専用の学習データを使って目的に特化したmodelを作成したいものです。
そのためには画像の中の正解情報を取得するAnnotation作業が必須になります。

Annotation作業

dev.classmethod.jp

f:id:salt22g:20201220201003p:plain
https://github.com/jsbroks/coco-annotator

めちゃくちゃめんどくさそう…

数枚ならまだしも学習データは数千枚になる場合が多いので手作業でぽちぽちしていると寿命が尽きてしまいます。
複雑な物体に対しては人間の手が必要ですが、簡易的な画像に対しては楽ができるはずです。

画像処理によるcoco format jsonの生成

用意した画像はこちらからダウンロードしました。
https://www.cis.upenn.edu/~jshi/ped_html/PennFudanPed.zip

簡単のため人がひとりしか写っていない画像だけをえらんでいます。
もちろん、複数の場合でもちょっとした改造を加えれば処理が可能です。

f:id:salt22g:20201220203804p:plain
左が入力画像、右が正解のMask情報の画像

Imageとmaskは同じ名前にしています。

  ┠ images
  ┃  ┠ 00001.png
  ┃  ┠ 00002.png
  ┃  ┠ 00003.png
  ┃  ┠ 以下略
  ┠ masks
  ┃  ┠ 00001.png
  ┃  ┠ 00002.png
  ┃  ┠ 00003.png
  ┃  ┠ 以下略
import json
import collections as cl
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage
from skimage import measure
from skimage.segmentation import clear_border
from skimage.filters import threshold_otsu
import cv2
import glob
import sys
import os

### https://qiita.com/harmegiddo/items/da131ae5bcddbbbde41f

def info():
    tmp = cl.OrderedDict()
    tmp["description"] = "Test"
    tmp["url"] = "https://test"
    tmp["version"] = "0.01"
    tmp["year"] = 2020
    tmp["contributor"] = "salt22g"
    tmp["data_created"] = "2020/12/20"
    return tmp

def licenses():
    tmp = cl.OrderedDict()
    tmp["id"] = 1
    tmp["url"] = "dummy_words"
    tmp["name"] = "salt22g"
    return tmp

def images(mask_path):
    tmps = []
    files = glob.glob(mask_path + "/*.png")
    files.sort()

    for i, file in enumerate(files):
        img = cv2.imread(file, 0)
        height, width = img.shape[:3]

        tmp = cl.OrderedDict()
        tmp["license"] = 1
        tmp["id"] = i
        tmp["file_name"] = os.path.basename(file)
        tmp["width"] = width
        tmp["height"] = height
        tmp["date_captured"] = ""
        tmp["coco_url"] = "dummy_words"
        tmp["flickr_url"] = "dummy_words"
        tmps.append(tmp)
    return tmps


def annotations(mask_path):
    tmps = []

    files = glob.glob(mask_path + "/*.png")
    files.sort()
    
    for i, file in enumerate(files):
        img = cv2.imread(file, 0)
        tmp = cl.OrderedDict()
        contours = measure.find_contours(img, 0.5)
        segmentation_list = []

        for contour in contours:
            for a in contour:
                segmentation_list.append(a[0])
                segmentation_list.append(a[1])


        mask = np.array(img)
        obj_ids = np.unique(mask)
        obj_ids = obj_ids[1:]
        masks = mask == obj_ids[:, None, None]
        num_objs = len(obj_ids)
        boxes = []

        for j in range(num_objs):
            pos = np.where(masks[j])
            xmin = np.min(pos[1])
            xmax = np.max(pos[1])
            ymin = np.min(pos[0])
            ymax = np.max(pos[0])
            boxes.append([xmin, ymin, xmax, ymax])

        tmp_segmentation = cl.OrderedDict()

        tmp["segmentation"] = [segmentation_list]
        tmp["id"] = str(i)
        tmp["image_id"] = i
        tmp["category_id"] = 1
        tmp["area"] = float(boxes[0][3] - boxes[0][1]) * float(boxes[0][2] - boxes[0][0])
        tmp["iscrowd"] = 0
        tmp["bbox"] =  [float(boxes[0][0]), float(boxes[0][1]), float(boxes[0][3] - boxes[0][1]), float(boxes[0][2] - boxes[0][0])]
        tmps.append(tmp)
    return tmps

def categories():
    tmps = []
    sup = ["person"]
    cat = ["person"]
    for i in range(len(sup)):
        tmp = cl.OrderedDict()
        tmp["id"] = i+1
        tmp["name"] = cat[i]
        tmp["supercategory"] = sup[i]
        tmps.append(tmp)
    return tmps

def main(mask_path, json_name):
    query_list = ["info", "licenses", "images", "annotations", "categories", "segment_info"]
    js = cl.OrderedDict()
    for i in range(len(query_list)):
        tmp = ""
        # Info
        if query_list[i] == "info":
            tmp = info()

        # licenses
        elif query_list[i] == "licenses":
            tmp = licenses()

        elif query_list[i] == "images":
            tmp = images(mask_path)

        elif query_list[i] == "annotations":
            tmp = annotations(mask_path)

        elif query_list[i] == "categories":
            tmp = categories()

        # save it
        js[query_list[i]] = tmp

    # write
    fw = open(json_name,'w')
    json.dump(js,fw,indent=2)

args = sys.argv
mask_path = args[1]
#mask_path =  ""
json_name = args[2]
#json_name = "person_sample.json"

if __name__=='__main__':
    main(mask_path, json_name)


こちらの記事を参考にさせていただきました。ありがとうございました

qiita.com

www.slideshare.net

肝となるのはこの部分です。

contours = measure.find_contours(img, 0.5)

find_contoursは画像中の物体の輪郭のpix座標を取得してくれます。

f:id:salt22g:20201220202843p:plain
https://sabopy.com/py/scikit-image-72/

また、bboxについては

pos = np.where(masks[j])

この部分で取得したmask情報を持つpixの最大値最小値を使っています。
Mask R-CNNのdemo codeを参考にしています。
pytorch.org

取得したjson fileはこちら。

{
  "info": {
    "description": "Test",
    "url": "https://test",
    "version": "0.01",
    "year": 2020,
    "contributor": "salt22g",
    "data_created": "2020/12/20"
  },
  "licenses": {
    "id": 1,
    "url": "dummy_words",
    "name": "salt22g"
  },
  "images": [
    {
      "license": 1,
      "id": 0,
      "file_name": "FudanPed00011.png",
      "width": 459,
      "height": 420,
      "date_captured": "",
      "coco_url": "dummy_words",
      "flickr_url": "dummy_words"

   #〜中略〜#

  ],
    "annotations": [
      {
        "segmentation": [
          [
            48.0,
            129.00229357798165,
            47.96153846153846,
            129.0,
            48.0,
            128.9878048780488,
            48.016129032258064,
            129.0,
            48.0,

   #〜中略〜#

      "id": "0",
      "image_id": 0,
      "category_id": 1,
      "area": 62868.0,
      "iscrowd": 0,
      "bbox": [
        36.0,
        55.0,
        156.0,
        403.0
      ]
    },

   #〜中略〜#

  ],
  "categories": [
    {
      "id": 1,
      "name": "person",
      "supercategory": "person"
    }
  ],
  "segment_info": ""
}

元の画像とこのjson fileがあればほとんどのCOCOを基準に開発されている物体検出が動くはず。
ではまた。

sshで急にサーバーに繋げなくなったとき

サーバーがメンテナンス等を終えたあと再びsshで接続しようとするとこんなエラーにでくわすことがあります。

% ssh hogehoge
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@       WARNING: POSSIBLE DNS SPOOFING DETECTED!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
The RSA host key for hogehoge has changed,
and the key for the corresponding IP address hogehoge
is unknown. This could either mean that
DNS SPOOFING is happening or the IP address for the host
and its host key have changed at the same time.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:hogehogehogehogehogehogehogehogehogehoge.
Please contact your system administrator.
Add correct host key in /Users/hoge/.ssh/known_hosts to get rid of this message.
Offending RSA key in /Users/hoge/.ssh/known_hosts:7
RSA host key for hogehoge has changed and you have requested strict checking.
Host key verification failed.
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
"誰かが厄介なことをしてるかもな!"
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
"誰かが盗聴してるかもな!(超意訳)"

かなりビビりますが、注目するのはこの部分。

Offending RSA key in /Users/hoge/.ssh/known_hosts:7

ホームディレクトリ以下にある隠しファイル.ssh/known_hostsを開いて該当の行を削除。
するとsshで接続できるようになりました。
めでたしめでたし。

二つのファイルに保存されている画像を合成する

python version

import numpy as np
import cv2
import os
import glob
import sys
import re

args = sys.argv
filename1 = args[1]
filename2 = args[2]
outname = args[3]


if __name__ == "__main__":


    new_dir = outname + "/"
    os.makedirs(new_dir, exist_ok=True)

    files1 = glob.glob(filename1 + "/*.png")
    files1.sort()

    files2 = glob.glob(filename2 + "/*.png")
    files2.sort()

    i = 0
    for file1, file2 in zip (files1, files2):
        img1 = cv2.imread(file1)
        img2 = cv2.imread(file2)
        merge_img = cv2.add(img1, img2)
        cv2.imwrite(new_dir + "{:08}.png".format(i), merge_img)
        i += 1

C++ vesion

前半部分はこの方のコードを拝借。
Python built-in zip() method implemented in C++ · GitHub

#include "opencv2/opencv.hpp"

#include "TStyle.h"
#include "TFile.h"
#include "TTree.h"

#include "TMath.h"
#include "TRandom.h"

#include <stdio.h>
#include <stdlib.h>
#include <string>

#include <iostream>
#include <fstream>
#include <sstream>
#include <math.h>
#include <random>
#include <vector>

#include <sys/stat.h>
#include <glob.h>


template<typename T>
class Zip
{
    
public:
    
  typedef std::vector<T> container_t;
    
  template<typename... Args>
  Zip(const T& head, const Args&... args)
    : items_(head.size()),
      min_(head.size())
  {
    zip_(head, args...);
  }
    
  inline operator container_t() const
  {
    return items_;
  }
    
  inline container_t operator()() const
  {
    return items_;
  }
    
private:
    
  template<typename... Args>
  void zip_(const T& head, const Args&... tail)
  {
    // If the current item's size is less than
    // the one stored in min_, reset the min_
    // variable to the item's size
    if (head.size() < min_) min_ = head.size();
        
    for (std::size_t i = 0; i < min_; ++i)
      {
	// Use begin iterator and advance it instead
	// of using the subscript operator adds support
	// for lists. std::advance has constant complexity
	// for STL containers whose iterators are
	// RandomAccessIterators (e.g. vector or deque)
	typename T::const_iterator itr = head.begin();
            
	std::advance(itr, i);
            
	items_[i].push_back(*itr);
      }
        
    // Recursive call to zip_(T, Args...)
    // while the expansion of tail... is not empty
    // else calls the overload with no parameters
    return zip_(tail...);
  }

  inline void zip_()
  {
    // If min_ has shrunk since the
    // constructor call
    items_.resize(min_);
  }

  /*! Holds the items for iterating. */
  container_t items_;
    
  /*! The minimum number of values held by all items */
  std::size_t min_;
    
};

template<typename T, typename... Args>
typename Zip<T>::container_t zip(const T& head, const Args&... tail)
{
  return Zip<T>(head, tail...);
}


std::vector<std::string> get_file_path(std::string input_dir, std::string wildcard){
  glob_t globbuf;
  std::vector<std::string> files;
  glob((input_dir + wildcard).c_str(), 0, NULL, &globbuf);
  for (int i = 0; i < globbuf.gl_pathc; i++){
    files.push_back(globbuf.gl_pathv[i]);
  }
  globfree(&globbuf);
  return files;
}


int main(int argc, char *argv[]){

  std::string input_dir1 = argv[1];
  std::string input_dir2 = argv[2];
  std::string output_dir = argv[3];
  
  std::string png = "/*.png";

  mkdir(Form("result_image/%s", output_dir.c_str()), S_IRWXU | S_IRWXG | S_IRWXO);

  std::vector<std::string> events1 = get_file_path(input_dir1, png);
  std::vector<std::string> events2 = get_file_path(input_dir2, png);
  std::sort(events1.begin(), events1.end());
  std::sort(events2.begin(), events2.end());

  std::string f;
  int i = 0;

  for (auto j : zip(events1, events2)){
    
    std::cout << j[0] << std::endl;
    std::cout << j[1] << std::endl;
    cv::Mat img1 = cv::imread(j[0], cv::IMREAD_COLOR);
    cv::Mat img2 = cv::imread(j[1], cv::IMREAD_COLOR);
    
    cv::Mat out_img;
    cv::add(img1, img2, out_img);
    
    cv::imwrite(Form("result_image/%s/%08d.png",output_dir.c_str(), i), out_img);
    i += 1;
  }
}

素粒子原子核物理解析用 Docker image

Docker imageを作成しました。Dockerhubでも公開しています。
f:id:salt22g:20200513060910p:plain
https://hub.docker.com/repository/docker/salt22g/docker-rpg

こちらの記事でも書きましたが、環境構築というのはとても面倒で、パッケージのversionが変わると不具合があったり、何度繰り返しても毎回違うところでつまずくことがあったりします。そこで原子核物理理系大学院生である私が普段使っている解析ツールをDocker imageにしてまとめてみました。その名もdocker-rpg(名前がダサい…??)

想定している使い方

  • 中央計算機、サーバーなどでの使用(Linux)
  • ローカルでの開発環境テスト
  • 開発環境の共有(後輩へのプログラミング講習会etc)

docker-rpgはどんなDocker imageなのか

R - ROOT
[https://root.cern.ch/}
物理解析の基本的な解析ツールで多くの素粒子原子核の研究室で使用されており、大量のデータを解析する際に重宝されます。グラフの描画ツールとしても優秀で、解析と同時に物理学っぽい(怒られそう)図を作ることができます。

f:id:salt22g:20200513062437p:plain
rootの起動画面
f:id:salt22g:20200513062809p:plain
rootのdemoプログラムより

P- python3
https://www.python.jp/
現在最も開発が盛んでプログラミングのスタンダードとなっている言語です。初心者にも理解しやすく、画像処理からデータ解析、機械学習まで用途は様々。
筆者の主言語もpython3です。

G- Geant4
https://geant4.web.cern.ch
モンテカルロシミュレーションによって粒子の物理過程をシミュレーションできるツールです。
物理実験はもちろん医療分野においてもよく用いられています。

f:id:salt22g:20200513062124p:plain
https://indico.physics.lbl.gov/event/581/contributions/1401/attachments/1284/1424/Geant4_CesarGR.pdf

これらの開発ツールは導入が面倒です。
macOSではhomebrewがあるので簡単になっていますが、他の人と共有しているLinuxサーバーでは制約の大きさから依存関係のパッケージをインストールできず導入ができないことがあります。

また、ROOTとGeant4についてはWindows PC上では上級者でないと動かすのはなかなか大変です。
基本的にLinuxの仮想環境またはWSLを立ち上げる必要があります。
phst.hateblo.jp
(↑こちらの記事は参考になります。)

そこで、是非このDocker imageで開発環境をサボりましょう。

使用時の様子

Mac

f:id:salt22g:20200513054238p:plain

Linux (Ubuntu)

f:id:salt22g:20200513070547p:plain
Linuxサーバ上でbuildしたGeant4 マクロの実行中の様子

Windows

まだテストできてない。
近々追記する予定です。

pythonのglobで末端のファイル名だけ取得したい

pythonのglob.globによるファイル名の一括取得。便利ですよね。
glob君に任せ切りにしていたら一瞬迷ったのでメモとして残します。
全然大した話ではないのですし、もっと良いやり方があるかもしれません。

2020/05/17 15:20 追記

os.path.basename()

これで行けるらしい。俺がアホだった。

cv2.imwrite(new_dir + os.path.basename(file1), merge_img)

問題

こんな処理をしたい時がありました。

  • 二つのファイルに同じ名前だが異なる処理をした画像がある。
  • これらのファイルから同じ名前の画像を読み込み横につなげる。
  • 別のファイルに元と同じ名前で書き出す。

ファイル構造はこんな感じ。名前は連番ではない場合もあります。

current_dir/
  ┠ gattai.py
  ┠ images_A/
  ┃  ┠ abcdefg.png
  ┃  ┠ hijk.png
  ┃  ┠ lmnopq.png
  ┃  ┠ 以下略
  ┠ images_B/
  ┃  ┠ abcdefg.png
  ┃  ┠ hijk.png
  ┃  ┠ lmnopq.png
  ┃  ┠ 以下略

↑を↓にしたい。

current_dir/
  ┠ gattai.py
  ┠ images_A/ #略
  ┠ images_B/ #略
  ┠ images_C/ #自分で名前をつけて作成
  ┃  ┠ abcdefg.png #処理されて合体した画像
  ┃  ┠ hijk.png
  ┃  ┠ lmnopq.png
  ┃  ┠ 以下略

最初は何も考えずこんなコードを書きました。

import numpy as np
import cv2
import os
import glob
import sys
import re

args = sys.argv
filename1 = args[1] #images_A
filename2 = args[2]  #images_B
outname = args[3]   #images_C

if __name__ == "__main__":

    new_dir =  outname + "/"
    os.makedirs(new_dir, exist_ok=True) #出力用のファイルを生成

    files1 = glob.glob(filename1 + "/*.png") #ファイル内の画像のパスを取得
    files1.sort()
    files2 = glob.glob(filename2 + "/*.png")
    files2.sort()


    for file1, file2 in zip(files1, files2): #読み込む画像パスを名前をループで取得
        img1 = cv2.imread(file1)
        img2 = cv2.imread(file2)
        images = [img1, img2]
        merge_img = np.concatenate(images, axis=1)
        cv2.imwrite(new_dir + file1, merge_img)

実行してみると…

python3 gattai.py images_A images_B images_C

実はこの部分がまずい。

new_dir + file1 ## => images_C/images_A/hogehoge.png

globで取得したfile1には
images_A/hogehoge.png
というように相対パスが全て書き込まれます。

os.makedirs(new_dir, exist_ok=True)

で作成したのはimages_Cまでなのでその中のimages_Aは作成されておらず画像は保存できません。

f:id:salt22g:20200517053640p:plain
模式図(わかりづらい)

解決

pythonの文字列分割機能を使って解決しました。
qiita.com

phrase = "If you can dream it you can do it"
# 「split」使用 .split(区切り文字, 分割数)
print(phrase.split()) ## => ["If","you","can","dream","it","you","can","do","it"]

word = "HelloWorld"
# 「rsplit」使用 .rsplit(区切り文字, 分割数)
print(word.rsplit("l",1)) ## => ['HelloWor', 'd']

今回globで取得したパスは
/images_A/hogehoge.png
取得したいのはhogehoge.pngの部分なので、"/"で区切った末端の部分を取れば良いですね。

phrase.split("/") ## => ["", "images", "hogehoge.png"]
phrase.split("/")[-1] ## => ["hogehoge.png"]

ということで最後のcv2.imwriteをこのように書き換えました。

cv2.imwrite(new_dir + file1.split("/")[-1], merge_img)

これで当初の狙い通りの出力になりました。めでたし。

Docker locale設定のerrror

Docker image作成時に起きたlocaleに関するerrorを解決しました。
ググっても「Ubuntuの日本語化」に記事ばかりで遠回りになってしまい、しばらくうまく行きませんでした。
最終的に解決法がわかったのでこの記事に書いておきます。

環境

hostOS: macOS & ubuntu(どちらでも同じ症状)
Docker version: 19.03.8
元にしたDocker image: ubuntu:18.04(公式)
hub.docker.com

症状

# docker run -it hogehoge
manpath: can't set the locale; make sure $LC_* and $LANG are correct
bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)

使っている分には影響はなかったがなんか気持ち悪い。
調べてみるとlocale(言語設定)の問題らしい。

# locale
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory
LANG=en_US.UTF-8
LANGUAGE=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=en_US.UTF-8

正しく設定できていないとのこと。
使えるlocaleを確認すると…

# locale -a
C
C.UTF-8
POSIX

え…デフォルトに英語ないの??!…公式imageなのに…

解決法1

Dockerfikeに以下の記述を追加

# locale
RUN apt-get update
RUN apt-get install -y locales
RUN locale-gen en_US.UTF-8

これでen_US.UTF-8の環境が構築されるのでerrorが消える。

解決法2

コンテナ内で以下のコマンドを叩く

# apt-get update
# apt-get install -y locales
# locale-gen en_US.UTF-8

これで起動時のerrorも消えてスッキリしました。
↓解決後

# locale
LANG=ja_JP.UTF-8
LANGUAGE=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=en_US.UTF-8

# locale -a
C
C.UTF-8
en_US.utf8
POSIX

ちゃんと追加されてますね。

大学院生Macユーザーへ Dockerの勧め

最近研究でDockerを使うようになり、Linuxサーバー上で実行環境を作成するなどしていました。
使っているうちに「Docker面白いな」と感じたので手持ちのMac上でも使えるようにして詳しくなりたいと思った次第です。

f:id:salt22g:20200511183927p:plain
Dockerのクジラ可愛い

Dockerとはなんぞ。

qiita.com
色々なサイトで紹介されています。上記のサイトは余計な絵が少なくておすすめ。
筆者はDockerを環境構築をサボれるツールだと考えています。
「全く同じ開発環境(Docker image)をどのマシンでも使うことができる。」
これはチームでの開発はもちろんですが、理系大学院生にとって後輩への引継ぎ、指導が楽になることを意味します。

研究室で必要なツールを1から後輩のPCにインストールするとなると指導や質問対応に多くの時間を割かねばなりません。
それはそれぞれのPCの環境(OSやバージョン)が異なることが大きな理由の一つです。
特にMacユーザーの私がプログラミングの講習会をする際には、Windowsユーザーのことも考える必要がありました。
手元の動かせるWindowsマシンで環境構築を試さなければなりません。
Dockerを導入すれば同一の開発環境をWindowsでもmacOSでも準備できます。
今回はまず、MacOSへの導入についてです。

Dockerの導入

まずはmacOSにインストール。
環境はこちらの記事を参照。今回もHomebrewを使います。
salt22g.hatenablog.jp

CUI及びGUIアプリを取得。

% brew install docker
% brew cask install docker

インストールできたか確認。

% docker --version
Docker version 19.03.8, build afacb8b

f:id:salt22g:20200507095505p:plain
メニューバーにコンテナを載せたクジラが出現

Dockerの主なコマンドはこちらのサイトによくまとまっています。
qiita.com

Docker imageを作成、起動

今回作ったDocker imageについては後日詳しくお話しします。
Dockerfileの作り方についてもこのブログで公開する予定です。

Dockerfileを作成したフォルダに移動して

% docker build -t <イメージ名> .

イメージが作成できているか確認してみましょう。

% docker images
REPOSITORY                              TAG                 IMAGE ID                  CREATED                 SIZE
自分が作ったイメージ名            latest              イメージID               作成された時刻        ファイルサイズ
ubuntu                                         18.04               c3c304cb4f22        2 weeks ago             64.2MB

自分が作ったイメージの名前が表示されればOKです。
早速起動しましょう。

% docker run -it <イメージ名>
[root@8fc1f79b0233] /workspace

docker runには様々なoptionがあるのでそれはまたどこかで…

自分のイメージをDockerhubにpush

Docker hub
作成したimageをクラウド上で管理するのがDocker hubです。
他の人が作ったimageはもちろん。公式imageも掲載されています。
今回は筆者が作ったimageをpushしてみます。

アカウントを作成
hub.docker.com

f:id:salt22g:20200507095954p:plain
アカウントが作れたらクジラをクリックしてsign inしておく。

予めdockerhubでリポジトリを作成。

f:id:salt22g:20200507104735p:plain
リポジトリを作成

f:id:salt22g:20200508153426p:plain
こんな感じ

% docker tag <ビルドしたimageの名前> <DockerhubのユーザID>/<さっきDockerhubで作ったリポジトリ名>:<tag名(latestとするのが一般的)>
% docker login -p="Dockerhubのパスワード" -u="Dockerhubのユーザー名"
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded

なんかWARNINGが出たけれど無視。
Login Succeededが出ればOK。

docker push salt22g/docker-rpg:latest

f:id:salt22g:20200511012440p:plain
push後のリポジトリ画面

GUIを使ってみる

まずはsocatをインストール。

brew install socat

XQuartzもインストール。
salt22g.hatenablog.jp
この記事でも紹介したソフトです。
sshで接続した先の画像やGUIを取得するサーバーみたいなものです(多分)

brew install caskroom/cask/brew-cask
brew cask install xquartz
open -a XQuartz #普通にlaunchpadから起動してもOK

f:id:salt22g:20200513051908p:plain
XQuartzを起動するとxtermという画面が出てきます。

Docker側から見たMac(hostマシーン)のIPアドレスを取得。

ifconfig en0 | grep inet | awk '$1=="inet" {print $2}'
    xxx.xxx.xxx.xxx #ipアドレス

socatを起動。

socat TCP-LISTEN:6000,reuseaddr,fork UNIX-CLIENT:\"$DISPLAY\"

ターミナルから返答がないはずなので別ターミナルからdockerを以下のオプション付きで起動。

docker run -it <イメージ名>-e DISPLAY="xxx.xxx.xxx.xxx:0"

f:id:salt22g:20200513053506p:plain
こんな感じ

dockerコンテナ内で作成したグラフが表示できるか見てみましょう。

f:id:salt22g:20200513054132p:plain
pythonのmatpltlibとか
f:id:salt22g:20200513054238p:plain
ROOTとか

後輩や部下の環境構築に困っている方々いかがでしょう。