なめこ備忘録

プログラミングに関する備忘録や経験したこと,考えたことなど好き勝手書きます.

[tex: ]

画像処理.js -エッジ検出 2

こんにちは,なめこです.

jsでの画像処理2回目です.
前回の記事はこちらになります.

nanameko.hatenablog.com

今回はなんとなくエッジ検出について調べた時に見つけた手法を実装してみたいと思います.

こちらの記事の既存手法と提案手法(色情報と大津の閾値判別法を用いたエッジ検出)を両方実装していきます.
(既存手法の名称はわからなかったのでここでは「動的色境界によるエッジ検出」とします)

※詳細なアルゴリズムに関しては書かれていなかったので,正確には"それっぽい"実装となります.全くの見当外れではないとは思いますが100%正しいものではないと思いますので悪しからず

www.ail.cs.gunma-u.ac.jp

色情報を用いたエッジ検出のメリット

先に今回のような色情報RGBを使用することのメリットを簡単に説明しておきます.

まず前提として,カラー画像からグレースケール画像へ変換する際は以下のような式による計算が行われています.

$$Y = 0.299\times R+0.587\times G+0.114\times B$$

式に示すまでもないとは思いますがRGB -> Grayは非可逆な変換であり,RGBの組み合わせとYは一対一での対応はできません.
色によって係数が大きく異なるのも特徴的かもしれません.
そのためカラー画像の時に差が大きく明確であった境界も,グレースケール画像に変換することでその差が小さくなってしまう場合があります.

例えばRGBで表現された以下の2つの色 $$R:255, G:0, B:255$$ $$R:0, G,180, B:0$$ これはカラー画像においては全く違いますが,グレースケール画像に変換するとほぼ同じものになります.

つまりこういった色変化によって生じるエッジがある場合,グレースケール画像を前提とした手法では検出できません(あるいは検出できたとしても弱いエッジとなるためその後の処理で切り捨てられる可能性がある).

色の境界を用いたエッジ検出を行うことで,これらのエッジを検出することができます.
実用例としては道路標識検出などがあるようです. 駐車禁止の道路標識などがグレースケールでの手法だと検出できないようですね.

動的色境界によるエッジ検出

ひとまず簡単そうな既存手法から始めます.

アルゴリズム

記事に示されていた処理の流れとしては以下の通りです.

  1. カラー画像をRGB成分に分解
  2. 分解した画像を2値化
  3. 三つの画像を統合(3bit画像の生成)
  4. 色の変化している部分をエッジとして判定

3を経た上での4の処理の詳細がちょっとよくわからなかったので,今回は以下の流れで処理することにしました.

  1. カラー画像をRGB成分に分解
  2. 分解した画像を2値化
  3. 3つの画像それぞれでエッジ検出
  4. 3つのエッジ画像のいずれかでエッジであればエッジとして判定

2値化には大津の閾値判別法(判別分析法)を用います(2値化の代表的な手法なので詳細は省きます).

エッジ検出は既にSobelフィルタを実装済みなのでこちらの方が楽だと判断しました.
3bit画像で色変化が起きている部分を~という処理と大きな違いはないように思います.

実装

今回カラー画像として読み込んであるので,imgは3次元配列となります. ちなみにbinは2値化,sobelはSobelフィルタを適用する関数となります.実装の詳細はGitHubへどうぞ.

const imgBGR = [];
const imgBGRbin = [];
const imgBGRsobel = [];

// 色ごとに操作
for(let n=0; n<3; n++){
    // 単色のデータ取得
    imgBGR[n] = map(img, v=>v[n]);
    // 2値化
    imgBGRbin[n] = bin(imgBGR[n], {maxVal:255});
    // エッジ検出
    imgBGRsobel[n] = sobel(imgBGRbin[n]).sobelImg;
}
// RGB画像に戻す処理(3bit画像の生成)
const imgBin = map(imgBGRbin[0], (v,i,j) => [v, imgBGRbin[1][j][i], imgBGRbin[2][j][i]]);
// いずれかの色画像にエッジがあればカラー画像のエッジとして処理
const imgEdge = map(imgBGRsobel[0], (v,i,j) => (v+imgBGRsobel[1][j][i]+imgBGRsobel[2][j][i])>0?255:0);

RGB画像って言ってるのになんで変数名がBGRなのかというと,OpenCVではBGRの順になっているからです.まあ別にどの色を使うとかは考える必要はないのでなんとなく合わせてるだけですが.

単色のデータの取り出しは比較的簡単でしたが,RGB画像に戻す処理がちょっと汚いコードになってます.
綺麗な処理を思い付いたらしれっと直すかもしれません.メモリアクセス的に残念な気がしますし.

最終的なエッジ画像の作り方も結構雑です.3色のエッジ画像の画素を足して値があればエッジにしています.
今回は勾配の大きさによる切り捨ては行なっていません.

実行結果

2値化後の3bit画像及び今回の手法でのエッジ検出の結果を示します.

前回のSobelやCanny法によるエッジ抽出と比較するとよくわかりますが,グラデーションなどで境界が曖昧な部分もかなり綺麗に検出できています.
ただその代わりに陰影の部分に強く反応してしまっています.

f:id:NAMEKO:20190217040626p:plain
3bit画像

f:id:NAMEKO:20190217040712p:plain
エッジ抽出結果

色情報と大津の閾値判別法を用いたエッジ検出

既存手法である動的色境界によるエッジ検出の改良版です.

既存手法では明暗が激しい画像において(逆光の強い画像など),明暗の境界として表現されるのエッジしか検出できない問題があるため,それへの対応を行った手法です.

アルゴリズム

基本的には上記の既存手法と同様ですが,この手法では画像を小領域に分割してそれぞれに処理を行います.

大津の閾値判別法による閾値決定を小領域ごとに行うことで,小領域ごとの明るさの違いに対応した閾値が定まります.
ただしこれでは小さすぎる変化にも反応してノイズとして出てしまうので,小領域全体の明るさに対応したまた別の閾値を定め,その閾値以下であればその小領域内のその色の変化を無視するようにしています.

また最初にノイズ除去のためのガウシアンフィルタをかけています.

処理の流れとしては以下の通りです.

  1. 画像全体(各色ごと)にガウシアンフィルタをかけてノイズ除去
  2. 画像を小領域に分割
  3. 分割画像ごとに2値化(大津の閾値判別法 +a )
  4. 2値化した分割画像ごとにエッジ検出
  5. 分割画像の結合

このうち3の+aの詳細は不明だったので,「小領域全体の輝度の平均」と「大津の閾値判別法で求められる分散」の比が一定以下となる場合に色変化をなくす,という方法を適用することにします.
完全に「こんな感じかな?」って感じの思いつきレベルですのでどれほど正しいかは不明です

実装

let splitsize = 64;
let threshold = 1;

// ガウシアンフィルタ設定
const gaussianFilterSize = 5;
const gaussianFilterSizeHalf = (gaussianFilterSize-1)/2;
const sigma = gaussianFilterSizeHalf/2;
const gaussianFilter = Array.from(new Array(gaussianFilterSize)).map((_, j) =>
    Array.from(new Array(gaussianFilterSize)).map((_, i) => {
        return math.exp(-(j*j+i*i)/(2/sigma/sigma))/(2*math.PI*sigma*sigma);
    }));

// 変数準備
const imgBGR = [];
const imgBGRbin = [];
const imgBGRsobel = [];
const sizes = image.sizes;
const rowNum = sizes[0]/splitsize;
const colNum = sizes[1]/splitsize;

// 色ごとに操作
for(let n=0; n<3; n++){
    // 単色のデータ取得
    const v = map(img, v=>v[n]);
    // ガウシアンフィルタの適用
    const vGaus = conv.conv(v, image.sizes, gaussianFilter, [gaussianFilterSize,gaussianFilterSize], {mode:conv.EXPAND});
    const val = map(v, v=>0);
    const valBin = map(v, v=>0);

    // 小領域ごとに処理
    for(let j=0; j<rowNum; j++){
        for(let i=0; i<colNum; i++){
            const img_small = v.slice(j*splitsize, (j+1)*splitsize).map(v => v.slice(i*splitsize, (i+1)*splitsize));
            
            // otsuの2値化
            const img_smallbin = (img => {
                maxVal = 255;
                const hystgram = Array.from(new Array(256)).fill(0);
                let t = 0;
                let sum = 0;
                let sumPow = 0;
                let omg = img.length*img[0].length;
                for(let V of img){
                    for(let v of V){
                        hystgram[v]++;
                        sum += v;
                        sumPow += v*v;
                    }
                }
                // 全体平均の計算
                const mean = sum/omg;
                if(mean == 0){
                    return map(img, v => 0);
                }

                let omg1 = 0;
                let sum1 = 0;
                let sigmab_max = 0;
                for(let [i, n] of hystgram.entries()){
                    omg1 += n;
                    sum1 += i*n;
                    const omg2 = omg - omg1;
                    const sum2 = sum - sum1;
                    const m = sum1/omg1 - sum2/omg2;
                    const sigmab = omg1*omg2*m*m;
                    if(sigmab > sigmab_max){
                        sigmab_max = sigmab;
                        t = i;
                    }
                }
                sigmab_max /= omg*omg;
                // 全体平均と分散による閾値処理
                if(sigmab_max/mean < threshold){
                    return map(img, v => 0);
                }
                return map(img, v => t<v?maxVal:0);
            })(img_small);

            // Sobelフィルタの適用
            const img_smallsobel = sobel(img_smallbin).sobelImg;

            // 画像の結合処理
            for(let l=0; l<splitsize; l++){
                for(let k=0; k<splitsize; k++){
                    if(j*splitsize+l < sizes[0] && i*splitsize+k < sizes[1]){
                        valBin[j*splitsize+l][i*splitsize+k] = img_smallbin[l][k];
                        val[j*splitsize+l][i*splitsize+k] = img_smallsobel[l][k];
                    }
                }
            }
        }
    }
    // 2値化画像
    imgBGRbin[n] = valBin;
    // エッジ画像
    imgBGRsobel[n] = val;
}
// RGB画像に戻す処理
const imgBin = map(imgBGRbin[0], (v,i,j) => [v, imgBGRbin[1][j][i], imgBGRbin[2][j][i]]);
// いずれかの色画像にエッジがあればカラー画像のエッジとして処理
const imgEdge = map(imgBGRsobel[0], (v,i,j) => (v+imgBGRsobel[1][j][i]+imgBGRsobel[2][j][i])>0?255:0);

記事では小領域を50x50としていましたが,キリのいい数字が良かったので64x64としました.
また閾値は何度か試してみてとりあえず設定した値です.

既存手法の実装での2値化処理は用意しておいたモジュールに投げてましたが,今回は大津の閾値判別法の処理途中で求められる画素の総和や分散などが必要だったためその場で即時関数として実装しています.

分割・結合処理は正直なところかなり無駄が多いのでもう少しマシな方法を考えることにします.

実行結果

小領域ごとに2値化している関係で少し妙な感じの3bit画像ができています.
また,3bit画像を見るとわかりますが,エッジが小領域の端の部分にしか含まれていない場合は高確率で切り捨てられています.
これが原因で一部エッジが途切れていますね.

ただ既存手法で反応が強かった陰影には今回ほとんど反応していないので一長一短といったところでしょうか.

ちなみに小領域のサイズを変えると結果が全然違ってきます.画像によって最適な小領域のサイズが全然違ったので色々試してみる必要がありそうです.

f:id:NAMEKO:20190217042532p:plain
3bit画像(領域分割して生成したものを再度結合)

f:id:NAMEKO:20190217042642p:plain
エッジ検出結果

まとめ

今回は色情報を用いたエッジ検出を行いました.

基本的には,グレースケールの時に使っていた処理を色ごとに使って後で結合するといった流れのようです.
前回の比較もしたかったのでイラストに対して実行しましたが,自然画像(特に道路標識が写っている逆光の強い画像)に対して実行してみるとその有用性が理解できるので面白いかもしれません.

今回の実装は以下のGithubのedge/dinamicColorBoundaryEdge.js(既存手法),edge/colorEdge.js(提案手法)にあたります.

github.com

次回は「流れ場を考慮したエッジ検出」を実装する予定です.
たぶん次でエッジ検出は一旦終わりですかね.

参考文献

OpenCV: Color conversions

色情報と大津の閾値判別法を用いたエッジ検出 - OhtaLabWiki

判別分析法(大津の二値化) 画像処理ソリューション

画像処理.js -エッジ検出 1

こんにちは,なめこです.

久しぶりの技術記事です.

大学での実験課題や趣味プログラミング,あとお仕事で画像処理に関する知識やら画像処理を行うjsプログラムやらが増えてきたので,せっかくなのでまとめていきたいと思います.
毎週(もしかしたら隔週)ちょっとずつ「画像処理.js」のタイトルで記事にしていくつもりです.もちろんjsで実装します.他の言語を使う予定なんてないです.処理速度?知らない子ですねぇ.

また基本的に画像処理のライブラリにある関数は使わずに泥臭くアルゴリズムそのものを実装をしていく予定です (画像処理ライブラリは入出力を簡単に行うためにあると思ってる).

ライブラリ

画像処理用,というより画像の入出力用としてopencv4nodejsを使用します.この子は普通に優秀です.人のこと言えないですけどなんでjsで画像処理しようと思ったんですかね.不思議.

github.com

その他数学的な計算用にmath.jsを使用します.

techc.omorita.com

opencv4nodejsでの基本的な操作をまとめておきます.
以降,画像の入出力やMat <-> Array変換の処理の記述は省略します(変数名の末尾がimgとなっているものは画像の配列).

画像入出力

\\ カラー画像読み込み
const colorImage = cv.imread(image_path, cv.CV_8UC3);
\\ グレースケールで読み込み
const grayImage = cv.imread(image_path, cv.CV_8UC3);

\\ 画像表示
cv.imshow('image', colorImage);
\\ キー入力があるまで待機
cv.waitKey();

\\ 画像保存
cv.imwrite(outImage_path, colorImage);

Mat <-> Arrayの相互変換

\\ Mat  -> Array
const img = image.getDataAsArray();

\\ Array -> Mat
const image = new cv.Mat(img, cv.CV_8UC3);

画像に対する基本処理

畳み込み演算

画像処理系の基本である畳み込み演算の実装をします.

せっかくなので使い回しをしやすいようにモジュール化してみました.

--- 2月16日更新 ---

画像の領域外処理を0埋めとするか,画像の拡張を行うかの設定をできるようにしました.
エッジ検出を行う際は画像を拡張させて畳み込むことで,縁取りのようなものが出ないようになります.

const ZEROPADDING = 0;
const EXPAND = 1;

module.exports.ZEROPADDING = ZEROPADDING;
module.exports.EXPAND = EXPAND;

module.exports.conv = (img, imgSizes, filter, filterSize, {func=null, mode=ZEROPADDING} = {func:null, mode:ZEROPADDING}) => {
    const cols = imgSizes[1];
    const rows = imgSizes[0];
    const filterCols = filterSize[0];
    const filterRows = filterSize[0];
    const filterColsHalf = (filterCols-1)/2;
    const filterRowsHalf = (filterRows-1)/2;

    if(func == null){
        if(mode==ZEROPADDING){
            return Array.from(new Array(rows)).map((_, j) => Array.from(new Array(cols)).map((v, i) => 
                filter.reduce((pre, cur, l) => {
                    let row = j+l-filterRowsHalf;
                    if(row < 0 || rows <= row)return pre;
                    return pre+cur.reduce((pre, cur, k) => {
                        let col = i+k-filterColsHalf;
                        if(col < 0 || cols <= col)return pre;
                        return pre+img[row][col]*cur;
                    }, 0);
                }, 0)
            ));
        }else if(mode==EXPAND){
            return Array.from(new Array(rows)).map((_, j) => Array.from(new Array(cols)).map((v, i) => 
                filter.reduce((pre, cur, l) => {
                    let row = j+l-filterRowsHalf;
                    if(row < 0)row = 0;
                    if(rows <= row)row = rows-1;
                    return pre+cur.reduce((pre, cur, k) => {
                        let col = i+k-filterColsHalf;
                        if(col < 0)col = 0;
                        if(cols <= col)col = cols-1;
                        return pre+img[row][col]*cur;
                    }, 0);
                }, 0)
            ));
        }
    }else{
        if(mode==ZEROPADDING){
            return Array.from(new Array(rows)).map((_, j) => Array.from(new Array(cols)).map((v, i) => 
                func(filter.reduce((pre, cur, l) => {
                    let row = j+l-filterRowsHalf;
                    if(row < 0 || rows <= row)return pre;
                    return pre+cur.reduce((pre, cur, k) => {
                        let col = i+k-filterColsHalf;
                        if(col < 0 || cols <= col)return pre;
                        return pre+img[row][col]*cur;
                    }, 0);
                }, 0))
            ));
        }else if(mode==EXPAND){
            return Array.from(new Array(rows)).map((_, j) => Array.from(new Array(cols)).map((v, i) => 
                func(filter.reduce((pre, cur, l) => {
                    let row = j+l-filterRowsHalf;
                    if(row < 0)row = 0;
                    if(rows <= row)row = rows-1;
                    return pre+cur.reduce((pre, cur, k) => {
                        let col = i+k-filterColsHalf;
                        if(col < 0)col = 0;
                        if(cols <= col)col = cols-1;
                        return pre+img[row][col]*cur;
                    }, 0);
                }, 0))
            ));
        }
    }
};

画像(2配列)とフィルタ,それぞれのサイズを引数として畳み込み後の画像(2次元配列)を返す関数として実装しました.
funcとして関数を与えると畳み込み後の各画素に対して任意の処理を追加することができる仕様です.

ちなみに領域外は0として処理してあります.

mapとかreduceとかをフル活用してるので可読性は低いですね.
ただまあ畳み込み後の配列を確保と計算を同時に実装した結果こうなりました.jsの配列に対するこれらの処理が好きなので変えるつもりはないです.

各画素への処理

毎回img.map.....って感じで書くのも面倒なのでこれもモジュール化します.
面倒と言ってもOne-Linerで済むのですが変なところでtypoするのも嫌ですし,何度も使うので簡単にできるものは簡単にしましょう.エンジニアは怠惰であるべき.

module.exports = (img, func) => img.map((v, j) => v.map((v, i) => func(v, i, j, img)));

はい,特段特殊なことはしてないです.全画素に対して行いたい処理を関数として渡せばいいだけですね.
必要に応じて添字とか画像の配列そのものを取得できます.

エッジ検出

準備が整ったので本題に入ります. 今回は一般的なエッジ検出ということでSobelフィルタとCanny法を実装していきます.

Sobelフィルタ

Sobelフィルタは基本的なエッジ検出で,その他の手法を用いる際の前段階として使われることが多いです. 処理の内容としては単純で,x軸方向,y軸方向それぞれで(離散的な)微分をとるだけです.
そうすると各軸方向の勾配の大きさを持つベクトルが得られるので,そのベクトルの大きさを計算すれば完了です.

各軸方向のエッジ(正確にはエッジに直交する)を得るためのフィルタは以下の通りです.

\begin{align} x軸方向 = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix}, y軸方向 = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix} \end{align}

このフィルタの適用後, \begin{align} g_{ij} = \sqrt{gx_{ij}^2 + gy_{ij}^2} \end{align}

とすれば各画素における濃度勾配が得られます.

実装

--- 2月16日更新 ---

畳み込み処理のモジュールの仕様変更に対応しました.

// Sobelフィルタ作成
const sobelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const sobelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
const filterSizes = [sobelX.length, sobelX[0].length];

// Sobelフィルタ適用
const edgeXImg = conv.conv(img, image.sizes, sobelX, filterSizes,
    {func:val=>(math.abs(val)>255?255:math.abs(val)), mode:conv.EXPAND});
const edgeYImg = conv.conv(img, image.sizes, sobelY, filterSizes,
    {func:val=>(math.abs(val)>255?255:math.abs(val)), mode:conv.EXPAND});

const edgeImg = map(edgeXImg, (v, i, j) => {
    const r = math.sqrt(v*v+edgeYImg[j][i]*edgeYImg[j][i]);
    return r>255?255:r;
});

絶対値とったり最終的にはuint8の形にしたいという理由で無理やり0~255の範囲に納めてます.
その他特に難しい処理はないので説明は省きます.

実行結果

結構綺麗にエッジが取れたんじゃないかと思います.

f:id:NAMEKO:20190210204812p:plain
Sobelフィルタ

Canny法

今回初めてちゃんと調べたのですが,案外処理が多いんだなと思いました.

手順としては以下の通りです.

  1. Gaussianフィルタを適用
  2. Sobelフィルタを適用
  3. Non maximum supperession処理
  4. Hysteresis threshold処理

詳細を説明するのは面倒なので簡単にだけ説明すると,

  1. ノイズ除去
  2. 輪郭抽出(濃度勾配計算とついでに勾配方向の計算)
  3. 非極大抑制(輪郭の細線化)
  4. 濃度勾配が小さく,勾配が大きい画素に隣接していないエッジの除去

と言った感じです.
詳細を知りたい方は参考文献の方をご参照ください.

内容としては割と単純だったのでさくっと実装しましょう.

実装

--- 2月16日更新 ---

畳み込みモジュールの仕様変更への対応と一部処理の間違いの修正を行いました.

// 定数定義
const imgSize = image.sizes;
let gaussianFilterSize = 5;
let thresholdLow = 0.25;
let thresholdHigh = 0.35;

// Gaussianフィルタ作成
const gaussianFilterSizeHalf = (gaussianFilterSize-1)/2;
const sigma = gaussianFilterSizeHalf/2;
const gaussianFilter = Array.from(new Array(gaussianFilterSize)).map((_, j) =>
    Array.from(new Array(gaussianFilterSize)).map((_, i) => {
        return math.exp(-(j*j+i*i)/(2/sigma/sigma))/(2*math.PI*sigma*sigma);
    }));

const gaussianImg = conv.conv(img, imgSize, gaussianFilter, [gaussianFilterSize, gaussianFilterSize], {mode:conv.EXPAND});

// Sobelフィルタ作成
const sobelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const sobelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
const filterSizes = [sobelX.length, sobelX[0].length];

// Sobelフィルタ適用
const sobelXImg = conv.conv(gaussianImg, imgSize, sobelX, filterSizes, {mode:conv.EXPAND});
const sobelYImg = conv.conv(gaussianImg, imgSize, sobelY, filterSizes, {mode:conv.EXPAND});

const sobelImg = map(sobelXImg, (v, i, j) => {
    const r = math.sqrt(v*v+sobelYImg[j][i]*sobelYImg[j][i]);
    return (r>255?255:r)/255;
});

// エッジに直行する角度の計算
const thetaImg = map(sobelXImg, (v, i, j) => math.atan2(sobelYImg[j][i], v));

// non maximum supperession処理
const nmsImg = map(sobelImg, (v, i, j, img) => {
    const theta = thetaImg[j][i];
    let di = 0;
    let dj = 0;
    const t = math.abs(theta);
    if(t < math.PI*3/8 || t > math.PI*5/8){
        di = 1;
    }
    if(t > math.PI/8 && t < math.PI*7/8){
        dj = (theta<0?theta+math.PI:theta)<(math.PI/2)?1:-1;
    }
    if((img[j+dj]?(img[j+dj][i+di] > v):true) || (img[j-dj]?(img[j-dj][i-di] > v):true)){
        return 0;
    }
    return v;
});

// hysteresis threshold処理
const cannyImg = map(nmsImg, (v, i, j, img) => {
    if(v < thresholdLow)return 0;
    if(v > thresholdHigh)return v;
    const theta = thetaImg[j][i];
    let di = 0;
    let dj = 0;
    const t = math.abs(theta);
    if(t < math.PI*3/8 || t > math.PI*5/8){
        dj = 1;
    }
    if(t > math.PI/8 && t < math.PI*7/8){
        di = (theta<0?theta+math.PI:theta)<(math.PI/2)?-1:1;
    }
    if((img[j+dj]?(img[j+dj][i+di] > thresholdHigh):false) || (img[j-dj]?(img[j-dj][i-di] > thresholdHigh):false)){
        return v;
    }
    return 0;
});

はい,まあそこそこ長くなりましたね.

Gaussianフィルタは自作するところからしてます.ちなみにCannyを含む多くの場合,カーネルサイズは5とするようです.

Sobelフィルタの濃度勾配計算の際にさりげなく0~1の範囲に縮小してあったりします.
角度計算にはarctanが使われています.atan2は象限も考慮してくれる優れものです.

Non maximum supperessionとHysterisis thresholdは濃度勾配の角度に応じて参照する画素が変わるので,ちょっと処理が面倒でした.
x軸方向,y軸方向,2つの斜め方向の計4つの方向に分けて参照する画素が変わるような設計です.
本当は角度の値に応じて一つ一つ処理を書いていくほうが綺麗なコードになるのですが,きっともっと単純化できるはずだと中途半端に考えた結果妙なコードが生まれました.多分一応処理としては合ってます.違ってたらごめんなさい実際ちょっと間違えてました.ごめんなさい.(2月16日追記)

因みにthresholdLowとthresholdHighは天から降ってきた値です.その時々によって最適な閾値は変わるのでしかたないです.

また領域外処理を書くのが面倒でちょっとよろしくない書き方をしてあります.
jsの配列とundefinedには以下のような特徴があります.

const a = [1,2,3];
console.log(a[5]); // <- undefined
console.log(a[-1]); // <- undefined
console.log(undefined < 1); // <- false
console.log(undefined ? 0 : 1); // <- 1

要するに領域外にアクセスしても即時エラーにはならず,またundefinedは評価の際は単体・比較に関わらずfalseとなる,ということです.
これを利用して「undefinedが出たら領域外ってことにしてね」という処理にしてあります.

いやほんとこれは良くないので良い子は真似しないでね.

実行結果

それぞれの処理の結果も入れておきます.

画像が1024x1024でそこそこ大きいせいかちょっと微妙な感じがありますね.パラメータチューニング次第で改善されるかもしれませんが,とりあえず実装はできてると思うのでこれでよしとしましょう.

--- 2月16日更新 ---

Canny法の処理の修正を行ったので結果が改善されました.
Sobelフィルタでの処理時には陰影にあたる部分も輪郭線として出ていましたが,Canny法ではそれらがなくなっていることがわかります.
その分境界がもともと曖昧だった輪郭は抽出されていません.

f:id:NAMEKO:20190216201315p:plain
Gaussianフィルタ

f:id:NAMEKO:20190216201429p:plain
Sobelフィルタ

f:id:NAMEKO:20190216201451p:plain
Non maximum supperession処理

f:id:NAMEKO:20190216201537p:plain
Hysteresis threshold処理(Canny法適用後の画像)


まとめ

今回は画像処理の基本的な処理を行うための関数の実装と一般的なエッジ検出を実装してみました.

エッジ検出をするときはSobelさんにはよくお世話になるので,Sobelフィルタくらいはすぐに実装できるようになっておくといいかもしれません.まあ画像処理系のライブラリを使えば普通Sobelフィルタの関数があるので自前で実装する機会はほとんどないと思いますが...

今回の実装も含め,jsによる画像処理の実装は以下のGithubにまとめてあります.

github.com

次回は最近見つけた「色情報を使ったエッジ検出」を実装してみたいと思います.
来週記事にできるといいなぁ.

参考文献

edge detection (Sobel filter/Prewitt filter)

【画像処理】Cannyエッジ検出器の原理・特徴・計算式 | アルゴリズム雑記

Canny edge detection

大学院への進学をやめることにした話

こんにちは,なめこです.

今回は,まあ,タイトルの通りですが,大学院への進学をやめて就職することにした経緯とか,進学前と後の自分の考えの変化とかそういったことを書いていきたいと思います.
自分の考えを書こうとすると駄文の大量生産をしてしまう悪癖があるので長文になりそうな気がします(これが既に駄文な気がしてる).

誰に向けたものでもなく,ただ単に自分の考えの整理をしつつのアウトプットとして書いてるだけなので興味と覚悟がある方だけどうぞ.

判断や結論はあくまで自分の主観によるものなので特定の誰かの考えを否定するものではありません.悪しからず.
こんな考え方の人もいるんだなー程度に捉えていただければ.

あとあくまで情報系に限った話であることにもご注意ください.他分野だとどうとかは知らないですので.

そんなことを考えるに至った経緯

少々転機がありまして,改めて(大袈裟かもしれませんが)人生設計を考え直すことにしたのが始まりです.

もともと,「絶対に修士まで行って勉強したい研究したい!」と考えていたわけでもなく,「自分のやりたいことを考えると進学した方がいいのかな」「高専から編入するなら修士までは行かないとな」といった感じに割とふわっとした動機で進学していました.

もちろん,もう少しちゃんと(何がちゃんとなのかは今となっては謎)学んでみたいとか,研究への憧れとか純粋な向上心(?)もありましたし,高専という名の井戸から出て大海を知りたいという考えもありました(蛙ではなくなめこですが).

あとはまあ,不純な動機としては「こんな田舎に居られるか!私は都会に行くぜ!」とか,「実家暮らしは嫌!一人暮らしをしたい!」とか,「もうちょっと学生やっていっぱい遊んでいたい」とか,「修士卒なら生涯年収が...ぐへへ...」等々...いやこれ思ってたよりひどいですね.書いてて普通に出てくるので自分で割とショック受けてます...穴があったら入りたい.なんなら自分で掘るまである.

とまあ,そんな感じで不純度70%くらいで編入してたわけですが(それでもちゃんと自分なりの考えがあった...はず),編入後紆余曲折あったのでそれらをきっかけに再度考えることにしました.

あとつくばに来てからの友人がこんな記事を書いたのを知って,話してみたのもきっかけの一つですね.
ああ,別に編入までして大学に来たんだからって大学院進学にこだわらなくてもいいんだなって.今考えると当たり前の話ですが,一度持った先入観ってなかなか抜けないんですよね.

sakura-lov.hatenablog.com

大学に来るまでのイメージと考え

考え方が甘々なのでアレですが...

高専生,及び自分の能力

高専生は即戦力となる」という言葉は高専生ならば一度は耳にしたことがあると思います.
ただ正直在学中にこれを実感できる人ってかなり少数だと思ってます.

明確な意思を持って外に出ようとしなければ基本的には閉じた空間ですし,外に出たとしても俗にいう強い人がゴロゴロいて上を見るとキリがないので,自信を持って「社会で力を発揮できます!」なんて人は本当に能力があるか人生をナメくさってるのどっちかじゃないでしょうか(過激派).

かくいう自分も自己評価は低いタイプなので,特に社会に出ても現状できることなんてないと思ってました.
私の世代はそもそも外に出ようとするタイプ自体が少数だったので(その分,こと勉学においては強者がいっぱいでした),少なくともクラス内ではそこそこの能力はあるけど世間的に見るとそんなに高くないと思ってたのもその理由の一つです. 気まぐれでなにかのハッカソンとかに出て圧倒的強者にボコボコにされてましたし...

そもそもこういうのって自分がそこまで特別なことをしているっていう自覚がまるでないんですよね.ちょっと調べればできるとか思ってました.その土台となる知識があるかないかで全然違うんですね.

エンジニアとしての仕事

こちらはもっとふわっとした認識でしたね.
きっと想像もできないようなすごい人たちがすごいことしてるんだろうなーみたいな.(雑すぎる)

高専レベルの勉強程度では高度な仕事はできないのではと思ってましたし,研究系の仕事を任されることはまずなくて,良くも悪くも何でも屋になってしまうのではという心配も少なからずありました(毎日ルーチンワークをこなす日々...みたいな?よくわかんないですが).
個人的には「待遇のいいところで出来るだけ新しいものに触れていたいなー」程度のふわっふわした考えをしていて,「まあ研究開発にあたるお仕事がそれかなぁ」くらいに思って,「とりあえずそこらへんの職にしよー」って感じです.

我ながらふわっふわです.考えてるようで考えてないんじゃないんですかね.もしかしなくても馬鹿なのん?

お金の話

生涯年収とか初任給とかで調べるといっぱい出てきますけど,一般的には学歴高い方がお給料は上がります.

趣味だとかに十分にお金をかけれるようになるためには高い給料が必要になってきますし,一軒家に憧れもあった私はある程度年収を上げたいと思っていました,

あと猫飼いたいですしね.猫.猫様の快適な暮らしのために最適な家を設計するレベル.私,将来は猫カフェ開くんだ... いや,開きませんが.開かない,と思う....開かないよね?......開かないといいなぁ.

まあ他にも色々諸々の考えがあってお金を得るためには修士卒くらいは欲しいかなと思ってました.

大学に来てから感じた現実

大学

高専生と大学生

まず思ったのが「あ,高専生って能力があるんだ」ってことです.

これは別に高専生が特別だというわけではなく,専門のカリキュラムを5年通して受けて,曲がりなりにも卒業まで行っていれば,嫌でもその分野に関する基礎的な知識や考え方が身についてるという話です.

一般的な大学生は,高校3年間は入学のための試験勉強が主でしょうし,大学の最初の2年間は基礎教養がメインのようでまだそれほど専門的なことを授業ではやっていないようです(もちろん自主的に色々やってる学生もいますが).
単純比較するのはよくないですが,高専生が卒業するタイミングであくまで専門に関する基礎能力・仕事の処理能力のみを全体の平均で比較すると,高専生に軍配が上がるのかなーと思いました.
あくまでも高専卒業タイミングということなので,そこから先はいくらでも変動するとは思いますが.

とりあえず編入時点では専門的にはスペック高めになるわけですね.一般教養?知らない子ですね.

そんな中で編入生がどういう風に見られるかというと,まあ強い人って認識なようです.
編入しようと思う人は専門系がある程度得意とか興味がある人が多いでしょうし,編入はその中での狭き門ではあるので割合的に強い人が多くなりやすいのでしょうね.
あと先に行ったカリキュラム的なこともその要因な気がします.高専時代の専攻から大きく逸脱していない範囲で授業を取ると,かなりの割合が復習ないし培った知識でなんとかなる場合が多いです.
結果的に,高専からの編入生は「授業内容をすごく理解しているすごい人」に見られやすいわけです.ただの復習になってるってだけなんですがね.

高専からの編入生はさしずめ異世界転生者でしょうか.大変そうな授業の見極めさえできれば俺TUEEEEEできます.たぶん(うちの高専が筑波と相性が良かっただけかもしれない).

自分の経験や能力

編入生も込みで周りと比較してみて思ったのは,こと開発経験においてはそこそこあるほうなんだなということです.JSとか生きてれば1度はいじるでしょとか思ってるJS信者でしたが普通はそんなこともないようです.
プロジェクトがポシャった経験も多いので実績としてはほとんどないのですが,それでも複数のプラットフォーム・言語で開発した経験というのは案外レアなんだなと感じました.
まあWindowsゲームやらAndroidアプリにWebサービス開発,果てはArduinoやらFPGAまでいじってるわけですから,この年代ではそこそこ少数派ではある気もします.
こっちに来てからいろいろあってiOSアプリ開発Mac OSアプリ開発もしました.現代におけるメインプラットフォームは一通り触ったんじゃないですかね(流石に誇張).

ただ同時に自身のアウトプットの少なさに絶望しました. githubに草生やしたりとかコンテストとか...もっといろいろやってそれっぽい実績生やしておけばよかったなと.

研究

さて大学にわざわざ編入までしてきた理由の一つである研究に関してです.
まだ研究室配属が決まった段階で何一つ研究はしていないのですが,研究に関する話を聞いたり,後述するインターンで色々経験したことを踏まえて改めて考えてみました.

結論を先に言うと今は大学での研究にそれほどの魅力を感じていません.

どうやら自分は仮に研究をするとしても,応用研究として商品に繋がるものを研究するほうがしたいようで,これはどこかに就職したほうがいいでしょう.
また,そもそも研究系の仕事でないとしたくない,というわけでもなくなったので自分の中で研究に関する意欲が薄れてしまっています.ここらへんの詳細はインターンの章で.

わざわざ編入までしておいて何がしたいんですかね.
...いやほんと何しにきたんでしょうか.

インターン

大学に来た,と言うよりせっかく関東圏に来たのでいくつかインターンに行きました.

インターンに行くまでもいろいろありましたがそれはまた別の話.
またインターンに関してはすでに記事にしたものもあるので一部割愛. もし詳しく知りたい酔狂な方がいましたらこちらをお読みください.

nanameko.hatenablog.com

nanameko.hatenablog.com

今のインターン

運良く受け入れていただけた今のインターン先の話をします(いやほんとなんで受け入れてくれたんだろうか). 詳細はまたそのうち記事にすると思うので省きますが,現在はセルシスというところで長期のインターンをさせてもらってます. ちゃんと(?)お給料が発生するところなのでどちらかというとアルバイトではありますが,とりあえず働かせてもらってます.

www.celsys.co.jp

こちらでは機械学習系の部署に配属させてもらえたのでお仕事のたびにDeepLearningと格闘してます.

ここで仕事をするようになって,研究をするならここのように直接商品に繋がることをしたいと思いました.
同時にこう行った仕事の難しさも感じました.実行してから結果が出るまでに1,2日かかるので気軽にトライ&エラーなんてできませんし,こうなったら正解なんて答えがあるわけでもこうやったら成功するなんてものもないので成果が出せないとただただ申し訳なくなります.日々感じる給料泥棒感... ここら辺は経験による部分も大きいので慣れていくしかないですね.

また,BtoCだとユーザの声を直接聞けるのもいいなと思いました.
セルシスのサービスのユーザはTwitterユーザも多いので,たまに見かけると嬉しくなります.
いや,まだ私リリースできるような成果出てないんですけど.

インターン総括

どちらかと言うとここが本題.

大学に来てから1年経たないうちにインターンを3つ経験して(今考えるとそこそこ多い),エンジニアとして仕事をするビジョンがかなり明確になりました. BtoBもBtoCもありましたし,ネイティブアプリ開発やWebのセキュリティ対策,研究系のお仕事にも携わることができました.結構幅広く体験できたんじゃないかと思ってます.

能力を示せるアウトプットが少ないため(ここが原因だと思ってる.そうだといいなぁ)選考に落ちてしまうことも多かったです(なんなら受かったところも「求める条件には達していないがポテンシャルに期待」と書かれてたりする). それでもインターン先では十分に評価をしていただけたようで,お褒めの言葉をいただけることもあったので仕事に対しては自信が持てました.
褒められることって少ないのでとても嬉しい.なめこは褒めると伸びる子なのでもっとみんな褒めて(なお褒めすぎると増長する模様).

ここでもらえた評価や職場での周囲と自分の能力を比較したところ,スキルとしては十分に通用するのではないかと感じました.
確かに足りないものは多くありますが,そのほとんどは実務経験によるものかなと.

また,研究開発でもない限り新しいことに触れる機会は少ないと勝手に思ってましたがそんなこともなかったです.
ITメインの企業であるということも理由の一つだとは思いますが,勉強会を熱心に行ってる企業って結構あるんですね.
新しいことのinputやoutputを一見仕事に関係ないように思えることも含めて仕事として行える,というのは自分にとってとても魅力的に映りました. 企業選びと自分の向上心次第で自分のやりたいことができる,ということを知りました.

あと単純に楽しかったです.やっぱり自分は開発自体が好きなんだなと実感しました.
今は週一,二程度しかお仕事していませんが,可能ならば毎日お仕事したいです.あ,土日祝日はお休みでお願いします.あと有給は自由に取らせて(ほんとに).

感じたことの総括

さて,大海に出てみた所感ですが,「今の自分でも十分に戦える」といった感じです.いや,何と戦うんだって話なんですが.
少なくとも,自分のやりたい仕事に対して現時点で能力的に圧倒的に足りないものあるとは全く感じませんでした.

...アウトプットが圧倒的に不足しているので就職時に困ったことになりそうではありますが.

再びお金の話

一人暮らしを始めて,バイトをするようにもなって(耐えられなくてすぐやめてしまいましたが),一番痛切に感じたのはお金の重要性です.
家賃,食費,光熱費...生きていくにはお金がかかります.それに加えてどこかに行くとか,遊びとか,趣味だとかにも当然お金がかかるわけです.

大学で一人暮らしをして苦しんだのはお金がないってことです.いや,普通のアルバイトが肌に合わなすぎてメンタルやられてやめておいて何を言うのかって感じなんですけど.
「大学生は時間がある」は人によりますがほぼ真であると思います.ただその時間を全て有意義に使えるわけもなく,生活のためにバイトを多く入れないといけなかったり,時間が合わないとか場所がないとかで遊ぶこと自体が少なかったり...あ,友達が少ないだけですかそうですか.

あと関係ないのですがこの歳になると飲むことがメインになっちゃうのってなんでなんですかね.それはそれで楽しいので悪いわけではないですが,たまには昔のようにとりあえずカラオケ行くかって感じで遊びたい...なんでこっちでできた友人はカラオケ行く人いないのん(実はいるのかも?)...今やカラオケは地元帰った時にだけするレア度の高い遊びになってしまった...

閑話休題.お金の話に戻りましょう.

大学生は時間があってもお金がありません.むしろ在学しているだけでお金が飛んでいってます(学費,高い).
バイトをしていても社会人のようにイベントとかでパーっとって使い方は難しいわけです.そもそも社会人はイベントに行けるのかという問題もありますが.

さて学歴と給料の話は先にしましたが,エンジニアにおいてはどうなのでしょうか.
調べてみたり考えてみた結論としては,初任給こそ変わる可能性があるがその後は読めない,といったところに落ち着きました.
やりたいことや性格等を踏まえて考えると,ベンチャー企業への就職が自分に合ってると感じているのですが,そういったところだと初任給すら変わらないこともザラみたいです.経歴や勤務年数ではなく実力で判断するところも多いみたいですし(実情がどうかは別として).

ちゃんと探してみると待遇や給料のいいところは予想よりもずっと多くて,そこまで臆病にならなくてもいいのかなと思いました.
特にメガベンチャー,いいですよね.夢と安定の両方がある.

あとインターン先も含めていろんな人の話を聞いてみると転職の選択肢も見えました.
数年経って「どうしてもやりたいことができた」とか「給料をもっと上げたい」ってなったら怖がらず転職しちゃえばいいのかなと.

話があっちこっち行きましたが,要約すると,

  • 将来的にと言わず今すぐお金が欲しい
  • 別に就職タイミングで収入が全て決まってしまうというわけではない
  • なんなら転職という選択肢もある

ってことですね.わぁよくぼうにちゅうじつー.

生活とかの話

大学に来て,一人暮らしをしてわかったことがあります.

今の暮らしが圧倒的に向いていないということです.

授業が朝から入ってる時などは問題がなかったのですが,いざ授業が減って暇になると食事のタイミングが変わり,次第に食事そのものの頻度が減っていきました.
ひどい時は気づいたら何も食べずに1日が経っていた時さえありました.おかげで体重は減る一方でここ5年間(もしかしたらもっと長いかも)での最低体重を3kgも更新してました.

家族や友人にも心配されたりもしたので生活習慣を見直す必要性を切に感じました. 仕事をするようになれば少なくとも平日は2,3食は確保できるはずなので,それなら少しは安定するのかなと思ったり思わなかったり...するといいなぁ(希望的観測).

あとこれは地元就職でもしない限りどうしようもないんですが,病気になった時に一人暮らしってきついですね.
辛くても最低限家事をこなさないと生きていけないという...ちょうど今インフルにかかってる(峠は越えた.症状のピーク時は本気で死ぬんじゃないかと思った)ので身にしみて感じました.

大学院

大学院に行くともれなく研究の日々が待っています. 編入して1年経たないうちに体調やらメンタルやら壊しまくってる自分に果たしてそれが耐えられるのでしょうか.

多分無理です.まず間違いなくメンタルはやられますし,体重はさらに減る一方になる気がします.

それだけのものを支払って大学院に進学するメリットはあるのでしょうか.いや,ない.
少なくとも自分にはないです.向いてる向いていないって問題でしかないですが.

結論

結論も何も,冒頭にもタイトルにもなんなら経緯説明とかにもちょこちょこそれっぽいこと書いてあるわけですが,一応.

自分のやりたいことや求めているものを改めて考えた時,大学院への進学は必須ではないと感じました.
また,現在では進学自体が魅力的には映っておらず,早く就職したいと思っていることに気づきました.
自分の今の能力でも仕事で結果を出せそうだと自信を持ちました.
単純に研究漬けの日々に耐えられないのではという不安が過ぎりました.
あとシンプルに今のままだと体がやばそうです...このペースで体重を減らしていくとそのうち栄養失調で倒れるんじゃないですかね.食べねば.

と,まあ長々と書きましたが,様々なことを鑑みた結果進学をやめて就職しようと思います.

就職先について

興味があるのもそうですが,待遇とかも良いのでメガベンチャーにいくつか挑戦してみたいと思っています. あとは普通にベンチャー企業にいくつか.

ただ正直今はもうそこまで関東圏に憧れはないんですよね...しかしある程度都会でないと健全なエンジニア生活ができるかの不安もあるわけで.

スタートが既にちょっと(ちょっとどころではない可能性)遅いのが不安ではありますが就活頑張ってみようと思います.

蛇足中の蛇足

無駄に長くなりすぎました...こんなつもりじゃなかったんです.
なんか8000 charactersって文字列が見えてます.ちょっとよくわかんないです.

普段からアウトプットしていかないからこんなに増えるんでしょうね(そうであって欲しい).
溜め込むのも良くないので,これからはできる限りちょこちょこ近況報告などを書いていきたいです(書くとは言っていない).

オプト株式会社で2週間のインターンをしてきた件

こんにちは,なめこです.
9月の後半2週間を利用してオプト株式会社でインターンをしてきたので,その体験記を残します.

オプト株式会社に関して

インターネット広告代理店です.
顧客への手法の提案,データの解析,可視化など,広告という媒体を扱うだけでもいろいろあるんだなと...

www.opt.ne.jp

オフィスが広かったです.ビルの4,5階分(もっとあったかも?)くらいは全部オプトでした.
会議やら勉強会やらで階段を登ったり降りたりしてました.

経緯

前回に引き続きpaiza経由での応募でした.
面接時にまともに受けごたえをできた覚えがないので,何故受け入れてくれたのかは謎です.

期間

タイトルにもありますが2週間でした.
といっても運悪く祝日が2回も被ってしまったので,実際の日数としては8日でした...

配属先

稼働中のサービスの開発部のSREチームでした.
SRE(Site Reliability Engineering)はサイトのセキュリティ向上や緊急対応,ログ解析,開発に必要なツール作成など,主に運用に関する部分をプログラミングによって対応・改善し,品質の維持・向上を行います.その仕事の特性上,フロントエンドからバックエンドまで結構幅広く仕事をすることになります.

詳細に関してはここら辺を見るとだいたいわかるかと.

Googleが提唱した「Site Reliability Engineering(SRE)」とは|フリエン

やったこと

大きな括りとしてはセキュリティの強化にあたります.  
具体的には以下のような対策を,実際のサービスで行いました.

  • CSRF対策
  • XSSI対策

CSRFに関して

CSRFに関しては以前以下のような記事で一応まとめてあります.

nanameko.hatenablog.com

XSSIに関して

こちらはまとめてないので簡単に説明しておきます.
※ザックリ説明なので必要な部分が省かれている可能性があります.

XSSI(Cross-Site Script Inclusion)はクロスドメインのjsをscriptタグとして埋め込むことでそのjsで扱っているデータにアクセスする攻撃です.
scriptタグはクロスドメインでも実行が可能なため,ブラウザやフレームワークが勝手に良きに計らってくれないのが特徴的ですね(これがデフォルトで止められると,例えばjQueryを大元のURLで呼び出すことができなくなる).

対策としてはCSRFトークンの発行が有効です.
ただ,そもそもjsで重要な情報を扱うべきではないという考えもあるので,守るべき情報なら他の手段で扱う方がいいのかもしれません.

以下に参考文献を載せておきます. あまり知られていないのか対策の必要が出る自体あまりないのか,調べてもそういった記事があまりヒットしないですね

blog.motikan2010.com

XSSIに関連しているJSONPに関しても載せておきます(Wikipediaですが...).

JSONP - Wikipedia

事前準備

Scala, TypeScript, Playframeworkの勉強をしました.
勉強に使えるサイトなども教えてもらえたのでそちらを参考に進めました.

業務で使う言語やフレームワークはどれも全くの未経験でしたが,TypeScriptは静的JavaScriptだったり,Playframeworkがrailsライクだったりしたのでなんとか業務開始までには間に合いました.(なのでtsとplayはふわっとした理解)

体験記

入社研修から環境構築まで

初日の午前中はお話を聞くお仕事でした.目の前に支給されるMacBook Pro(持ってない)が置いてあって,待てをされ続けるペットの気分でした...

その後諸々の記入などを終え,環境構築に入ったのですが...正直この環境構築が最も鬼門でした.
READMEに不備があったり(これはまあ,バージョンなどは社内では共通認識の部分があるので多少は仕方がない),環境構築の過程で自動的にインストールされるイメージが自分の入社する数日前の段階から仕様が変わってしまっていたり...(直近で1から環境構築してる人なんていないので誰も気づけない)

そんな感じで2日もかかりました...全8日なので1/4が消えたことになります.つらい.

その後お仕事中に動作がおかしくなって結果的に再度環境構築をすることになるのですがそれはまた別のお話.

CSRF対策

インターンが決まってからあらかじめ説明されていたのがCSRF対策です.事前にある程度まとめられたサイトのURLなども教えてもらってたので,(一応)勉強済みです.

まずは実際どこにその脆弱性があるのかを確認する作業から始まりました.リクエストやレスポンスのログを見ながらいろんな部分にfetchリクエストを飛ばしてました.また毎回コンソール叩くのは大変なので,攻撃用(対策がなされたかの確認用でもある)ページを作って不正なアクセスを繰り返しました.合法的に攻撃できるのは中々レアな経験な気がします.

この際,全く同じ脆弱性だと思われていたものが違うものだと判明したので,そちらも後ほど対策をすることになりました.

その後,対策案の作成をしました.ポピュラーなやり方としてはCSRFトークンの発行ですが,トークンの諸々の処理を実装するのにかかる時間的コストが重めなので他の手法でできないかを調べたり考えたりしてました.

そして実際にコードをいじくりまわして対策...しましたが,まあ初めましてのコードでどこにどう突っ込めばいいのかはあんまりわからなかったので,ここはほとんどメンターさんがやってくれました...何しに来たのだろうか.

XSSI対策

XSSIは最初はCSRFだと認識されていたみたいです(まあ対策としては同じトークン発行で解決できます).が,今回の場合トークン発行はコストが大きかったため,他の方法で解決するのを目標に対策を考えました.

結果的に,「そもそもこのAPI(のような何か)の存在がよくないから消したい」ということになり,若干メンターさんに渋られましたが(残り日数的に間に合うかわからないため)押し通して謎API削除に乗り出しました.

詳細は省きますが,ルーティング周りやAPI周り,フロントエンドの処理まで編集しまくって見事某API撲滅に成功しました.結局バックエンドからフロントエンドまで全部いじりましたね...

その他

定期|不定期で開催される勉強会や会議などにも参加させていただけました.「こんなのが出たので使ってみた」とか「こんな本読んだのでまとめてみた」といった感じの話を聞けて楽しかったです. SREの本の輪読会といったものも開催していて,そこでようやくSREについての理解が追いついた気がします.

あといろんな方とランチを食べにいったりもしました.
経歴が中々すごい方とかもいて色々と話が聞けたのがよかったです.Slackのおかげでコミュ障でも大丈夫...

感想

環境構築だけは死にかけてましたが(READMEって大切だなととても実感しました),全体としてはかなり良かったです.
セキュリティは高専や大学の授業でもありましたが,実際に対策を行うといったことは今までほとんどなかったので良い経験になったと思います.
問題の特定,対策案の検討,実装,PRまでの一通りの流れを経験させていただけたので,エンジニアとしての未来像もうっすらとイメージできるようになった気がします.
インターン終了後の打ち上げで,お褒めの言葉をいただけたのがとても嬉しかったです.

ある程度の数の言語に触れてきたおかげか,新しい言語の理解も早くできるようになった気がします.
というかまあ,先人のプログラムを軽めにいじるだけならそこまで深い理解じゃなくても十分だなと感じました.

あと余談ですが,MBPやっぱりいいですね...Touch Bar派ではなかったのですが,Touch IDの前に陥落しました.
早くお金貯めて買おうと思います.

謝辞

今回受け入れてくださったオプト株式会社さん,実際に面倒を見ていただいたメンターさん,ランチなどで色々な話をしてくださった方々,2週間(それも8日間)の短い間でしたがお世話になりました.実際に仕事で貢献しつつ,自分にもプラスとなったのでとても良かったです.ありがとうございました.

また,今回もお仕事中の糖分補給を支えてくれた,なめこの生命の源ことSANGARIAのまろやかいちご&ミルクに最大限の感謝を捧げ,締めの言葉とさせていただきます.

製品情報 > 乳性飲料 > まろやかいちご&ミルク - はてしなく自然飲料を追求するサンガリア

CSRFに関する備忘録

こんにちは,なめこです.

最近業務でCSRF脆弱性への対策をする機会があったので,調べたことを備忘録として残しておきます.
あくまで備忘録なのでざっくりとした理解の部分もあります.

他にわかったことがあれば追記していく予定です.

CSRFとは

CSRF(cross-site request forgeries)はWebアプリケーションの脆弱性を利用した攻撃のことで,リクエスト強要とも呼ばれます.
悪意のあるサイトのリンクを踏ませることなどによって,ログイン中のサービスに対して意図しない操作を実行させる攻撃のことです.

仕組みに関しては後述しますが,この攻撃が成功すると,ログイン中にユーザが可能な操作を全て行えます. 例えばSNSなどでは,意図しない投稿,情報の公開/非公開の制限の操作などが可能です.
また,CSRFへの対策がされていない銀行のサービスがあった場合,他の口座への振込を強制されるようなインシデントが発生する可能性があります.

原理

一般的にWebアプリケーションのログイン状態は,Session IDという文字列をユーザ側とサーバ側両方で管理し,通信のたびにそれを照合することで保持されています.(他の認証方式もありますが本筋とはずれるので割愛)
このSession IDはユーザ側ではCookieなどに保存され,サーバにリクエストをする際に正しいIDを持っているかどうかで正当なリクエストであるかを判断しています. (Cookieはその値を設定したサーバに対してリクエストする際,自動的に送られます)
これは別のページからリクエストがあった場合も同様であり,正規のリクエストであるかの検証をSession IDとは別で行なっていない場合にCSRF脆弱性が生じます.

CSRF脆弱性のあるサービスのページにログイン中に,XSSや悪意のあるページによってそのページへのリクエストが送られると,そのリクエストにはSession IDの格納されたCookieが付随するので正当なリクエストとして処理されてしまいます.
このリクエストはログイン中のユーザのものとして送られるので,そのユーザがそのサービスで可能な操作であれば全て行える,ということになります.

対策

ユーザ側

「危険なURLを踏まないようにする」「適度にログアウトする」等の注意程度の対策となります.
そもそもユーザが危険を感じながら使わないといけないという状況があまりよろしくないので,ページを運営している側が対策をすることが必要です.

サービス提供側

主な対策はトークンの発行やreferrer監視,origin監視となります. 以下にその詳細を記します.

CSRFトーク

Session IDとは別に,推測不可能な文字列を発行し,正規のリクエストを行う際にはそれを付与するという方式です. Session IDとトークン,両方が正しいものが正当なリクエストとなります.

このトークンはユーザ側に持たせる必要がありますが,Session IDとは別の方法で保持させた方が良いでしょう.
セッションや,formのhidden値として発行するのが一般的でしょうか.

攻撃者は保存されているものを悪用してリクエストを偽ることはできますが,保存されている値そのものを参照することはできません(詳細はCORSで調べると良いかと).
そのため攻撃者はトークンの偽造はできず,攻撃を防ぐことが可能になります.

この方法が,現時点では最も有効なようです.

referrer監視

Referrer(Referer)はそのリクエストがどのURLから送られてきたのかが記録されています.
CSRFは外部ドメインからの攻撃なので,リクエストのReferrerを監視することで攻撃を防ぐことが可能です.
しかし,Referrerはブラウザ側の設定で非送信にすることができるので,ユーザの設定次第では正規のリクエストでも不正のものとして扱われてしまうという危険があります.

また,XSS脆弱性が残っている場合,正規のページから不正なリクエストを送信できるため,正当なリクエストとして処理されます.

Origin監視

Originにはリクエストが送られてきたURLのうち,サーバ名のみが記録されています.
手法としてはreferrer監視とほぼ同様となります.そのためXSSの弱点も同様に存在します.

※2018/9/27追記
OriginはReferrerと違い,ブラウザの設定で非送信にはできなさそうです.
また,クロスドメインなリクエストに関しては必ずOriginが付与されます(設定次第ではnullにはなるが,Originがあることには変わらない). なのでXSS脆弱性が存在せず,正規ユーザからの不正なリクエストには寛容なシステムであるならばOrigin監視で十分に対策できると考えられます.
Origin監視はCSRFトークンの発行よりも実装コストが低いことが多そうなので(フレームワークで簡単にフィルタ実装できたりするため),こちらを採用するのもアリかもしれません.

pre flight

まだちゃんと調べられてないです.(後日調べてまとめるかも)

フレームワークについて

フォームにトークンを自動的に埋め込み,リクエストの正当性を検討するフレームワークも多いようです(増えている?).
気にしてなくても既に対策されているかもしれませんね,

参考文献

www.ipa.go.jp

watanabe-tsuyoshi.hatenablog.com

3日間の短期インターンをしてきた件

お久しぶりです.なめこです.
この度,東京の地にて3日間に及ぶインターンに参加してきたので,その体験記のようなものを書いてみようと思います.

どこまで書いてよくて,どこからがダメなのかわからないので基本ふわっふわしてます.
また,業務として関わったのは本当に端っこの方だけで,その視点から見ているので実際とは違う可能性があります.
悪しからず.

企業情報

今回インターンの受け入れをしていただいたのは「株式会社インディバル INDIVAL,Inc.」さんです.

www.indival.co.jp

サービスとしては「shotworks」「シフトワークス」「ダイレクトマッチング」等,アルバイトや正社員の求人マーケット系ですね.

短期バイト・単発アルバイト探しなら【ショットワークス】

アルバイト・パート求人情報【シフトワークス】で楽に見つかるバイト探し

採用ホームページ集客・応募率改善|indeed掲載やリスティング広告運用も

経緯

今年度に入りようやく東京に近い地域(東京の任意の場所に行くのに片道2時間かかるとしても近いったら近いんです)に住むことになったので,東京にインターンに行こうと思い立ったのがきっかけです.
ついでにお金もらえれば最高とか思ってないです.なめこ,嘘,吐かない(嘘).

そんな感じでいくつか有給のインターンに応募して無事お祈りされ,paizaに登録してみてお祈りされ...と心が折れかかっていました.
そんな折,paizaを通してインディバルさんから「話でもどうですか?」と連絡がありました.無給でしたが半ば自棄になって面談に行ったのが全ての始まりです.

面談後の帰り道で内定出たのは驚きました.速すぎる...
せっかくなので経験積んでみようと思い,インターン参加を決意しました.

事前準備

swiftの学習を中心に,実際のアプリ作成,XUITest/XCUITestの作成,Appiumの学習をしました.

swiftは学習期間自体長くなかったため,友人より本を借りつつ一から学んでみました.
基本的に新しい言語の学習は,ネットで適宜必要な部分を調べる形でしかしてこなかったので,改めて本で学習してみると色々と学びもありました.
まあ,最終的に「残りは雰囲気でなんとかなるでしょう」と開き直って読破することなく終わりましたが...

アプリ開発に関しては元から少ししてたので確認程度に,テストに関しては軽く調べて実際に動かしてみて,雰囲気だけ理解して終わりました.

Appium?調べたけどよくわかんなかったです.とりあえずこういうものがあるんだな,といった程度の認識です.

業務内容

特筆すべき点はそんなにないですが一応事前に言われてた内容は以下の通りです.

  • バグ修正
  • テスト作成

体験記

1日目

出社してすぐにメンターさんのところに案内され簡単に挨拶をした後,これから始める作業をざっくばらんに説明を受けてお仕事スタートしました.ここまで10分程度ですね...爆速すぎる...

最初はとあるアプリのバグ修正をしました.
画面遷移の多いアプリ開発の経験はそれほど多くなかったので,プロジェクト内の大量のディレクトリとファイルを見て一瞬「ひいっ」ってなりました.
アプリの構成などの説明は一切受けていなかったので,とりあえず構成を確認しながらバグ発生画面のコードを探しました.
バグ自体は単純なものでしたが,いまいち原因が読み取れず少しずれたアプローチをしてしまいました.しばらくしてメンターさんに相談したところ,言語のバージョンアップによって発生したバグであることを教えられ,同様のバグに対処してるwebページのURLをぺいってされました.そんなの聞いてない...

バグ修正後は初コミット...!
お仕事した感がありますね.

その後XUITest/XCUITestの作成...というより既存のコードがいろいろ酷くてまともに機能していなかったので,これも修正に入りました.
メンターさん曰く,まだちゃんとテストコードは書いてない,とのこと....いや,それにしても...うん.
一度の取得でいいものを何度も取得していたり,前に書かれているテストが動いていること前提で書かれていたり...好きに書き換えていいと言われたので自分好みの構成に直しました.

そしてテストを直したり,直したテストの実行によって見つかったバグを直したりと,基本的にバグ修正を延々としていました.

初日はそんな感じで終了しました.
バグいっぱい直した.もう普通にお仕事してる気がするけどきっと気のせい.

2日目

前日と同様のお仕事をこなしていると,別アプリのバグ修正を依頼されました.
例によってアプリ構成はよくわからないままコードの海を漂うところから...
バグの内容から原因には当たりをつけてはいたのですが,考えてた構成と違っていたため,またもや若干違ったアプローチを(以下略.前日同様メンターさんに誘導されつつバグ原因となる位置を特定,修正は1行...よくある話ですね.
このバグ修正中にチョットナニヲシタイノカワカンナイコードを発見してしまったのですが,バグが発生するようなものではなかったため,きっとこれは意味のある文なんだと自分に言い聞かせてそっと目をそらしました.

その後紆余曲折(?)はありつつ,ひとまずXUITest/XCUITestの作成は終了し,別のツールでテストを行うための環境設定に入りました.

2日目はそんな感じで終了しました.バグ修正が板についてきた感.

3日目(最終日)

前日に引き続き別ツールの環境設定と,そのツールでのテストの作成をしました.
そちらのツールはまだ社内で導入されているわけではないようで,メンターさんと共に手探りでコード書きました. ある程度まではうまく行ったのですが,最後はメンターさんと一緒に詰まってリタイア.
時間が時間だったので若干残ってたXUITest/XCUITestのバグを直して3日間のインターンが終了しました.

帰宅後,メンターさんから「これで動いたー」といったメールをいただいて一安心.

感想

インディバルさんも短期でのインターンは初だったようで,お互い探り合いながら進んでいきました. 勢いで参加を決めたインターンでしたが,実際の業務に関われて得るものは多かったと思います.

自分のチーム開発経験との差異がどれだけあるのかが気になっていましたが,人数が増えていろいろ最適化されている部分以外はそれほど大きく変わるものではなかったように感じました.
まあそんなに大きく変わるわけもないですよね.いろいろオープンソース化されて個人と企業での環境差自体がそこまで大きくないわけですし.

メンターさんも詰まってたのを見れたのもある意味プラスでした(酷い).条件は自分とそんなに変わらないんだなと思えたので...新規のシステムの導入時は特に.
経験の差が大きいと感じたのは望んだ情報へのアクセスの早さと飲み込みの早さでしょうか.検索スキルってやはり重要なんだなと再認識しました.

個人的にxcodeのストーリーボードみたいなのを使うのがあまり好きではなく,個人の開発ではオブジェクトの作成・位置やサイズ設定も含めて全部コードで書いてしまうのですが,それだとデザイン側との切り離しができず,位置やサイズの確認のために毎回デバッグしなければならないので,大人数での開発にはやはり向いていないなと再認識.
これからはもう少し多人数での開発に適したやり方を意識していきたいと思います.

謝辞のようなもの

今回インターンの受け入れをしてくださった株式会社インディバルさん,およびメンターさん3日間お世話になりました.ありがとうございました.

そして,お仕事中の糖分補給を支えてくれたSANGARIAのまろやかいちご&ミルクに最大限の感謝を.ほんといつもお世話になっております.

製品情報 > 乳性飲料 > まろやかいちご&ミルク - はてしなく自然飲料を追求するサンガリア

筑波大学3年次編入試験の点数開示,そして近況報告

みなさまお久しぶりです.なめこです.

本当はもっと前から色々書くべきことがあったような気がしないでもないですが,そこまでの時間の余裕がなくて投稿できませんでした.
少しは新生活に慣れてきたので,近況報告を含め書いてみようと思います.

近況報告

ここでは言ってなかったと思いますが,実は高専を卒業して筑波大学情報学群情報科学類に3年次編入をしました.
筑波大学編入生の受け入れが多く,弊学類の同期だけでも15人ほどの編入生がいます.
だいたいが高専からの編入なので話しやすいですが,高専時代以上に専門にとんがったような人がほとんどなので,会話の内容がとてもマニアックな方向に行くことも多いです.この前はお好み焼きの話から名前空間とか抽象化の話に発展していきました.ちょっと訳がわからないですね.

念願の一人暮らしですが,自炊とバイトに自由時間を奪われ続けているため,そんなにゆったりとした生活はできていないように感じます. 授業数が減る夏以降のなめこの活躍に期待しましょう.

筑波大学編入学試験

概要

筑波大学情報学群情報科学類での編入試験は数学・物理・情報基礎で各大問2つとなってるうちの任意の4問を解くことになります.満点が200点なので各50点の配分ですね.
また外国語として事前に受験しておいたTOEICのスコアを提出することになります.満点は100点です.
TOEICスコア何点がテストで何点になるのかは後述ですが,大学編入の目安となる600点を超えていればそこそこ大丈夫そうです.
専門科目200点,外国語100点で合計300点が満点となります.受験期に調べてた感じだとボーダーが8割程度とされていたので,240点を超えることが目標となります.

私の場合

英語が得意ではなく,モチベーションも一向に上がらずでTOEICを受け始めたのは4年の3月の終わり頃からだったと思います.
まあそんな状態で点数が大きく上がるはずもなく,最終的な点数は目安の600点を100以上下回っていました.730点で満点になると聞いていたのでそこから割合計算で予想点数を出してみると64くらいでした.8割どころか6割です.
ただ数学と情報基礎に関しては得意分野ではあったので,「TOEIC低くても他で満点取れば受かるやろ!」という謎の自信に基づき,ひたすら数学の勉強をしていました.

そんな感じで受験して,なんとか合格をもぎ取りました.
試験前日の夜にTOEICスコアを見られた瞬間に不合格とされる悪夢にうなされたのも,今となってはいい思い出です?

点数開示の話

ようやく本題です.
点数開示ができる期間に入ったのでアポとって開示してきました.
結果は以下の通りです.

総合点 外国語 専門科目
259 64 195

わかっていたことですが,なんていうか極端ですね.予定通り数学と情報でぶん殴ったって感じでした.
周りと話してる感じだと,たぶん合格者の中では英語は最下位で専門がトップっぽいです.

TOEICが730以上は100になってたみたいでしたし,自分の英語の予想点数が完全に一致していたので,やはり英語は730を100点としてそれに対しての割合で点数が決まるようですね.

あとがき

そんなわけで点数開示とその他諸々のお話でした.
英語がそこまで得意じゃなくても,編入ならなんとかならないこともないという事例ですね.
ただ「なんとかならないこともない」というだけで,事前に点数を確定させられるTOEICで高得点確保しておくに越したことはないです.他で大きなミスはできないというプレッシャーとの戦いになって精神衛生上よくないので...

この記事が今後受験する方の参考になれば幸いです.