1.概要
- 数年間に渡って画像処理の仕事をしてきた時に残っていた古典的画像処理周りのメモ
- 特に、2012年以前のCNNを代表とするDNN系の手法、最近のViT系の手法は別の記事にまとめる
- 画像処理でよく使うlibraryはnumpy, opencv, scikit-image, matplotlib, jupyterlab, pytorch, dlib, imageprayなどなど
- 個人的に使うOpenCVのスニペットも残す
2.知識メモ
2.1.基本
2.1.1.画像の種類
大きく分けて2種類ある。
- ベクター画像
- ベクター画像は、線や曲線といった図形集合の重ね合わせをデータとして保持した形式
- e.g. svg
- ラスター画像
- ラスター画像はピクセル(画素ともいう)と呼ばれる色の数値を格子状に並べたデータを保持した形式
- e.g. png, jpg
2.1.2.画像の圧縮方式の違い
- PNGは可逆圧縮だが、JPGは非可逆圧縮
- つまり、JPGはデータの圧縮の際にデータを削除するので小さな線分やオブジェクトが劣化する可能性がある
- 一般的には、PNGを使うのを推奨
- ただし、ベクトル化した場合はSVGを推奨
2.1.3.色空間
- 色空間(カラースペース)とは、色を数値に変換するシステムのこと

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

OpenCVでは次の型だが、おおむねCV_8U=np.uint8を使う。
型 | 範囲 |
---|
CV_8U | 0 - 255 |
CV_16U | 0 - 65535 |
CV_32F | 0 - 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))
|
入力画像

出力画像

カラースペースの特徴は以下になる。
カラースペース | 変換コード | チャネル | 特徴 |
---|
BGR | cv.COLOR_BGR2BGRA | B, G, R | 青、緑、赤の基本的なRGBカラースペース。一般的なカメラで使用される形式。 |
BGRA | cv.COLOR_BGR2BGRA | B, G, R, A | BGRカラースペースにアルファチャネルを追加。透過性を持つ。 |
HSV | cv.COLOR_BGR2HSV | H, S, V | 色相、彩度、明度のカラースペース。色を直感的に理解しやすい。 |
Lab | cv.COLOR_BGR2Lab | L, a, b | 光度(L)と2つの色成分(a、b)のカラースペース。人間の視覚に近い。 |
Luv | cv.COLOR_BGR2Luv | L, u, v | 光度(L)と2つの色成分(u、v)のカラースペース。CIE 1976 L*u*v* 標準。 |
HLS | cv.COLOR_BGR2HLS | H, L, S | 色相、光度、彩度のカラースペース。HSVと似ているが、光度が異なる。 |
YUV | cv.COLOR_BGR2YUV | Y, U, V | 輝度(Y)と2つの色成分(U、V)のカラースペース。ビデオ圧縮に使用される。 |
XYZ | cv.COLOR_BGR2XYZ | X, Y, Z | CIE 1931標準色度図のカラースペース。標準色度としてよく使用される。 |
YCrCb | cv.COLOR_BGR2YCrCb | Y, Cr, Cb | 輝度(Y)と2つの色成分(Cr、Cb)のカラースペース。映像処理に使用される。 |
2.1.4.画像の状態
画像が現実の写像になっていない時に使われる用語。
英語 | 日本語 | 対義語 |
---|
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.
2.2.座標系
2.2.1.代表的な座標系
いくつかあるが、次などが頻出する

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

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

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

2.3.3.紙からデータを抜き出す鉄板パターン
特に紙の場合は色が単一でデータがスパースなので、画像処理がやりやすい特徴がある。
- グレースケール
- 白黒をインバート
- コンター(contour)をとる
- 図形化する
ビット演算子で画像を合算処理する方法。
- よく使われるのが、白黒でのAND
- ある2つの処理が合った時に、完全に両方のオペランドで黒(0)じゃないときは、白にするというやり方
- つまり、2つの処理の両方で通った時のみ、表示するというやり方
1
2
3
4
5
6
| >>> 1 | 1
1
>>> 0 & 1
0
>>> 0 & 0
0
|
2.4.カメラ
2.4.1.キャリブレーション
- カメラが物理的にどう見ているかを理解するために、カメラの内部および外部のパラメータを特定するプロセス
- カメラのレンズの歪みを表現するパラメータや、カメラのワールド座標系での位置姿勢を推定できる
2.4.2.内部パラメータ・外部パラメータ
- 内部パラメータ
- 外部パラメータ
- カメラの位置、向き、回転など、カメラが世界に対してどう配置されているかのパラメータ
2.5.ROI系
2.5.1.ROIとは
- ROI: Region of Interest
- Subpixelとも言ったりもする

2.5.2.Saliency Map(顕著性マップ)
Saliency Mapとは、ある画像を人が見ると、視線はどこに向くか?をピクセル単位で考えたもの。

図:Saliency Mapの例。
- 左上:画像
- 右上:実測した視点を赤いXで示したもの
- 左下:Saliency Map
- 右下:Saliency Mapをカラーにして画像に重ねたもの
2.6.Segmentation
2.6.1.セグメンテーションとは
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.
2.6.2.セグメンテーションの4つの特徴
重要なの4つの特徴
- color: 色
- intensity: 強度(いわゆるコントラスト)
- location: 場所
- texture: パターン
2.6.3.セグメンテーションの種類
やりかたは、discontinuity か similarityの強さに応じて2つに大分される。
- The discontinuity approach splits an image according to sudden changes in intensities (edges).
- The similarity approach splits an image based on similar regions according to predefined criteria
テクニックは次のような感じ。

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

2.7.ノイズ除去系
2.7.1.デノイズ
cv2.fastNlMeansDenoising
を使う

2.7.2.Blurryな画像をClearにする方法
目的に応じて使い分ける必要がある。
- binarizationで分ける
- 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をつかう
- モルフォロジー変換を使う
- ガウシアンフィルタ

2.8.カーネル系
2.8.1.ConvとPoolの違い
DNNではおおむね次の2つのフィルターをかけるが、数学的には次の違いになる。
- Conv:
- 特徴マッチの為の処理
- invariant function
- Pool:
- 位置情報の為の処理
- variant function
2.8.2.代表的なカーネル
代表的なのは下。

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

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

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

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

- CNNのフィルタ
- CNNのConv、Poolなどのカーネルを古典的なアルゴと併用してを使う事もできる
- ImageNetなどで学習済みのCNNモデルのDNNを使ってベクトルを産出しできる
- それをPCAなどにかける事で、少数画像で教師あり学習(クラスタリング)をする事も可能
2.8.3.白と黒の意味
- 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")
|
2.9.ベクトル化
2.9.1.ベクトル化(スケルトン化)
次のように情報を縮尺する事。

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

2.9.3.ベジェ曲線
ベジェ曲線(Bezier Curve)とは、複雑な凹凸を持つ曲線でも、制御点の座標と関数により指定した二点間を結ぶ滑らかな曲線を表現できる手法。
2.10.凸包(convex hull)
2.10.1.凸包とは
凸包(convex hull)とは, 与えられた点をすべて包含する最小の凸多角形(凸多面体)のこと。

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

2.10.3.凸製の欠陥の例
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()
|

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


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




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



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

2.12.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.13.特徴量マッチング
2.13.1.特徴量の出し方
2.13.2.特徴量マッチングとは?
- 2つのオブジェクトで特徴量を抽出し、マッチングをする事
- 一番有名なのはテンプレートマッチング
一般的に、下のようなパイプラインになる。
- 2つの画像を用意する
- 特徴量detectorの処理
- まず、特徴点を出す
- その特徴点の特徴量を出す
- SIFTの場合は128次元
- 特徴量のmatcherの処理
- 2つの特徴量を変換する
- KDTree (木構造から検索)
- ブルーフォース (全部を検索)
- 検索する手法
- KNN法 (K個取得する)
- マッチのThresholding化
- 2つの特徴量が1以下など
2.13.3.特徴量
2.13.3.1.大域特徴量(global feature)とは
- 大域特徴量とは,画像全体から抽出される特徴量
- カラーヒストグラムなど
2.13.3.2.局所特徴量(local feature)とは?


2.13.4.マッチング手法
2.13.4.1.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万回もチェックする必要がある
2.13.4.1.1.FLANNの実装例
radius near neighborsとは?
- KNNの亜種。予め半径を決めておき、その範囲内をnear neibhbourにする
- K-nearestのestにならない理由は、何個かわからないから

K-nearest neighbor searchとは?
- K個近くのデータを撮ってくる。それを基にデータを決める
- つまり、類は友を呼ぶ
KDTreeアルゴリズムとは?
- 決定木で作る
- 決定木の深さ=領域の個数=n^2
- バイナリツリーみたいな感じ
- KNNで近傍を探すのに、木構造を利用して探す
see Kd-treeと最近傍探索 適応システム特論・担当三上
KNNとFixed-Radius KNNの違いとは?

see https://ai-master.gitbooks.io/knn/content/simple-extensions.html
2.13.4.2.ブルーフォース法
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番目のセットの他のすべての特徴と照合する。そして、最も近いものが返される。これを繰り返す

2.13.5.特徴量の比較


2.13.6.特徴量の出し方
2.13.6.1.Feature DetectorとFeature descriptorの違いは?
次のようにまとめられる。
アルゴ | input | outputs | 特徴 | example |
---|
feature detector | image | locations (場所) | 特徴については教えてくれない | corner detector |
feature descriptor | image | feature descriptors / feature vectors | 特徴について教えてくれる。scale invariantなことが多い。 | SHIFT, HOG, SURF, KAZE, AKAZE |
2.13.6.2.HOG特徴量とは?
- 勾配ベースの特徴量
- 白黒はっきりしていると取りやすい
- SVMを使ったりする
2.13.6.3.Haar like特徴量
- 隣接する2つの矩形の明るさの違い
- OpenCVだと顔認識に使われている
2.13.6.4.FAST algorithmとは?
2.13.6.5.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によって最初に発表された高速で堅牢なローカル機能検出器

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

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

2.14.2.2.pタイル法
Pタイル法(Percentile Method)は、2値化したい領域が全データの領域に占める割合をパーセント(%)で指定し2値化する手法。
2.14.2.3.Multilevel Thresholding
Otsuの二値化と違い、複数のThreasholdでクラスタリングできる。

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

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

ある特徴的なオブジェクトを検出するのに使える。
see Hands-on Morphological Image Processing
2.14.3.2.モルフォロジ処理の具体的な処理とは?
- モルフォロジ処理は、“構造要素"と呼ばれる画像を移動させる要素と,“ミンコフスキー(Minkowski)和・ミンコフスキー差” と呼ばれる演算から成り立っている
- モルフォロジー処理における代表的な処理は、「Erosion(エロージョン)」と「Dilation(ダイレーション)」と呼ばれる処理
- それぞれ孤立点の除去,不連続な点の接続と穴埋めのために利用される
- Erosionで細くしてノイズを減らして、Dilationで大きくして元の大きさに戻すイメージ

- opening
- erosionの後にdilationなので、ノイズが消える
- closing
- dilationのあとにerosionなので、ノイズが増える
2.14.3.3.ミンコフスキー和と差
- ミンコフスキー和・差は、二つのベクトルの集合同士を使って新しいベクトルの集合を求める.
- このミンコフスキー和と差を用いることで、ゲームにおける衝突判定やロボットのモーションプログラミングで計算を簡単にできる.

2.14.4.ワープ処理 (Image warping)

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

2.14.6.統計的にノイズを消す方法
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)
|

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

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

2.15.2.一般的な距離
- マンハッタン距離
- ユークリッド距離
- マハラノビス距離
- チェビシェフ距離

2.15.3.文字列間の距離
OCR後の文字列間の距離比較を行うには、次の距離が有名。

2.16.円
2.16.1.Circle, Curve, Arc, Fircleの違い
- Circle: 円
- Curve: 曲線
- Arc: 円弧
- Fircle: 手書きの円
メモ
- トポロジー的には、Arcは図形では、凸
- Circle -> D Cut -> Squaireのように相が変換する
2.16.2.RANSAC algorithmでArcフィッティング
The RANSAC (Random sample and consensus) algorithm is the gold standard in eliminating noise.
アルゴのフロー
- データをランダムにサンプリングする.(非復元抽出)
- モデルを学習する
- 全てのデータに対して誤差を計算する
- 3で求めた誤差から正常値と外れ値を決める
- 正常値のみでモデルの性能を評価する
- 既定回数に達したら終了.そうでなければ1に戻る
ようは、アンサンブル学習のbaggingをしている。
RANSACとGradient descent algoの違い。

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



2.16.4.円形度
確か、円形度はISOで定義があったはず。
円形度=L24π×S
(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
|
2.16.5.円摩度 (アスペクト比で補正した円形度)
see http://progearthplanetsci.org/highlights_j/072.html
New parameter of roundness R: Circularity corrected by aspect ratio.

R=Circularity+(Circularityperfect_circle−Circularityaspect_ratio)
ここで、
- Circularityperfect_circle
- Circularityaspect_ratio
- 真円のアスペクト比のみを変化させたときに得られる円形度
2.16.6.円磨度 (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)
2.16.7.楕円の歪み度
Major AxisとMinor Axisの比を取ればOK。

2.17.モーメント
Huモーメントは次の3つに対して不変な特徴を持つ。
- 平行移動 (translation)
- スケール (scale, size)
- 回転 (rotation)
2.17.1.モーメント (素モーメント)
2変数関数f(x,y)に対して、(p+q)次のモーメント(素モーメント)を次のように定義する。
pとqはそれぞれモーメントの次数。
Mpq=∫−∞∞∫−∞∞xpyqf(x,y)dxdy
これをグレースケールの画像に適用すると
Mij=x∑y∑xiyjI(x,y)
つまり、x座標×y座標×ピクセルの値の合計値となる。
素モーメントそのものは不変量ではない。
M00M01M10M11M20M02=面積,=Y軸座標値とピクセル値の面積=Y軸座標値の期待値,=X軸座標値とピクセル値の面積=X軸座標値の期待値,=XY軸座標値とピクセル値の体積=XとY軸座標の面積,=Y軸座標値2とピクセル値の面積=Y軸座標2の期待値,=X軸座標値2とピクセル値の面積=X軸座標2の期待値,
- M00の解釈
- すると、M00などは、面積と捉えられる。
- また、ピクセル値を確率と捉えると、面積ではなく、確率和と理解できる。
- M01 の解釈
- M01はY軸座標値とピクセル値の掛け算=その面積。
- ただし、ピクセル値の値域[0,1]なので、確率と捉えると、
- Y座標値の期待値の合計=期待値和
期待値=確率×確率変数
- 合計なので、単位はCount×ExpectedValue
- M11の解釈
- M11はXY軸座標値とピクセル値の掛け算=その体積。
- ただし、ピクセル値の値域[0,1]なので、確率と捉えると、
- Y座標値の期待値とX軸座標値の期待値の積の合計=期待値積の和=混合分布の和
- M02の解釈
- M02はY軸座標値^2とピクセル値の掛け算=その体積。
- ただし、ピクセル値の値域[0,1]なので、確率と捉えると、
- Y座標値^2の期待値の合計=期待値平方和
2.17.2.中心モーメント(並行不変性)
中心モーメントは平行普遍性をもつ。
なぜこの中心モーメントが並行不変性をもつかというと、ピクセル座標の原点を物体の中心(重心)にしているから。
なので、物体がどこにあろうが、同じ値を出す=並行不変値
μij=x∑y∑(x−xˉ)i(y−yˉ)jI(x,y)
ただし、xˉ,yˉは先に示した重心を示す。
このμのパターンを定義すると以下。
μ00μ01μ10μ11μ20μ02=M00,=0,=0,=M11−xˉM01=M11−yˉM10,=M20−xˉM10,=M02−yˉM01,
- μ01の解釈
- μ01=0の理由はyˉがY軸の重心ピクセル座標であり、
- y−yˉの値は+と-で分布して合計すると必ず0になるから。
- ※ 重心はピクセルのブロブの原点。
- μ11の解釈
- つまり、あるピクセルの値(確率)とそのピクセルの座標の原点からの距離を加味した指標
- μ20の解釈
- 重心から座標がどれだけ離れているか、ただし、二乗しているので、絶対値。
- つまりは、重心からどれだけはなれているかを加味した値。
2.17.3.スケール不変性
次のような値を考えることで、スケールに対する不変量を作ることができる。
このモーメントはi+j≥2のとき並進不変性を持つ。
なぜ、スケール普遍化というと、分母に面積を撮っているから。つまり、単位面積あたりの値にしている。
ηij=μ00(1+2i+j)μij
- i+j<2の理由
- 上の定義を代入してみるとわかるが、i+j<2のときは定数になってしまい意味を持たない。
- なぜなら、下のように、μのijの合計が2未満のときは、分母を加味して定数だから。
μ00=面積μ10=0μ01=0
- η11の解釈
i=1j=1ηij=μ002μ11
- η20の解釈
i=2j=0ηij=μ002μ20
2.17.4.回転不変性
- 更にηijを用いて、回転に対して不変な量を構築することができる。これはHuによって発見されたため、Huモーメント不変量と呼ばれている
- 1から6までは回転不変。7はskew invariant
I1=η20+η02
I2=(η20−η02)2+4η112
I2=(η20−η02)2+4η112
I3=(η30−3η12)2+(3η21−η03)2
I4=(η30+η12)2+(η21+η03)2
I5=(η30−3η12)(η30+η12)[(η30+η12)2−3(η21+η03)2]+(3η21−η03)(η21+η03)[3(η30+η12)2−(η21+η03)2]
I6=(η20−η02)[(η30+η12)2−(η21+η03)2]+4η11(η30+η12)(η21+η03)
I7=(3η21−η03)(η30+η12)[(η30+η12)2−3(η21+η03)2]−(η30−3η12)(η21+η03)[3(η30+η12)2−(η21+η03)2].
see https://stackoverflow.com/questions/11379947/meaning-of-the-seven-hu-invariant-moments-function-from-opencv
2.17.5.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.png | Lena3.jpg | sendo1.jpg | Sendou2.jpg | monnalisa.jpg | apple.jpg | Red_Apple.jpg |
---|
Lenna.png | 0.00 | 0.04 | 5.93 | 5.87 | 4.19 | 2.43 |
Lena3.jpg | 0.04 | 0.00 | 5.93 | 5.86 | 4.22 | 2.41 |
sendo1.jpg | 5.93 | 5.93 | 0.00 | 0.09 | 7.82 | 4.55 |
Sendou2.jpg | 5.87 | 5.86 | 0.09 | 0.00 | 7.86 | 4.47 |
monnalisa.jpg | 4.19 | 4.22 | 7.82 | 7.86 | 0.00 | 3.97 |
apple.jpg | 2.43 | 2.41 | 4.55 | 4.47 | 3.97 | 0.00 |
Red_Apple.jpg | 4.83 | 4.83 | 1.19 | 1.11 | 7.88 | 4.94 |
2.17.6.画像のモーメントと面積と重心
モーメントとは?
Mnm=∑画像全体(x座標n×y座標m×画素値)
画像中の全ピクセルについての総和。
2.17.7.モーメントのフロー
モーメントの計算方法は2つのフローに従う。
- ピクセルずつデータを取る
- 座標ごとのべき乗をもとめて足す
まず、データ(x座標、y座標、画素値)を調べる

次に、全てのピクセルについて次を求める。
さらに、上記の2つと画素値(0または1)を掛け合わせる。
例えば、(15,17,1)のピクセルについて、n=2、m=0の場合は次となる。
M2,0=152×170×1=225×1×1=225
このピクセルでの計算結果は225となる。
全てのピクセルでの計算結果を足し合わせたものが画像のモーメントMnm。
単純化すると次になる。

- 画像なので座標はピクセル座標
- (0,0), (2,2)の範囲のピクセルを考える
- つまり、範囲はグリッド座標でいう、(0,0) から (3,3)までのブロブ
- 面積は9
- M0,0 = 9
- M1,0= 0+1+2+0+1+2+0+1+2 = 9 (左上から横方向にチェック)
- M00M10=9/9=1
つまり、X座標では1の座標をもつピクセルが重心。
また、重心=ブロブの原点。
つまり、重心を原点ととると、全てのx座標の合計、全てのy座標の合計のそれぞれが0となる。
2.18.Contour(輪郭)
2.18.1.contourとperimeterの違い
2.18.2.Contourのフロー
- グレースケールに変換
- 画像を2値化する
- 輪郭の取得
- 輪郭の分析
例
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()
|
2.18.3.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のマージ(
階層クラスタリング
)
2.18.4.findContoursの引数
- binaryデータを渡して、その中のオブジェクトを取得するメソッド
- 簡易的物体検知であり、以外に複雑な構造をしているのでメモ
- 手法(mode)+輪郭取り方(method)でパターンが分かる

NOTE: 外の白枠は分かりやすさでついているもので、実際はない。
2.18.4.1.findCountoursのmode
RETR_TREE: 輪郭の階層情報をツリー形式で取得する。


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


RETR_LIST:階層なし


RETR_EXTERNAL:外側のみ


2.18.4.2.findCountoursのmethod
CHAIN_APPROX_NONE:輪郭上のすべての座標を取得

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

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

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

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

2.19.ガイド
2.19.1.Scikit learn ガイド
有名なScikit-leranのガイド。

2.19.2.Dlib選択ガイド
こちらは一部で有名なDlibガイド。

3.スニペット
3.1.簡易的なログ残し
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'))
|
3.2.シードの設定
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
|
3.3.画像の変換
基本は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,
)
|
3.4.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)
|

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

クラスターの結合方法は次の4つがあるが、このアルゴは3番の最短距離法を利用している。
- ウォード法:クラスターの併合時、平方和が最小になるようにする方法
- 最長距離法:距離の最も遠いデータ同士から併合していくする方法
- 最短距離法:距離の最も近いデータ同士から併合していく方法
- 群平均法:すべてのデータの平均を計算し、クラスターの距離として併合する方法




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
|
結果

3.8.二値化の確認
二値化は必須スキルなので、一覧にして確認する為のスニペット。
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')
|
元画像

結果

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