Featured image of post OpenCVでの古典的な画像処理について

OpenCVでの古典的な画像処理について

目次

概要

  • 数年間に渡って画像処理の仕事をしてきた時に残っていた古典的画像処理周りのメモ
  • 特に、2012年以前のCNNを代表とするDNN系の手法、最近のViT系の手法は別の記事にまとめる
  • 画像処理でよく使うlibraryはnumpy, opencv, scikit-image, matplotlib, jupyterlab, pytorch, dlib, imageprayなどなど
  • 個人的に使うOpenCVのスニペットも残す

知識メモ

基本

画像の種類

大きく分けて2種類ある。

  • ベクター画像
    • ベクター画像は、線や曲線といった図形集合の重ね合わせをデータとして保持した形式
    • e.g. svg
  • ラスター画像
    • ラスター画像はピクセル(画素ともいう)と呼ばれる色の数値を格子状に並べたデータを保持した形式
    • e.g. png, jpg

画像の圧縮方式の違い

  • PNGは可逆圧縮だが、JPGは非可逆圧縮
  • つまり、JPGはデータの圧縮の際にデータを削除するので小さな線分やオブジェクトが劣化する可能性がある
  • 一般的には、PNGを使うのを推奨
  • ただし、ベクトル化した場合はSVGを推奨

色空間

  • 色空間(カラースペース)とは、色を数値に変換するシステムのこと

カラースペース

レンダリングするメディアタイプによって、色域は異なるので注意。

レンダリングによる差

OpenCVでは次の型だが、おおむねCV_8U=np.uint8を使う。

範囲
CV_8U0 - 255
CV_16U0 - 65535
CV_32F0 - 1

また、色変換によって分類の一助になる可能性もあるため、分析をする前に先に確認するのも良い。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import cv2 as cv
import matplotlib.pyplot as plt

# グリッド形式で色空間のチャンネルを表示する関数
def show_color_space_grid(image_path, color_spaces, grid_size=(3, 3), output_filename="color_spaces.png"):
    image = cv.imread(image_path)

    # グリッドを作成
    fig, axs = plt.subplots(grid_size[0], grid_size[1], figsize=(40, 30))

    # 各色空間の変換と表示
    index = 0  # グリッドのインデックス
    for color_space in color_spaces:
        conversion_code = color_space["code"]
        channels = color_space["channels"]
        title = color_space["title"]

        # 指定した色空間に変換
        converted_image = cv.cvtColor(image, conversion_code)

        # 各チャンネルを軸に表示
        for j, channel_name in enumerate(channels):
            row, col = divmod(index, grid_size[1])  # 行と列を計算
            channel_image = converted_image[:, :, j]

            # カラーマップを設定
            cmap = 'gray' # if len(channels) == 1 else None

            # 軸にプロット
            axs[row][col].imshow(channel_image, cmap=cmap)
            axs[row][col].set_title(f"{title} - {channel_name}")
            axs[row][col].axis("off")

            index += 1  

    # ファイルに保存
    plt.tight_layout()
    plt.savefig(output_filename)  
    plt.close() 


# 使用する色空間
color_spaces = [
    {"code": cv.COLOR_BGR2BGRA, "channels": ["B", "G", "R"], "title": "BGR"},
    {"code": cv.COLOR_BGR2HSV, "channels": ["H", "S", "V"], "title": "HSV"},
    {"code": cv.COLOR_BGR2Lab, "channels": ["L", "a", "b"], "title": "Lab"},
    {"code": cv.COLOR_BGR2Luv, "channels": ["L", "u", "v"], "title": "Luv"},
    {"code": cv.COLOR_BGR2HLS, "channels": ["H", "L", "S"], "title": "HLS"},
    {"code": cv.COLOR_BGR2YUV, "channels": ["Y", "U", "V"], "title": "YUV"},
    {"code": cv.COLOR_BGR2XYZ, "channels": ["X", "Y", "Z"], "title": "XYZ"},
    {"code": cv.COLOR_BGR2YCrCb, "channels": ["Y", "Cr", "Cb"], "title": "YCrCb"},
]


image_path = "test.png" 
show_color_space_grid(image_path, color_spaces, grid_size=(8, 3))

入力画像

名刺画像

出力画像

変換後の白黒画像

カラースペースの特徴は以下になる。

カラースペース変換コードチャネル特徴
BGRcv.COLOR_BGR2BGRAB, G, R青、緑、赤の基本的なRGBカラースペース。一般的なカメラで使用される形式。
BGRAcv.COLOR_BGR2BGRAB, G, R, ABGRカラースペースにアルファチャネルを追加。透過性を持つ。
HSVcv.COLOR_BGR2HSVH, S, V色相、彩度、明度のカラースペース。色を直感的に理解しやすい。
Labcv.COLOR_BGR2LabL, a, b光度(L)と2つの色成分(a、b)のカラースペース。人間の視覚に近い。
Luvcv.COLOR_BGR2LuvL, u, v光度(L)と2つの色成分(u、v)のカラースペース。CIE 1976 L*u*v* 標準。
HLScv.COLOR_BGR2HLSH, L, S色相、光度、彩度のカラースペース。HSVと似ているが、光度が異なる。
YUVcv.COLOR_BGR2YUVY, U, V輝度(Y)と2つの色成分(U、V)のカラースペース。ビデオ圧縮に使用される。
XYZcv.COLOR_BGR2XYZX, Y, ZCIE 1931標準色度図のカラースペース。標準色度としてよく使用される。
YCrCbcv.COLOR_BGR2YCrCbY, Cr, Cb輝度(Y)と2つの色成分(Cr、Cb)のカラースペース。映像処理に使用される。

画像の状態

画像が現実の写像になっていない時に使われる用語。

英語日本語対義語
noisedノイズのあるclean
rotated回転したupright
distorted歪んだ(レンズの歪みなどの、不規則な変形)undistorted
skewd傾斜した(透視的効果などによる特定の方向への変形)Aligned(整列した)など
unclearアンクリアclear
blurredぼやけたsharp
superimposedスーパーインポーズされた(重ねられた)isolated

他にも色々あるが、imgaugのtoolのgifが分かりやすい。

aleju/imgaug: Image augmentation for machine learning experiments.

座標系

代表的な座標系

いくつかあるが、次などが頻出する

  • ワールド座標系
  • カメラ座標系
  • ピクセル座標系
    • topleftがx, y = (0, 0)

座標系

絶対座標・相対座標

2次元の画像処理では、ピクセル座標が絶対座標で、ROI内の座標が相対座標。

ROI座標

紙に対する処理

アンチエイリアス

「アンチエイリアス」は描いた線のフチを僅かにボカして、なめらかな線に見えるよう変換するといった、ペン・図形・選択範囲などあらゆる描画ツールに付加できる機能。

アンチエイリアス

deskew

スキャンしたデータの傾きなどを修正する事。

deskew

紙からデータを抜き出す鉄板パターン

特に紙の場合は色が単一でデータがスパースなので、画像処理がやりやすい特徴がある。

  • グレースケール
  • 白黒をインバート
    • 文字が白に、背景が黒に
  • コンター(contour)をとる
    • 文字や図などの輪郭をとる
  • 図形化する

ビット演算子で画像を合算処理する方法。

  • よく使われるのが、白黒でのAND
  • ある2つの処理が合った時に、完全に両方のオペランドで黒(0)じゃないときは、白にするというやり方
  • つまり、2つの処理の両方で通った時のみ、表示するというやり方
1
2
3
4
5
6
>>> 1 | 1
1
>>> 0 & 1
0
>>> 0 & 0
0

カメラ

キャリブレーション

  • カメラが物理的にどう見ているかを理解するために、カメラの内部および外部のパラメータを特定するプロセス
  • カメラのレンズの歪みを表現するパラメータや、カメラのワールド座標系での位置姿勢を推定できる

内部パラメータ・外部パラメータ

  • 内部パラメータ
    • レンズ歪みの具合などを表すパラメータ
  • 外部パラメータ
    • カメラの位置、向き、回転など、カメラが世界に対してどう配置されているかのパラメータ

ROI系

ROIとは

  • ROI: Region of Interest
    • 分かりやすく言うと、興味があるところ
  • Subpixelとも言ったりもする

ROI

Saliency Map(顕著性マップ)

Saliency Mapとは、ある画像を人が見ると、視線はどこに向くか?をピクセル単位で考えたもの。

Saliency map

図:Saliency Mapの例。

  • 左上:画像
  • 右上:実測した視点を赤いXで示したもの
  • 左下:Saliency Map
  • 右下:Saliency Mapをカラーにして画像に重ねたもの

Segmentation

セグメンテーションとは

Image segmentation is the process of partitioning a digital image into multiple segments by grouping together pixel regions with some predefined characteristics. Each of the pixels in a region is similar with respect to some property, such as color, intensity, location, or texture. It is one of the most important image processing tools because it helps us to extract the objects from the image for further analysis. Moreover, the accuracy of the segmentation step determines success and failure in further image analysis.

セグメンテーションの4つの特徴

重要なの4つの特徴

  1. color: 色
  2. intensity: 強度(いわゆるコントラスト)
  3. location: 場所
  4. texture: パターン

セグメンテーションの種類

やりかたは、discontinuity か similarityの強さに応じて2つに大分される。

  1. The discontinuity approach splits an image according to sudden changes in intensities (edges).
  2. The similarity approach splits an image based on similar regions according to predefined criteria

テクニックは次のような感じ。

segmentationテクニック

Watershed(分水界)とは

OpenCVでのWatershed algorithmについて。 イメージはダム。ダムの水を堤防の限界まで埋めるように処理が走る。

watershed

ノイズ除去系

デノイズ

cv2.fastNlMeansDenoisingを使う

デノイズ

Blurryな画像をClearにする方法

目的に応じて使い分ける必要がある。

  • binarizationで分ける
    • binarizationできるならそれでOK
  • blurでノイズを消す
    • GaussianBlurなどをかけてnoiseを消す
  • 傾きが強いKernelを適用する (Sharpen Filter)
    1
    2
    
    sharpen_filter = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
    sharped_img = cv2.filter2D(image, -1, sharpen_filter)
    
  • Unsharp masking

    画像の微細構造(高周波成分)を強調するために使われる画像処理の技法。原画像をピンぼけ状態にぼかした画像(アンシャープマスク)を原画像から差し引くことで、微細構造が強調される。写真の場合は、アンシャープマスクを原画像とネガポジ反転させて重ねて焼き付ける。マスクのピンぼけ状態の度合いを変えることで、強調される微細構造の細かさを変化させることができる。

  • scikit image のapiをつかう
  • モルフォロジー変換を使う
  • ガウシアンフィルタ
    • 画像を平滑化するのに使う

Unsharp

カーネル系

ConvとPoolの違い

DNNではおおむね次の2つのフィルターをかけるが、数学的には次の違いになる。

  • Conv:
    • 特徴マッチの為の処理
    • invariant function
  • Pool:
    • 位置情報の為の処理
    • variant function

代表的なカーネル

代表的なのは下。

代表的なカーネル

  • Mexican hat or Laplacian filter.
    • エッジ検出や特徴抽出、画像の特定部分を強調するために使われる

Mexican hat filter

  • バイラテラルフィルタ(Bilateral filter)
    • 単純に注目画素の周りにある画素値を平均するのではなく、注目画素の近くにあるものをより重視して反映させようというのが重み付き平均化
    • じゃあその重みの振り分けをどうするかというときに、正規分布に従って振ればいいんじゃないというのがガウシアンフィルタ

Bilateral Filter

  • ノンローカルミーンフィルタ(Non-local Means Filter)
    • バイラテラルフィルタは注目画素の画素値と周辺画素の画素値の差に応じた重みをつけたけど、ノンローカルミーンフィルタはテンプレートマッチングのように周辺画素を含めた領域が、注目画素の周辺領域とどれくらい似通っているかによって重みを決定するフィルター

Bilateral Filter

  • ガボールフィルタ(Gabor filter)
    • ガボール関数はガウス関数と正弦・余弦関数から構成されている関数
    • 任意の周波数成分を抽出するフィルタリング機能を持つ

ガボールフィルタ

  • CNNのフィルタ
    • CNNのConv、Poolなどのカーネルを古典的なアルゴと併用してを使う事もできる
    • ImageNetなどで学習済みのCNNモデルのDNNを使ってベクトルを産出しできる
    • それをPCAなどにかける事で、少数画像で教師あり学習(クラスタリング)をする事も可能

白と黒の意味

  • CVだと、白=1、黒=0となる
  • 0は谷/海となり、1は峰/丘となる
  • 処理でも残したい所=白、消したい所=黒とする
  • グレースケールのときは、黒=0、白=255
  • 0は何かけても0なので、掛け算の処理をしても無効化もできる
  • さらに、ビット変換の際にも役立つ
  • 白黒反転したいときは、インバートする

白い所(180 ~ 255)を消す例。

1
2
3
black_filter = cv2.inRange(sharped_denoised_img_roi, 0, 180) # 0 ~ 180 => white
img_roi_bitwized_grey = black_filter & sharped_denoised_img_roi 
plt.imshow(img_roi_bitwized_grey , cmap="gray")

ベクトル化

ベクトル化(スケルトン化)

次のように情報を縮尺する事。

ベクトル化

グラフ化

  • この場合は、赤い点がnode
  • そして、緑の線がedgeとなる
  • edgeは2つのnode(start, end)をもつ
  • これらの2つの点を結ぶedgeをベクトル化(曲線化)する方法がベジェ曲線

Graph

ベジェ曲線

ベジェ曲線(Bezier Curve)とは、複雑な凹凸を持つ曲線でも、制御点の座標と関数により指定した二点間を結ぶ滑らかな曲線を表現できる手法。

凸包(convex hull)

凸包とは

凸包(convex hull)とは, 与えられた点をすべて包含する最小の凸多角形(凸多面体)のこと。

Convex hull

凸性の欠陥とは?

簡単に言うと、凸性の欠陥とは、手のひらのくぼみと赤線の凸包の間のこと。

凸性の欠陥

凸製の欠陥の例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import cv2
import numpy as np

#img = cv2.imread('sqimage.png')
img = cv2.imread('tstar2.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
_,thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
bw = cv2.bitwise_not(thresh)
contours,hierarchy = cv2.findContours(bw,2,1)
res_img = img // 2 + 128

# 輪郭描画
for cnt in contours:
    cv2.drawContours(res_img,[cnt],0,(255,128,0),2)

for cnt in contours:
    # 凸性の確認
    print('isContourConvex', cv2.isContourConvex(cnt))

    # 凸包(Convex Hull)
    # returnPoints=Trueを指定するとconvex hull上の点の座標
    # False を指定するとconvex hull上の点に対応する
    # 輪郭上の点のインデックスを返す.
    hull = cv2.convexHull(cnt,returnPoints = False)

    # 凸性欠陥情報の取得
    defects = cv2.convexityDefects(cnt,hull)
    if defects is None:
        print('defects is None!')
        continue

    for de in defects:
        #  [ 始点, 終点, 最も遠い点, 最も遠い点までの近似距離] 
        s_idx,e_idx,f_idx,d = de[0,:]   
        start = (*cnt[s_idx][0],)   # 始点
        end = (*cnt[e_idx][0],)     # 終点
        far = (*cnt[f_idx][0],)     # 最も遠い点

        cv2.line(res_img,start,end,(0,255,0),2) # 緑の線分
        cv2.circle(res_img,far,5,[0,0,255],-1) # 赤の点

cv2.imshow('convexityDefects test',res_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

入力と出力

検出

線分検出

ハフ変換

  • 直線や円などの幾何学的形状を画像から検出するための手法
  • 極座標系で点Pを表現し、そのハフ空間の中から選出する手法
  • Canny法でエッジを抽出した後処理することが多い

ハフ変換V

ハフ変換による直線抽出パイプライン

累積空間・パラーメータ空間・ハフ空間

エッジ検出

  • canny法がよく使われる
  • 仕組みはシンプルで次のようになる
    • ガウシアンフィルターでデノイズ
    • Sobelフィルターを使って縦横のエッジを取得
    • その強度(Edge Gradient / Manunitude)とOrientation(arctangentで)を求める
    • その後、ヒステリシスによって連続性があり強度が出たものを線分と判断する

Sobel

エッジの強調

Gradients of 2d Images

ヒステリシス閾値処理

  • DoG(Difference of Gaussian)
    • ガウスフィルタを使って画像を2回平滑化し、その結果を減算する方法
    • 1つは標準偏差が小さく、もう1つは標準偏差が大きいものを用いて、差で顕著化したフィルターを作って処理する
    • ガウスフィルタによる平滑化を経ているため、ノイズに強い特性がある
  • LoG(Laplacian of Gaussian)
    • ガウスフィルタとラプラシアンフィルタを用いていEdgeを強調する手法
    • ラプラシアンフィルターは二階微分を計算する為のフィルタ
    • ただし、ラプラシアンフィルタはノイズに弱いので、ガウスフィルタをかける

DoG

ラプラシアンフィルター

DoGのイメージ

画像位置合わせ

画像位置合わせ(image registration)

  • 画像位置合わせとは、2枚の画像の位置ずれを補正する処理のこと
  • 画像位置合わせは、同じシーンの複数の画像を比較する時などに用いる
  • 例えば、衛星画像解析やオプティカルフロー、医用画像の分野でよく登場する

画像位置合わせ

画像位置合わせの例

テンプレートの特徴を取って、キーポイントとして、マッチングしてワープする処理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import sys

import cv2 as cv
import numpy as np

template_path = sys.argv[1]
target_path = sys.argv[2]
out_path = sys.argv[3] if len(sys.argv) > 3 else "data/out/"

template_img = cv.imread(template_path, cv.IMREAD_GRAYSCALE)
target_img = cv.imread(target_path, cv.IMREAD_GRAYSCALE)

akaze = cv.AKAZE_create()
float_kp, float_des = akaze.detectAndCompute(target_img, None)
ref_kp, ref_des = akaze.detectAndCompute(template_img, None)

bf = cv.BFMatcher()
matches = bf.knnMatch(float_des, ref_des, k=2)

good_matches = []
for m, n in matches:
    if m.distance < 0.75 * n.distance:
        good_matches.append([m])

# 適切なキーポイントを選択
ref_matched_kpts = np.float32([float_kp[m[0].queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
sensed_matched_kpts = np.float32([ref_kp[m[0].trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)

# ホモグラフィを計算
H, status = cv.findHomography(ref_matched_kpts, sensed_matched_kpts, cv.RANSAC, 5.0)

# 画像を変換
warped_image = cv.warpPerspective(target_img, H, (target_img.shape[1], target_img.shape[0]))

def write_grid(img):
    y_step = 50
    x_step = 50
    img_y, img_x = img.shape[:2]
    # 横線を引く:y_stepからimg_yの手前までy_stepおきに白い(BGRすべて255)横線を引く
    img[y_step:img_y:y_step, :] = 0
    # 縦線を引く:x_stepからimg_xの手前までx_stepおきに白い(BGRすべて255)縦線を引く
    img[:, x_step:img_x:x_step] = 0
    return img

warped_image = write_grid(warped_image)
template_img = write_grid(template_img)
cv.imwrite(f"{out_path}/template.png", template_img)
cv.imwrite(f"{out_path}/warped.png", warped_image)

キーポイント マッチング ワープ

特徴量マッチング

特徴量の出し方

特徴量マッチングとは?

  • 2つのオブジェクトで特徴量を抽出し、マッチングをする事
  • 一番有名なのはテンプレートマッチング

一般的に、下のようなパイプラインになる。

  1. 2つの画像を用意する
  2. 特徴量detectorの処理
    1. まず、特徴点を出す
    2. その特徴点の特徴量を出す
      1. SIFTの場合は128次元
  3. 特徴量のmatcherの処理
    1. 2つの特徴量を変換する
      1. KDTree (木構造から検索)
      2. ブルーフォース (全部を検索)
    2. 検索する手法
      1. KNN法 (K個取得する)
    3. マッチのThresholding化
      1. 2つの特徴量が1以下など

特徴量

大域特徴量(global feature)とは

  • 大域特徴量とは,画像全体から抽出される特徴量
  • カラーヒストグラムなど

局所特徴量(local feature)とは?

マッチング手法

FLANN based Matcher

  • FLANN: Fast Library for Approximate Nearest Neighborsの意味
    • 特徴は名前の通り、approximateのNNを取り、BFMatcherより早く動く
    • フロー
      • Kd treeの構造を先にIndexとして作っておく。
      • そこからKNNかFixed-Round KNNで木構造から検索する。(木の探索)
        • KNNの場合は、近くのK個
        • Fixed-Round KNNの場合は半径内にある全部
      • とれたそれらのdiscriptorの距離を計算してあるパラーメター内の近さならそれをマッチした特徴としている
  • BFMatcher
    • 全部検索する
    • なので、サブピクセルの特徴量が100で、基の特徴量が100とかの場合は、1万回もチェックする必要がある
FLANNの実装例

radius near neighborsとは?

  • KNNの亜種。予め半径を決めておき、その範囲内をnear neibhbourにする
  • K-nearestのestにならない理由は、何個かわからないから

Redius near neighbors

K-nearest neighbor searchとは?

  • K個近くのデータを撮ってくる。それを基にデータを決める
  • つまり、類は友を呼ぶ

KDTreeアルゴリズムとは?

  • 決定木で作る
  • 決定木の深さ=領域の個数=n^2
  • バイナリツリーみたいな感じ
  • KNNで近傍を探すのに、木構造を利用して探す

see Kd-treeと最近傍探索 適応システム特論・担当三上

KNNとFixed-Radius KNNの違いとは?

KNN vs. Fixed-Radius KNN

see https://ai-master.gitbooks.io/knn/content/simple-extensions.html

ブルーフォース法

Brute-Force matcher is simple. It takes the descriptor of one feature in first set and is matched with all other features in second set using some distance calculation. And the closest one is returned.

ブルートフォースマッチャーはシンプルで、最初のセットの1つの特徴の記述子を取り、距離計算を使用して2番目のセットの他のすべての特徴と照合する。そして、最も近いものが返される。これを繰り返す

ブルートフォースマッチャー

特徴量の比較

画像特徴量の変遷

局所特徴量

特徴量の出し方

Feature DetectorとFeature descriptorの違いは?

次のようにまとめられる。

アルゴinputoutputs特徴example
feature detectorimagelocations (場所)特徴については教えてくれないcorner detector
feature descriptorimagefeature descriptors / feature vectors特徴について教えてくれる。scale invariantなことが多い。SHIFT, HOG, SURF, KAZE, AKAZE

HOG特徴量とは?

  • 勾配ベースの特徴量
  • 白黒はっきりしていると取りやすい
  • SVMを使ったりする

Haar like特徴量

  • 隣接する2つの矩形の明るさの違い
  • OpenCVだと顔認識に使われている

FAST algorithmとは?

binary string based

  • BRIEF アルゴリズムとは?
    • BRIEF (Binary Robust Independent Elementary Features, 二値頑健独立基本特徴)
  • BRISKアルゴリズム
    • Binary Robust Invariant Scalable Keypoints (BRISK) アルゴリズム
    • BRIEFを発展させた以下のような方法で、スケール不変性と回転不変性を得ているのがBRISKである
  • ORB Detector(Oriented FAST and rotated BRIEF)
    • OrientedFASTおよびrotatedBRIEFは、Ethan Rubleeetalによって最初に発表された高速で堅牢なローカル機能検出器

BRISKアルゴ

変換

距離変換

距離変換 (distance transform) とは、2値画像を入力としたとき、各画素の最も近い画素値0までの距離を計算した 距離マップ (distance map) を作成する処理。
dst = cv2.distanceTransform(src, distanceType, maskSize[, dst[, dstType]])で行う。

距離返還

二値化

Otsu法

  • ある画像を2値化(ピクセル値を255(最大値)or 0(最小値)にする = バイナリー化 = ざっくり言うなら白黒化)にする場合、その閾値を決定する必要がある
  • Otsu法は画像の輝度ヒストグラムを元に、いい感じの閾値を自動で決定してくれるアルゴリズム
  • ただし、ヒストグラムが大きく二つに別れるところをバッツリと二つに分ける(=閾値を決定する)アルゴリズムなので、そういった双峰性を持たないヒストグラムに対してはうまく閾値を決定できないらしい

大津の二値化

pタイル法

Pタイル法(Percentile Method)は、2値化したい領域が全データの領域に占める割合をパーセント(%)で指定し2値化する手法。

Multilevel Thresholding

Otsuの二値化と違い、複数のThreasholdでクラスタリングできる。

multilevel thresholding

adaptive thresholding

  • 画像中の小領域ごとにしきい値の値を計算する
  • そのため領域ごとに光源環境が変わってしまうような画像に対して、単純なしきい値処理より良い結果が得られる

Adaptive Thresholding

モルフォロジー変換

モルフォロジー変換とは?

  • 与えられた2値画像または濃淡画像からの特徴抽出を目的とした図形変形手法の理論体系
  • 画像のノイズ除去,平滑化,形状記述,テクスチャ解析,細線化処理などに用いられる.
  • Dilation, Erosion, Opening, Closingの基本演算と、
    • モルフォロジーグラジエント(Dailationした画像からErosionした画像を減算することで,エッジを検出する)
    • トップハット変換(元画像からOpeningした画像を減算する)
    • ブラックハット変換(Closingした画像から元画像を減算する) の7種類
  • 要は、以下の画像のように、対象画像を太くしたり補足したり、それらを組み合わせたりして目的のオブジェクトを取り出す方法(画像は上記リンクより拝借)

モルフォロジー変換

ある特徴的なオブジェクトを検出するのに使える。
see Hands-on Morphological Image Processing

モルフォロジ処理の具体的な処理とは?

  • モルフォロジ処理は、“構造要素"と呼ばれる画像を移動させる要素と,“ミンコフスキー(Minkowski)和・ミンコフスキー差” と呼ばれる演算から成り立っている
  • モルフォロジー処理における代表的な処理は、「Erosion(エロージョン)」と「Dilation(ダイレーション)」と呼ばれる処理
  • それぞれ孤立点の除去,不連続な点の接続と穴埋めのために利用される
  • Erosionで細くしてノイズを減らして、Dilationで大きくして元の大きさに戻すイメージ

モルフォロジー変換の具体例

  • opening
    • erosionの後にdilationなので、ノイズが消える
  • closing
    • dilationのあとにerosionなので、ノイズが増える

ミンコフスキー和と差

  • ミンコフスキー和・差は、二つのベクトルの集合同士を使って新しいベクトルの集合を求める.
  • このミンコフスキー和と差を用いることで、ゲームにおける衝突判定やロボットのモーションプログラミングで計算を簡単にできる.

ミンコフスキー差と和

ワープ処理 (Image warping)

ワープ処理

幾何学的変換(Geometric Transformation)

  • 幾何学的変換は集合の何らかの幾何学的な構造を持つ自身への(もしくは幾何学的な構造を持つ相異なる集合への)全単射
  • アフィン変換も幾何学的変換の一つ

alt text

統計的にノイズを消す方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
im = cv.imread("green-blurred.jpg")
yuv = cv.cvtColor(im, cv.COLOR_BGR2YCrCb) # or COLOR_BGR2YUV, should work equivalently here
#uv = yuv[:,:,(2,3)]
mean = np.mean(yuv, axis=(0,1))
std = np.std(yuv, axis=(0,1))
#mask = ((yuv - mean) / std >= 4.5).any(axis=2)
# EDIT: there needs to be an abs() on that difference, we want magnitudes, no signs
mask = (np.abs(yuv - mean) / std >= 4.5).any(axis=2)
mask_u8 = mask.astype(np.uint8) * 255
#grayscale = (np.abs(yuv - mean) / std / 5).max(axis=2)
cv.imshow("mask", mask_u8)
cv.waitKey(-1)

統計的なノイズ除去

画素補間法(image interpolation)

  • nearest neighbor interpolation
  • 最近傍補間法とも
  • コンピューターによる画像処理で、画像の回転・拡大・変形を行うときの画素補間法の一つ
  • 求めたい画素に最も近い位置の画素の情報を参照して補間する

  • バイキュービック法

    • コンピューターによる画像処理で、画像の回転・拡大・変形を行うときの画素補間法の一つ
    • 求めたい画素の周辺の4×4画素(16画素)の輝度値を参照し、三次式で近似して補間する
    • 濃淡や色の急激な変化を再現するのに適するが、計算に時間を要する
  • バイリニア法(bilinear interpolation)

    • コンピューターによる画像処理で、画像の回転・拡大・変形を行うときの画素補間法の一つ
    • 求めたい画素の周辺の2×2画素(4画素)の輝度値を参照し、その加重平均値を用いて補間する

    バイリニア補完

距離

分布の距離

2つの分布間の距離を出すKL距離などは厳密には距離ではないが、距離と言う。

KL距離

一般的な距離

  • マンハッタン距離
  • ユークリッド距離
  • マハラノビス距離
  • チェビシェフ距離

距離の比較

Circle, Curve, Arc, Fircleの違い

  • Circle: 円
  • Curve: 曲線
  • Arc: 円弧
  • Fircle: 手書きの円

メモ

  • トポロジー的には、Arcは図形では、凸
  • Circle -> D Cut -> Squaireのように相が変換する

RANSAC algorithmでArcフィッティング

The RANSAC (Random sample and consensus) algorithm is the gold standard in eliminating noise.

アルゴのフロー

  • データをランダムにサンプリングする.(非復元抽出)
  • モデルを学習する
  • 全てのデータに対して誤差を計算する
  • 3で求めた誤差から正常値と外れ値を決める
  • 正常値のみでモデルの性能を評価する
  • 既定回数に達したら終了.そうでなければ1に戻る

ようは、アンサンブル学習のbaggingをしている。

RANSACとGradient descent algoの違い。

RANSAC vs Gradient Descent Algo

ゼルニケ多項式 (Zernike Polynomials)とは?

曲面を多項式で表現する手法の1つとして、光学設計ではZernike(ツェルニケ)多項式がよく使われる。詳細は以下のスライド。

円形度

確か、円形度はISOで定義があったはず。

$$ 円形度 = \frac{4π \times S}{L^2} $$

(S = 面積, L = 図形の周囲長)

その例。

cv2で円形度。

1
2
3
4
5
6
7
8
9
import cv2
import numpy as np
# contourは輪郭点のリスト cv2.findContours
# areaは図形の面積 cv2.contourArea

def calcCircleLevel (contour, area):
    perimeter = cv2.arcLength(contour, True)
    circle_level = 4.0 * np.pi * area / (perimeter * perimeter); # perimeter = 0 のとき気をつける
    return circle_level

円摩度 (アスペクト比で補正した円形度)

see http://progearthplanetsci.org/highlights_j/072.html

New parameter of roundness R: Circularity corrected by aspect ratio.

$$ R = Circularity + (Circularity_{perfect\_circle} - Circularity_{aspect\_ratio}) $$

ここで、

  • $Circularity_{perfect\_circle}$
    • 真円の円形度
  • $Circularity_{aspect\_ratio}$
    • 真円のアスペクト比のみを変化させたときに得られる円形度

円磨度 (psephicity) vs. roundness

  • 円磨度 (psephicity)
    • 礫(れき、小さい石のこと)または砂粒の円味を帯びる程度。
    • 通常は構成岩石の比重と硬度との比をもって円磨率(coefficient of psephicity)という。
  • rounhdness
    • 砕屑物が川や海によって運搬される過程で破壊され摩滅によって次第に角や稜がとれて円い礫となる

円磨度の区分(Pettijohn : 1957)

  • 角(angular,円磨度 : 0~0.15)
  • 亜角(subangular,円磨度 : 0.15~0.25)
  • 亜円(aubrounded,円磨度 : 0.25~0.40)
  • 円(rounded,円磨度 : 0.40~0.60)
  • 十分に円磨(well-rounded,円磨度 : 0.60~1)

楕円の歪み度

Major AxisとMinor Axisの比を取ればOK。

モーメント

Huモーメントは次の3つに対して不変な特徴を持つ。

  • 平行移動 (translation)
  • スケール (scale, size)
  • 回転 (rotation)

モーメント (素モーメント)

2変数関数$f(x,y)$に対して、$(p+q)$次のモーメント(素モーメント)を次のように定義する。 pとqはそれぞれモーメントの次数。

$$ {M_{pq} = \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} x^p y^q f(x,y) dx dy } $$

これをグレースケールの画像に適用すると

$$ {M_{ij} = \sum_{x} \sum_{y} x^i y^j I(x,y)} $$

つまり、$x座標 \times y座標 \times ピクセルの値$の合計値となる。 素モーメントそのものは不変量ではない。

$$ {\begin{align} M_{00} &= 面積, \\ M_{01} &= Y軸座標値とピクセル値の面積=Y軸座標値の期待値, \\ M_{10} &= X軸座標値とピクセル値の面積=X軸座標値の期待値, \\ M_{11} &= XY軸座標値とピクセル値の体積=XとY軸座標の面積, \\ M_{20} &= Y軸座標値^2とピクセル値の面積=Y軸座標^2の期待値, \\ M_{02} &= X軸座標値^2とピクセル値の面積=X軸座標^2の期待値, \\ \end{align}} $$

  • $M_{00}$の解釈
    • すると、$M_{00}$などは、面積と捉えられる。
    • また、ピクセル値を確率と捉えると、面積ではなく、確率和と理解できる。
  • $M_{01}$ の解釈
    • $M_{01}$はY軸座標値とピクセル値の掛け算=その面積。
    • ただし、ピクセル値の値域[0,1]なので、確率と捉えると、
    • Y座標値の期待値の合計=期待値和 $$ 期待値=確率 \times 確率変数 $$
    • 合計なので、単位は$Count \times Expected Value$
  • $M_{11}$の解釈
    • $M_{11}$はXY軸座標値とピクセル値の掛け算=その体積。
    • ただし、ピクセル値の値域[0,1]なので、確率と捉えると、
    • Y座標値の期待値とX軸座標値の期待値の積の合計=期待値積の和=混合分布の和
  • $M_{02}$の解釈
    • $M_{02}$はY軸座標値^2とピクセル値の掛け算=その体積。
    • ただし、ピクセル値の値域[0,1]なので、確率と捉えると、
    • Y座標値^2の期待値の合計=期待値平方和

中心モーメント(並行不変性)

中心モーメントは平行普遍性をもつ。 なぜこの中心モーメントが並行不変性をもつかというと、ピクセル座標の原点を物体の中心(重心)にしているから。 なので、物体がどこにあろうが、同じ値を出す=並行不変値

$$ {\mu_{ij} = \sum_{x} \sum_{y} (x-\bar{x})^i (y-\bar{y})^j I(x,y) } $$

ただし、$\bar{x},\bar{y}$は先に示した重心を示す。

この$\mu$のパターンを定義すると以下。

$$ {\begin{align} \mu_{00} &= M_{{00}}, \\ \mu_{01} &= 0, \\ \mu_{10} &= 0, \\ \mu_{11} &= M_{{11}}-{\bar {x}}M_{{01}}=M_{{11}}-{\bar {y}}M_{{10}} , \\ \mu_{20} &= M_{20}-{\bar {x}}M_{10}, \\ \mu_{02} &= M_{02}-{\bar {y}}M_{01}, \\ \end{align}} $$

  • $\mu_{01}$の解釈
    • $\mu_{01}=0$の理由は$\bar{y}$がY軸の重心ピクセル座標であり、
    • $y-\bar{y}$の値は+と-で分布して合計すると必ず0になるから。
    • ※ 重心はピクセルのブロブの原点。
  • $\mu_{11}$の解釈
    • つまり、あるピクセルの値(確率)とそのピクセルの座標の原点からの距離を加味した指標
  • $\mu_{20}$の解釈
    • 重心から座標がどれだけ離れているか、ただし、二乗しているので、絶対値。
    • つまりは、重心からどれだけはなれているかを加味した値。

スケール不変性

次のような値を考えることで、スケールに対する不変量を作ることができる。 このモーメントは$i+j≥2$のとき並進不変性を持つ。 なぜ、スケール普遍化というと、分母に面積を撮っているから。つまり、単位面積あたりの値にしている。

$$ {\eta _{{ij}}={\frac {\mu _{{ij}}}{\mu _{{00}}^{{\left(1+{\frac {i+j}{2}}\right)}}}} } $$

  • $i+j<2$の理由
    • 上の定義を代入してみるとわかるが、$i+j<2$のときは定数になってしまい意味を持たない。
    • なぜなら、下のように、$\mu$のijの合計が2未満のときは、分母を加味して定数だから。 $$ {\begin{align} \mu_{00}=面積 \\ \mu_{10}=0 \\ \mu_{01}=0 \\ \end{align}} $$
  • $\eta_{11}$の解釈 $$ {\begin{align} i=1 \\ j=1 \\ \eta_{ij}=\frac{\mu_{11}}{\mu_{00}^2} \end{align}} $$
  • $\eta_{20}$の解釈 $$ {\begin{align} i=2 \\ j=0 \\ \eta_{ij}=\frac{\mu_{20}}{\mu_{00}^2} \end{align}} $$

回転不変性

  • 更に$\eta{ij}$を用いて、回転に対して不変な量を構築することが出来ます。これはHuによって発見されたため、Huモーメント不変量と呼ばれている
  • 1から6までは回転不変。7はskew invariant

$$ I_{1} = \eta _{{20}}+\eta _{{02}} \\ $$

$$ I_{2} = (\eta _{{20}}-\eta _{{02}}) ^{2}+4 \eta _{{11}}^{2} \\ $$

$$ I_{2} = (\eta _{{20}}-\eta _{{02}})^{2}+4\eta _{{11}}^{2} \\ $$

$$ I_{3} = (\eta _{{30}}-3\eta _{{12}})^{2}+(3\eta _{{21}}-\eta _{{03}})^{2} \\ $$

$$ I_{4} = (\eta _{{30}}+\eta _{{12}})^{2}+(\eta _{{21}}+\eta _{{03}})^{2} \\ $$

$$ I_{5} = (\eta _{{30}}-3\eta _{{12}})(\eta _{{30}}+\eta _{{12}})[(\eta _{{30}}+\eta _{{12}})^{2}-3(\eta _{{21}}+\eta _{{03}})^{2}]+(3\eta _{{21}}-\eta _{{03}})(\eta _{{21}}+\eta _{{03}})[3(\eta _{{30}}+\eta _{{12}})^{2}-(\eta _{{21}}+\eta _{{03}})^{2}] \\ $$

$$ I_{6} = (\eta _{{20}}-\eta _{{02}})[(\eta _{{30}}+\eta _{{12}})^{2}-(\eta _{{21}}+\eta _{{03}})^{2}]+4\eta _{{11}}(\eta _{{30}}+\eta _{{12}})(\eta _{{21}}+\eta _{{03}}) \\ $$

$$ I_{7} =(3\eta _{{21}}-\eta _{{03}})(\eta _{{30}}+\eta _{{12}})[(\eta _{{30}}+\eta _{{12}})^{2}-3(\eta _{{21}}+\eta _{{03}})^{2}]-(\eta _{{30}}-3\eta _{{12}})(\eta _{{21}}+\eta _{{03}})[3(\eta _{{30}}+\eta _{{12}})^{2}-(\eta _{{21}}+\eta _{{03}})^{2}]. \\ $$

see https://stackoverflow.com/questions/11379947/meaning-of-the-seven-hu-invariant-moments-function-from-opencv

Humomentの例

シンプルなHumomentによる類似画像検索。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import cv2
from math import log

def hudist(f1 , f2):
   def h2m(x):
       if x==0:
          return 0.0
       elif x>0:
          return 1.0/log(x)
       else:
          return -1.0/log(-x)
   def hum(f):
       img = cv2.imread(f , cv2.CV_LOAD_IMAGE_GRAYSCALE)
       assert(img!=None),("failed to open %s" % f)
       moments = cv2.moments(img)
       return cv2.HuMoments(moments)
   hu = hum(f1)
   hu2 = hum(f2)
   d = sum([abs(h2m(x1)-h2m(x2)) for (x1,x2) in zip(hu.T[0],hu2.T[0])])
   return d

if __name__=="__main__":
   imgs = ['Lenna.png','Lena3.jpg','sendo1.jpg','Sendou2.jpg','monnalisa.jpg','apple.jpg','Red_Apple.jpg']
   for f1 in imgs:
       print f1,
       for f2 in imgs:
           v = 100*hudist(f1,f2)
           print ("%5.2f" % v),
       print ""

元画像

Lenna.pngLena3.jpgsendo1.jpgSendou2.jpgmonnalisa.jpgapple.jpgRed_Apple.jpg
Lenna.png0.000.045.935.874.192.43
Lena3.jpg0.040.005.935.864.222.41
sendo1.jpg5.935.930.000.097.824.55
Sendou2.jpg5.875.860.090.007.864.47
monnalisa.jpg4.194.227.827.860.003.97
apple.jpg2.432.414.554.473.970.00
Red_Apple.jpg4.834.831.191.117.884.94

画像のモーメントと面積と重心

モーメントとは?

$$ M_{nm} = \sum^{画像全体} (x座標^{n} \times y座標^{m} \times 画素値) $$

画像中の全ピクセルについての総和。

モーメントのフロー

モーメントの計算方法は2つのフローに従う。

  1. ピクセルずつデータを取る
  2. 座標ごとのべき乗をもとめて足す

まず、データ(x座標、y座標、画素値)を調べる

次に、全てのピクセルについて次を求める。

  • x座標のn乗
  • y座標のm乗

さらに、上記の2つと画素値(0または1)を掛け合わせる。
例えば、(15,17,1)のピクセルについて、n=2、m=0の場合は次となる。

$$ M_{2,0} = 15^{2} \times 17^{0} \times 1 = 225 \times 1 \times 1 = 225 $$

このピクセルでの計算結果は225となる。

全てのピクセルでの計算結果を足し合わせたものが画像のモーメント$M_{nm}$。

  • $M_{0,0}$が面積になる理由

    • 白ピクセル(1)の場合 $$ M_{0,0} = 15^{0} \times 17^{0} \times 1 = 1 \times 1 \times 1 = 1 $$
    • 黒ピクセル(0)の場合 $$ M_{0,0} = 15^{0} \times 17^{0} \times 0 = 1 \times 1 \times 0 = 0 $$
    • つまり、白のピクセルの個数=面積がもとまるから。
  • $(\frac{M_{10}}{M_{00}}, \frac{M_{01}}{M_{00}})$で重心になる理由

    • Xを考えると、
    • 白ピクセル(1)の場合 $$ M_{1,0} = 15^{1} \times 17^{0} \times 1 = 15 \times 1 \times 1 = 15 $$
    • 黒ピクセルの場合 $$ M_{0,0} = 0 $$
    • 分母は次になる $$ M_{0,0} = 面積 $$

単純化すると次になる。

  • 画像なので座標はピクセル座標
  • (0,0), (2,2)の範囲のピクセルを考える
  • つまり、範囲はグリッド座標でいう、(0,0) から (3,3)までのブロブ
  • 面積は9
    • $M_{0,0}$ = 9
    • $M_{1,0}$= 0+1+2+0+1+2+0+1+2 = 9 (左上から横方向にチェック)
    • $\frac{M_{10}}{M_{00}}$=9/9=1

つまり、X座標では1の座標をもつピクセルが重心。 また、重心=ブロブの原点。 つまり、重心を原点ととると、全てのx座標の合計、全てのy座標の合計のそれぞれが0となる。

Contour(輪郭)

contourとperimeterの違い

  • perimeter: 周囲
  • contour: 輪郭

Contourのフロー

  1. グレースケールに変換
  2. 画像を2値化する
  3. 輪郭の取得
  4. 輪郭の分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import cv2
import numpy as np
import matplotlib.pyplot as plt
from skimage import sio

url = 'https://upload.wikimedia.org/wikipedia/commons/5/53/OpenCV_Logo_with_text.png'
img = sio.imread(url)

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)

contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

img_blank = np.ones_like(img) * 255
img_contour_only = cv2.drawContours(img_blank, contours, -1, (0,0,0), 3)

plt.imshow(cv2.cvtColor(img_contour_only, cv2.COLOR_BGR2RGB))
plt.show()

Contourから取れるモノ

  • 面積(cv2.contourArea
  • サイズ(cv2.boundingRect
  • 点の数
  • 領域を囲む周囲長(cv2.arcLength
  • 領域を塗りつぶし(cv2.drawContours
  • 少ない座標で輪郭(cv2.approxPolyDP
  • 輪郭の近似による輪郭の取得(cv2.approxPolyDP(cnt,0.1*cv2.arcLength(cnt,True),True)
  • 凸包による図形判別(cv2.convexHull
  • 外接矩形による角度取得(cv2.minAreaRect
  • 輪郭の類似度の計算(cv2.matchShapes
  • 重複したcontoursのマージ(NMS:Non-Maximum Suppression的な
  • 隣接するcontoursのマージ(階層クラスタリング

findContoursの引数

  • binaryデータを渡して、その中のオブジェクトを取得するメソッド
  • 簡易的物体検知であり、以外に複雑な構造をしているのでメモ
  • 手法(mode)+輪郭取り方(method)でパターンが分かる

入力画像

NOTE: 外の白枠は分かりやすさでついているもので、実際はない。

findCountoursのmode

RETR_TREE: 輪郭の階層情報をツリー形式で取得する。

RETR_TREEの結果

RETR_TREEの構造

RETR_CCOMP:白の輪郭の外側と内側の輪郭を構造化する

RETR_CCOMPの結果

RETR_CCOMPの構造

RETR_LIST:階層なし

RETR_LISTの結果

RETR_LISTの構造

RETR_EXTERNAL:外側のみ

RETR_EXTERNALの結果

RETR_EXTERNALの構造

findCountoursのmethod

CHAIN_APPROX_NONE:輪郭上のすべての座標を取得

CHAIN_APPROX_NONEの例

CHAIN_APPROX_SIMPLE:縦、横、斜め45°方向に完全に直線の部分の輪郭の点を省略

CHAIN_APPROX_SIMPLEの例

CHAIN_APPROX_TC89_L1, CHAIN_APPROX_TC89_KCOS:輪郭の座標を直線で近似できる部分の輪郭の点を省略

CHAIN_APPROX_TC89_L1, CHAIN_APPROX_TC89_KCOSの例

findContoursの戻り値

methodの設定により輪郭番号があったりなかったりするが、大体contoursは次の構造を取る。

1
contours[輪郭番号][点の番号][0][X座標, Y座標]

minとmaxで4点のcontourを作る例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import numpy as np

def create_contour_from_bbox(minx, miny, maxx, maxy):
    contour = np.array([
        [[minx, miny]],
        [[minx, maxy]],
        [[maxx, maxy]],
        [[maxx, miny]]
    ])
    return [contour]

# 使用例
minx, miny, maxx, maxy = 100, 50, 200, 150
contour = create_contour_from_bbox(minx, miny, maxx, maxy)

hierarchyは次の構造を取る。

1
2
3
4
hierarchy[0][輪郭番号][次の輪郭番号, 前の輪郭番号, 最初子供(内側)の輪郭番号, 親(外側)の輪郭番号]

NOTE: 
- 該当する輪郭番号が無い時は-1 

階層構造の例

上の場合は下になる。
分かりやすく言うと、階層的には、下、上、右、左の順番。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
hierarchy[0][0] は [ 3, -1,  1, -1]
hierarchy[0][1] は [-1, -1,  2,  0]
hierarchy[0][2] は [-1, -1, -1,  1]
hierarchy[0][3] は [-1,  0,  4, -1]
hierarchy[0][4] は [-1, -1,  5,  3]
hierarchy[0][5] は [ 6, -1, -1,  4]
hierarchy[0][6] は [-1,  5,  7,  4]
hierarchy[0][7] は [-1, -1,  8,  6]
hierarchy[0][8] は [ 9, -1, -1,  7]
hierarchy[0][9] は [-1,  8, -1,  7]

Floodfill

  • 他の物体検知手法としては、Floodfillもある
  • seedPoint に指定した画素と画素値の差が指定範囲内の画素を同じグループ (連結成分) と見なして塗りつぶしを行う
  • loDiff(下限)とupDiff(上限)をつけて塗りつぶすアルゴ

floodfill

オプティカルフロー

  • 物体の移動軌跡を行うのに使う手法
  • 動画像中のフレームで、移動体上の各点の移動方向と移動距離を表す量である動きベクトルを計算する
  • 物体トラッキングでは、カルマンフィルタを使って予測することも行う

オプティカルフロー

ガイド

Scikit learn ガイド

有名なScikit-leranのガイド。

ML Map

Dlib選択ガイド

こちらは一部で有名なDlibガイド。

Dlib Guide

スニペット

簡易的なログ残し

1
2
3
4
5
6
7
8
9
import codecs
from datetime import datetime, timedelta, timezone

JST = timezone(timedelta(hours=+9), 'JST')

def print_log(message):
    now = datetime.now(JST)
    print(f"{now}: {message}")
    print(f"{now}: {message}", file=codecs.open('log.txt', 'a', 'utf-8'))

シードの設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import os
import random

import numpy as np
import torch
from torch.backends import cudnn


def set_seed(seed=42):
    os.environ["PYTHONHASHSEED"] = str(seed)
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed(seed)
    cudnn.benchmark=True
    cudnn.deterministic = True

画像の変換

基本はPillow形式だが、numpyやtorchの形式を相互に変換する必要がある。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import math
from typing import Union

import cv2
import numpy as np
import torch
from PIL import Image
from skimage.metrics import structural_similarity
from torchvision import transforms


class CvUtil():
    @staticmethod
    def cv_bgr_to_cv_rgb(img_bgr: np.ndarray) -> np.ndarray:
        return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)

    @staticmethod
    def cv_bgr_to_cv_gray(img_bgr: np.ndarray) -> np.ndarray:
        return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)

    @classmethod
    def cv_bgr_to_pil(cls, img_bgr: np.ndarray) -> Image.Image:
        """
        ref https://discuss.pytorch.org/t/how-to-transform-images-to-tensors/71447
        """
        img_rgb = cls.cv_bgr_to_cv_rgb(img_bgr)
        return Image.fromarray(img_rgb).convert('RGB')

    @staticmethod
    def cv_to_pil(image: np.ndarray) -> Image.Image:
        '''
        OpenCV型 -> PIL型
        NOTE: cv2のはBGR前提
        ref: https://qiita.com/derodero24/items/f22c22b22451609908ee
        '''
        new_image = image.copy()
        if new_image.ndim == 2:  # モノクロ
            pass
        elif new_image.shape[2] == 3:  # カラー
            new_image = cv2.cvtColor(new_image, cv2.COLOR_BGR2RGB)
        elif new_image.shape[2] == 4:  # 透過
            new_image = cv2.cvtColor(new_image, cv2.COLOR_BGRA2RGBA)
        new_image = Image.fromarray(new_image)
        return new_image


class PilUtil():
    @staticmethod
    def pil_to_normalized_tensor(img_pil: Image.Image) -> torch.Tensor:
        # MNISTは ToTensor() すると [0, 1] になるので0.5を引いて0.5で割って [-1, 1] の範囲に変換する。
        normalize = transforms.Normalize(
            mean=[0.5],
            std=[0.5]
        )  # z = (x - mean) / std

        img_transform = transforms.Compose([
            transforms.Resize((256, 256)),
            transforms.Grayscale(),
            transforms.ToTensor(),  # [0, 255] => [0, 1]
            # normalize, # [0, 1] => [-1, 1]
        ])
        return img_transform(img_pil)

    @staticmethod
    def pil_to_tensor(img_pil: Image.Image) -> torch.Tensor:
        """
        ref https://discuss.pytorch.org/t/pytorch-pil-to-tensor-and-vice-versa/6312/9
        """
        return transforms.ToTensor()(img_pil).unsqueeze_(0)

    @staticmethod
    def pil_to_pil_gray(img_pil: Image.Image) -> Image.Image:
        """
        グレースケール化
        """
        return img_pil.convert('L')

    @staticmethod
    def pil_to_pil_bw(img_pil: Image.Image) -> Image.Image:
        """
        2値化
        """
        return img_pil.convert('1')

    @staticmethod
    def pil_to_cv(image: Image.Image) -> np.ndarray:
        '''
        PIL型 -> OpenCV型
        NOTE: cv2のはBGRになる
        '''
        new_image = np.array(image, dtype=np.uint8)
        if new_image.ndim == 2:  # モノクロ
            pass
        elif new_image.shape[2] == 3:  # カラー
            new_image = cv2.cvtColor(new_image, cv2.COLOR_RGB2BGR)
        elif new_image.shape[2] == 4:  # 透過
            new_image = cv2.cvtColor(new_image, cv2.COLOR_RGBA2BGRA)
        return new_image


class TensorUtil():
    @staticmethod
    def tensor_to_pil(img_tensor: torch.Tensor) -> Image.Image:
        """
        ref https://discuss.pytorch.org/t/pytorch-pil-to-tensor-and-vice-versa/6312/9
        """
        return transforms.ToPILImage()(img_tensor.squeeze_(0))

    @staticmethod
    def np_2d_to_pil_bw_img(np_2d: np.ndarray) -> Image.Image:
        """
        ref: https://stackoverflow.com/questions/37558523/converting-2d-numpy-array-of-grayscale-values-to-a-pil-image
        """
        return Image.fromarray(np.uint8(np_2d * 255), 'L')

    @staticmethod
    def tensor_2d_to_pil_bw_img(tensor_2_axis: torch.Tensor) -> Image.Image:
        """
        2次元のテンソルを白黒のPILの画像に変換
        ref: https://discuss.pytorch.org/t/pytorch-pil-to-tensor-and-vice-versa/6312/8
        """
        tensor_3_axis = TensorUtil.torch_add_dim(tensor_2_axis)
        im = transforms.ToPILImage()(tensor_3_axis).convert("L")
        return im

    @staticmethod
    def tensor_to_np(x_tensor: torch.Tensor) -> np.ndarray:
        # numpyはCPUのメモリを使うので、一旦CPUに落とす
        device = x_tensor.device
        # NOTE: ndarrayの場合はコピーはcopy()
        x_np = x_tensor.to('cpu').detach().numpy().copy()
        x_tensor = x_tensor.to(device)  # テンソルを元にもどス
        return x_np

    @staticmethod
    def tensor_to_np_gray(x_tensor: torch.Tensor) -> np.ndarray:
        """
        TODO: 一つのテンソルのみを受け取るように修正するべき
        tensorを白黒の画像に変換する
        tensorの構造は: (channnel, width, height)
        """
        # x_tensors = 0.5 * (x_tensors + 1)  # [-1,1] => [0, 1]
        x_tensor = x_tensor.clamp(0, 1)  # np.clipと同じ、[0, 1]にデータを収める
        x_tensor = x_tensor.view(1, 256, 256)
        img_gray = x_tensor.cpu().numpy()
        return img_gray

    @staticmethod
    def torch_remove_dim(x_tensor: torch.Tensor) -> torch.Tensor:
        """
        tensorの次元削除。データローダーのテンソル -> 画像化の時によく使う
        """
        return x_tensor.squeeze()

    @staticmethod
    def torch_add_dim(x_tensor: torch.Tensor) -> torch.Tensor:
        """
        tensorの次元拡張。画像 -> データローダーのデータ化の時によく使う
        """
        return torch.unsqueeze(x_tensor, 0)

    @classmethod
    def torch_create_dummy_2dim(cls, x_tensor: torch.Tensor) -> torch.Tensor:
        """
        x_tensor: [1, x, x]
        バッチノーマライゼーションをモデルで入れている時に使用する。
        1つのtensorだとエラーが出てしまうので、便宜上形式的に同じ画像を入れて回避する。
        ref: https://discuss.pytorch.org/t/appending-to-a-tensor/2665/11
        """
        x1_tensor = cls.torch_add_dim(x_tensor)
        x2_tensor = cls.torch_add_dim(x_tensor)
        return torch.cat([x1_tensor.clone(), x2_tensor.clone()], dim=0)


class NumpyUtil():
    @staticmethod
    def np_to_tensor(x_np: np.ndarray) -> torch.Tensor:
        """
        ref: https://tzmi.hatenablog.com/entry/2020/02/16/170928
        """
        x_np = x_np.astype(np.float32)
        # NOTE: tensorの場合はコピーはclone()
        x_tensor = torch.from_numpy(x_np).clone()
        device = 'cuda' if torch.cuda.is_available() else 'cpu'
        x_tensor = x_tensor.to(device)
        return x_tensor

    @staticmethod
    def np_add_dim(np_2d_arr: np.ndarray) -> np.ndarray:
        """
        軸を増やす。使用用途としては、二次元配列を白黒画像化したいときなど。
        """
        return np.expand_dims(np_2d_arr, 0)


class TypeCheckUtil():
    @staticmethod
    def is_pil(any_img) -> bool:
        return isinstance(any_img, Image.Image)

    @staticmethod
    def is_np(any_img) -> bool:
        """
        npかどうかのみ
        """
        return isinstance(any_img, np.ndarray)

    @staticmethod
    def is_tensor(any_img) -> bool:
        return isinstance(any_img, torch.Tensor)

    def is_cv(any) -> bool:
        """
        TODO
        """
        pass


class ImageUitl():
    @staticmethod
    def get_img_info(any_img):
        """
        Dimensions-of-an-input-image
        """
        if isinstance(any_img, Image.Image):  # PIL
            w, h = any_img.size
            return h, w
        elif isinstance(any_img, torch.Tensor):  # Torch
            # https://discuss.pytorch.org/t/dimensions-of-an-input-image/19439
            c, h, w = any_img.size()
            return h, w
        elif np.shape(any_img) == ():  # OPENCV
            # https://answers.opencv.org/question/185629/in-python-is-there-a-method-or-operator-to-determine-if-a-frame-is-empty-or-an-all-zero-matrix/
            h, w, c = any_img.shape
            return h, w
        else:
            raise AssertionError("it's not compatible image type")

    @staticmethod
    def split_img_horizontally(img_cv_bgr: np.ndarray, n: int) -> np.ndarray:
        """
        n枚に画像を分割する
        """
        if n is None:
            raise AssertionError("Failed to split image.")

        height, width, _ = img_cv_bgr.shape
        blob_width = math.floor(width / n)
        y1 = 0
        y2 = height

        buff = []
        for i in range(n):
            x1 = i * blob_width
            x2 = (i + 1) * blob_width
            c = img_cv_bgr[y1: y2, x1: x2]  # img_rgb.crop((x1, y1, x2, y2))
            buff.append(c)
        return buff

    @staticmethod
    def img_save(any_img: Union[np.ndarray, Image.Image], file_path: str):
        if TypeCheckUtil.is_pil(any_img):
            any_img.save(file_path)
        elif TypeCheckUtil.is_np(any_img):
            cv2.imwrite(file_path, any_img)
        else:
            raise AssertionError("failed to get image")

    @staticmethod
    def mse(x, y):
        return np.linalg.norm(x - y)

    @staticmethod
    def ssim(input_img, output_img, kernel_size=15, is_data_range = False):
        kernel = np.ones((kernel_size, kernel_size), np.float32) / 255
        output_img = cv2.filter2D(output_img, -1, kernel)
        input_img = cv2.filter2D(input_img, -1, kernel)
        if is_data_range:
            return structural_similarity(
                input_img,
                output_img,
                data_range=input_img.max() - input_img.min(),
            )
        else:
            return structural_similarity(
                input_img,
                output_img,
            )

borderの追加

1
2
3
4
import cv2
img = cv2.imread('./fig.png')
img_pad = cv2.copyMakeBorder(img, 50, 50, 50, 50, cv2.BORDER_CONSTANT, (0,0,0))
cv2.imwrite('./fig_pad.png', img_pad)

torchのdatasetの画像確認

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import matplotlib.pyplot as plt
import numpy as np
from torch.utils.data import DataLoader

def confirm_img(train_loader: DataLoader):
    images, labels = iter(train_loader).next()

    images = images.clamp(0, 1)  # np.clipと同じ、[0, 1]にデータを収める
    images = images.view(images.size(0), 1, 256, 256)

    image_gray = images[0].squeeze().cpu().numpy()
    plt.imshow(image_gray, cmap="gray")
    plt.show()

    plt.hist(images[0].flatten(), bins=256, range=(0, 1))
    plt.show()

Contourの確認スニペット

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import math
import random
import numpy as np
import cv2
import matplotlib.pyplot as plt

def show_image(img_bgr, title):
    plt.imshow(img_bgr)
    plt.title(title)
    plt.show()

def show_contours_with_multi_colors(contours, img_bgr, title):
    for cnt in contours:
        color = [random.randinit(0, 2455) for _ in range(3)]
        cv2.drawContours(img_bgr, [cnt], -1, color, -1)
    plt.imshow(img_bgr)
    plt.title(title)
    plt.show()

def show_contours_with_one_color(contours, img_bgr, title):
    cv2.drawContours(img_bgr, [contours], -1, (0,255,0), -1)
    plt.imshow(img_bgr)
    plt.title(title)
    plt.show()

def show_all_contours(contours, img_bgr):
    for cnt in contours:
        cv2.drawContours(img_bgr, [cnt], -1, (0,0,0), -1)
        plt.show()

階層的クラスタリング

  • CVでの階層的クラスタリング(agglomerative clustering algorithm)は、Contourのクラスタリングをする手法
  • その際のbboxの距離は、NMSやIoUなどとは違い、距離自体は、サイズを加味して計算する
  • 基本的に閾値以下で隣接するモノをまとめる手法
  • すべてのcontourについて比較する必要がある為、計算量Oは最低でも$O(n^2)$になるはず
  • 実際は一次元だが、イメージはDendrogramで、距離が閾値以下がまとまる

Dendrogram

クラスターの結合方法は次の4つがあるが、このアルゴは3番の最短距離法を利用している。

  1. ウォード法:クラスターの併合時、平方和が最小になるようにする方法
  2. 最長距離法:距離の最も遠いデータ同士から併合していくする方法
  3. 最短距離法:距離の最も近いデータ同士から併合していく方法
  4. 群平均法:すべてのデータの平均を計算し、クラスターの距離として併合する方法

ウォード法

最長距離法

最短距離法

郡平均法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import os
import cv2
import numpy

def calculate_contour_distance(contour1, contour2): 
    x1, y1, w1, h1 = cv2.boundingRect(contour1)
    c_x1 = x1 + w1/2
    c_y1 = y1 + h1/2

    x2, y2, w2, h2 = cv2.boundingRect(contour2)
    c_x2 = x2 + w2/2
    c_y2 = y2 + h2/2

    # NOTE: 
    # - XとYについての距離の最大値を取る
    # - 項事に考える
    #   - a = abs(c_x1 - c_x2) : 2つのbboxの中心点の距離
    #   - b = (w1 + w2)/2 : 2つのbboxの平均幅
    #   - c = a - b : 2つのbboxのサイズを加味した距離(同じ中心点の距離でも、小さいbbox程離れて、多いbbox程近くなる)
    return max(abs(c_x1 - c_x2) - (w1 + w2)/2, abs(c_y1 - c_y2) - (h1 + h2)/2)

def merge_contours(contour1, contour2):
    return numpy.concatenate((contour1, contour2), axis=0)

def agglomerative_cluster(contours, threshold_distance=40.0):
    current_contours = contours
    while len(current_contours) > 1:
        min_distance = None
        min_coordinate = None

        for x in range(len(current_contours)-1):
            for y in range(x+1, len(current_contours)):
                distance = calculate_contour_distance(current_contours[x], current_contours[y])
                if min_distance is None:
                    min_distance = distance
                    min_coordinate = (x, y)
                elif distance < min_distance:
                    min_distance = distance
                    min_coordinate = (x, y)

        if min_distance < threshold_distance:
            index1, index2 = min_coordinate
            current_contours[index1] = merge_contours(current_contours[index1], current_contours[index2])
            del current_contours[index2]
        else: 
            break

    return current_contours

結果

Contour Clustering

二値化の確認

二値化は必須スキルなので、一覧にして確認する為のスニペット。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import cv2
import numpy as np
import matplotlib.pyplot as plt

image_path = "test.png"
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
# image = np.random.randint(0, 256, (100, 100), dtype=np.uint8)

gray_image = cv2.cvtColor(cv2.cvtColor(image, cv2.COLOR_GRAY2BGR), cv2.COLOR_BGR2GRAY)

histogram = cv2.calcHist([gray_image], [0], None, [256], [0, 256])

_, otsu_thresh = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

adaptive_mean = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)

adaptive_gaussian = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)

fig_adjusted, axes_adjusted = plt.subplots(6, 5, figsize=(20, 24))

for i, ax in enumerate(axes_adjusted.flatten()[:25]):
    _, thresh_image = cv2.threshold(gray_image, i * 10, 255, cv2.THRESH_BINARY)
    ax.imshow(thresh_image, cmap='gray', vmin=0, vmax=255)
    ax.set_title(f'Threshold = {i * 10}')
    ax.axis('off')

ax_hist_adjusted = plt.subplot2grid((6, 5), (5, 0), colspan=3, fig=fig_adjusted)
ax_hist_adjusted.plot(histogram)
ax_hist_adjusted.set_title('Grayscale Histogram')
ax_hist_adjusted.set_xlabel('Pixel value')
ax_hist_adjusted.set_ylabel('Frequency')

ax_otsu_adjusted = plt.subplot2grid((6, 5), (5, 3), fig=fig_adjusted)
ax_otsu_adjusted.imshow(otsu_thresh, cmap='gray')
ax_otsu_adjusted.set_title('Otsu Thresholding')
ax_otsu_adjusted.axis('off')

ax_adaptive_mean_adjusted = plt.subplot2grid((6, 5), (5, 4), fig=fig_adjusted)
ax_adaptive_mean_adjusted.imshow(adaptive_mean, cmap='gray')
ax_adaptive_mean_adjusted.set_title('Adaptive Mean Thresholding')
ax_adaptive_mean_adjusted.axis('off')

plt.tight_layout()
fig_adjusted.savefig('binarization.png')

元画像

結果

まとめ

  • CV周りは理解しなければならない事が多い
  • 上記の通り、古典的なCVのアルゴでも、手法も多岐に渡る
  • Deep learning, ViTとCVの最新技術はまだまだ続く

参考文献

Built with Hugo
テーマ StackJimmy によって設計されています。