勉強しないとな~blog

ちゃんと勉強せねば…な電気設計エンジニアです。

OpenCVやってみる-9. 輪郭検出

OpenCVチュートリアルの続きです。

OpenCVにおける輪郭(領域) — OpenCV-Python Tutorials 1 documentation

とりあえず輪郭検出、表示

cv2.findContours()関数で、輪郭を検出することができます。
詳細はともかく、やってみます。

2値画像に対しての処理となること、白い物体と黒い背景があり、物体の輪郭を検出するということで、2値化のチュートリアルのところでやった画像を使ってみます。

HSV値からシール領域、台紙領域(および点数数字)を検出したものです。

import cv2
img1 = cv2.imread('harupan_200317_1.jpg')
img1 = cv2.resize(img1, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA)
img1_hsv = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV)
ret, img1_seal = cv2.threshold(img1_hsv[:,:,0], 160, 255, cv2.THRESH_BINARY)
ret, img1_card = cv2.threshold(img1_hsv[:,:,1], 50, 255, cv2.THRESH_BINARY_INV)

from matplotlib import pyplot as plt
plt.subplot(121), plt.imshow(img1_seal, cmap='gray'), plt.title('Seal area'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(img1_card, cmap='gray'), plt.title('Card area'), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20210505225936p:plain:w600

この画像でcv2.findContours()関数を実行、cv2.drawContours()関数で輪郭を表示します。

img1_seal_cp = img1_seal
img1_card_cp = img1_card
img1_seal_contours, img1_seal_hierarchy = cv2.findContours(img1_seal_cp, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
img1_card_contours, img1_card_hierarchy = cv2.findContours(img1_card_cp, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

img1_seal_ctrs = cv2.drawContours(img1.copy(), img1_seal_contours, -1, (0,255,0), 2)
img1_card_ctrs = cv2.drawContours(img1.copy(), img1_card_contours, -1, (255,0,0), 3)
img1_seal_ctrs = cv2.cvtColor(img1_seal_ctrs, cv2.COLOR_BGR2RGB)
img1_card_ctrs = cv2.cvtColor(img1_card_ctrs, cv2.COLOR_BGR2RGB)
plt.subplot(121), plt.imshow(img1_seal_ctrs), plt.title('Seal contours'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(img1_card_ctrs), plt.title('Card contours'), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20210506082420p:plain:w600

シールや台紙の輪郭はとれていそうですが、細かい余計な輪郭も出ているようです。

いくつか気づき点等。

  • 今回の環境ではcv2.findContours()関数の戻り値はcontours, hierarchyの2つだけでした。OpenCVのバージョンによるのか?
    以下のサイトではこの戻り値2つのパターンで記載されていました。
    OpenCV - findContours で画像から輪郭を抽出する方法 - pystyle
  • cv2.drawContours()関数では、引数の画像に上書きしてしまうようです。輪郭画像を元の画像に重ねるとき、元の画像をコピーしています。画像のコピーは、単純な代入だと参照になるようだったので、copy()関数を使います。
  • カラー画像表示の際、OpenCVではBGRフォーマット、matplotlibではRGBフォーマットで扱われるので、変換しています。

輪郭の特徴

輪郭の面積や周長を出せるようなので、これでほしい輪郭だけフィルタリングすることができるのでは?

まず面積

シール領域画像、台紙領域画像からの輪郭の面積の分布がどうなっているかを調べてみます。

img1_seal_areas = []
for ctr in img1_seal_contours:
    img1_seal_areas += [cv2.contourArea(ctr)]

img1_card_areas = []
for ctr in img1_card_contours:
    img1_card_areas += [cv2.contourArea(ctr)]

以下のように、輪郭面積の範囲を確認しながら、ヒストグラムを見てみました。

>>> max(img1_seal_areas)
1496.0
>>> plt.hist(img1_seal_areas, 150, [0,1500])
(array([819.,  96.,  34.,  30.,  12.,   2.,   5.,   1.,   0.,   0.,   0.,
         0.,   1.,   1.,   4.,   7.,   3.,   0.,   1.,   1.,   0.,   0.,
         2.,   1.,   1.,   0.,   1.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   1.,   0.,   1.,   0.,   0.,   0.,   0.,   0.,   2.,
         0.,   0.,   0.,   0.,   0.,   0.,   1.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   1.,   1.,   0.,   1.,
         0.,   0.,   1.,   0.,   0.,   0.,   0.,   2.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   1.,
         0.,   0.,   0.,   0.,   1.,   0.,   1.,   2.,   0.,   1.,   1.,
         0.,   2.,   0.,   1.,   2.,   1.,   1.,   0.,   0.,   1.,   0.,
         0.,   0.,   1.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   1.]), array([   0.,   10.,   20.,   30.,   40.,   50.,   60.,   70.,   80.,
         90.,  100.,  110.,  120.,  130.,  140.,  150.,  160.,  170.,
        180.,  190.,  200.,  210.,  220.,  230.,  240.,  250.,  260.,
        270.,  280.,  290.,  300.,  310.,  320.,  330.,  340.,  350.,
        360.,  370.,  380.,  390.,  400.,  410.,  420.,  430.,  440.,
        450.,  460.,  470.,  480.,  490.,  500.,  510.,  520.,  530.,
        540.,  550.,  560.,  570.,  580.,  590.,  600.,  610.,  620.,
        630.,  640.,  650.,  660.,  670.,  680.,  690.,  700.,  710.,
        720.,  730.,  740.,  750.,  760.,  770.,  780.,  790.,  800.,
        810.,  820.,  830.,  840.,  850.,  860.,  870.,  880.,  890.,
        900.,  910.,  920.,  930.,  940.,  950.,  960.,  970.,  980.,
        990., 1000., 1010., 1020., 1030., 1040., 1050., 1060., 1070.,
       1080., 1090., 1100., 1110., 1120., 1130., 1140., 1150., 1160.,
       1170., 1180., 1190., 1200., 1210., 1220., 1230., 1240., 1250.,
       1260., 1270., 1280., 1290., 1300., 1310., 1320., 1330., 1340.,
       1350., 1360., 1370., 1380., 1390., 1400., 1410., 1420., 1430.,
       1440., 1450., 1460., 1470., 1480., 1490., 1500.]), <BarContainer object of 150 artists>)
>>> 
>>> max(img1_card_areas)
79053.5
>>> plt.hist(img1_card_areas, 80, [0, 80000])
(array([971.,  26.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   1.]), array([    0.,  1000.,  2000.,  3000.,  4000.,  5000.,  6000.,  7000.,
        8000.,  9000., 10000., 11000., 12000., 13000., 14000., 15000.,
       16000., 17000., 18000., 19000., 20000., 21000., 22000., 23000.,
       24000., 25000., 26000., 27000., 28000., 29000., 30000., 31000.,
       32000., 33000., 34000., 35000., 36000., 37000., 38000., 39000.,
       40000., 41000., 42000., 43000., 44000., 45000., 46000., 47000.,
       48000., 49000., 50000., 51000., 52000., 53000., 54000., 55000.,
       56000., 57000., 58000., 59000., 60000., 61000., 62000., 63000.,
       64000., 65000., 66000., 67000., 68000., 69000., 70000., 71000.,
       72000., 73000., 74000., 75000., 76000., 77000., 78000., 79000.,
       80000.]), <BarContainer object of 80 artists>)

台紙領域画像からの輪郭で、大きな輪郭が1つ見られます。
これが台紙全体の輪郭ではないかと期待されるので、見てみます。

for ctr in img1_card_contours:
    if cv2.contourArea(ctr) > 79000:
        img1_card_contour_largest += [ctr]
img1_card_ctrs = cv2.drawContours(img1.copy(), img1_card_contour_largest, 1, (255,0,0), 3)
cv2.imshow('Card contours', img1_card_ctrs)

f:id:nokixa:20210507000242p:plain:w400

予想通りです。
射影変換用の角の4点検出に使えそうな感じがします。

後はシール領域、また、数字の輪郭を検出したい。
ヒストグラムを今度はもう少し細かく見てみます。

>>> plt.hist(img1_seal_areas, 100, [0,100])
(array([408.,  51., 144.,  24.,  69.,  26.,  29.,  21.,  33.,  14.,  18.,
        21.,  11.,  12.,   6.,   7.,   4.,   6.,   7.,   4.,   6.,   5.,
         4.,   5.,   2.,   1.,   2.,   5.,   3.,   1.,   3.,   6.,   5.,
         2.,   1.,   2.,   4.,   1.,   2.,   4.,   1.,   2.,   2.,   2.,
         2.,   1.,   0.,   1.,   0.,   1.,   0.,   0.,   1.,   0.,   0.,
         0.,   0.,   0.,   1.,   0.,   1.,   1.,   0.,   0.,   2.,   1.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   1.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.]), array([  0.,   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.]), <BarContainer object of 100 artists>)
>>> plt.hist(img1_card_areas, 100, [0,2000])
(array([869.,  49.,  11.,  20.,   6.,   2.,   3.,   1.,   0.,   4.,   1.,
         0.,   0.,   1.,   0.,   0.,   1.,   1.,   0.,   0.,   0.,   0.,
         0.,   1.,   1.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   4.,   4.,   5.,   6.,   4.,   1.,   1.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   1.,   0.,
         0.]), array([   0.,   20.,   40.,   60.,   80.,  100.,  120.,  140.,  160.,
        180.,  200.,  220.,  240.,  260.,  280.,  300.,  320.,  340.,
        360.,  380.,  400.,  420.,  440.,  460.,  480.,  500.,  520.,
        540.,  560.,  580.,  600.,  620.,  640.,  660.,  680.,  700.,
        720.,  740.,  760.,  780.,  800.,  820.,  840.,  860.,  880.,
        900.,  920.,  940.,  960.,  980., 1000., 1020., 1040., 1060.,
       1080., 1100., 1120., 1140., 1160., 1180., 1200., 1220., 1240.,
       1260., 1280., 1300., 1320., 1340., 1360., 1380., 1400., 1420.,
       1440., 1460., 1480., 1500., 1520., 1540., 1560., 1580., 1600.,
       1620., 1640., 1660., 1680., 1700., 1720., 1740., 1760., 1780.,
       1800., 1820., 1840., 1860., 1880., 1900., 1920., 1940., 1960.,
       1980., 2000.]), <BarContainer object of 100 artists>)

やっぱり台紙領域画像からの輪郭のほうが分かりやすそうで、面積1000以上に分布している25個の輪郭がシール輪郭に当たるのではと考えられます。(実際にはシールは23個しかないので、2個は違うものです。)

img1_card_contours_seal = []
for ctr in img1_card_contours:
    if 2000 > cv2.contourArea(ctr) > 1000:
        img1_card_contours_seal += [ctr]

img1_card_ctrs = cv2.drawContours(img1.copy(), img1_card_contours_seal, -1, (255,0,0), 3)
cv2.imshow('Card contours', img1_card_ctrs)

f:id:nokixa:20210507005223p:plain:w400

これもおよそ予想通り、いい感じです。
シールではない2つの輪郭は、台紙の下のほうの四角い領域でした。

最小外接円で円形領域を探す

シール領域画像の輪郭があまり使えていないので、なんとかならないかと。
シールの輪郭はだいたい円形をしているので、最小外接円との面積の差が小さいもの、で探すことができるのでは?と考えました。

img1_seal_ratios = []
for ctr in img1_seal_contours:
    (x, y), radius = cv2.minEnclosingCircle(ctr)
    img1_seal_ratios += [cv2.contourArea(ctr) / (radius*radius*np.pi)]
>>> max(img1_seal_ratios)
0.8911880248803259
>>> plt.hist(img1_seal_ratios, 100, [0, 1.0])
(array([367.,   0.,   0.,   1.,   1.,   1.,   8.,   6.,   4.,   3.,   6.,
         9.,  12.,   4.,   6.,   6.,   1.,   4.,   5.,   5.,   3.,   3.,
         4.,   4.,   4.,   7.,   6.,   4.,   3.,   3.,   6.,  39.,   3.,
         0.,   4.,   6.,   8.,   7.,  18.,   8.,  13.,   1.,   3.,   0.,
         7.,   6.,   3.,  24.,   3.,   2.,  11.,   2.,   5.,   5.,   6.,
        10.,  74.,   8.,   1.,   3.,   5.,  15.,   3., 160.,   2.,   2.,
         2.,   9.,   1.,   1.,  18.,   3.,   1.,   9.,  12.,   3.,   0.,
         1.,   3.,   5.,   3.,   3.,   4.,   2.,   0.,   1.,   2.,   0.,
         0.,   4.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
         0.]), array([0.  , 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1 ,
       0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2 , 0.21,
       0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3 , 0.31, 0.32,
       0.33, 0.34, 0.35, 0.36, 0.37, 0.38, 0.39, 0.4 , 0.41, 0.42, 0.43,
       0.44, 0.45, 0.46, 0.47, 0.48, 0.49, 0.5 , 0.51, 0.52, 0.53, 0.54,
       0.55, 0.56, 0.57, 0.58, 0.59, 0.6 , 0.61, 0.62, 0.63, 0.64, 0.65,
       0.66, 0.67, 0.68, 0.69, 0.7 , 0.71, 0.72, 0.73, 0.74, 0.75, 0.76,
       0.77, 0.78, 0.79, 0.8 , 0.81, 0.82, 0.83, 0.84, 0.85, 0.86, 0.87,
       0.88, 0.89, 0.9 , 0.91, 0.92, 0.93, 0.94, 0.95, 0.96, 0.97, 0.98,
       0.99, 1.  ]), <BarContainer object of 100 artists>)

これまた微妙な分布…
とりあえず面積比率の大きい(外接円との面積の差が小さい)ものをいくつかを取ってみたいと思います。

img1_seal_contours_seal = []
for i in range(len(img1_seal_contours)):
    if img1_seal_ratios[i] > 0.77:
        img1_seal_contours_seal += [img1_seal_contours[i]]

img1_seal_ctrs = cv2.drawContours(img1.copy(), img1_seal_contours_seal, -1, (0,255,0), 3)
cv2.imshow("Seal contours", img1_seal_ctrs)

f:id:nokixa:20210507014504p:plain:w400

やっぱりいまいちですが、このやり方も使い道はありそうです。
そもそもシール領域画像にもう少し前処理が必要だったかもしれません。

以上

今回はここまでにします。
あっという間に春のパン祭りが終わってしまった…
来年に生かせればいいな。

OpenCVやってみる-8. エッジ検出

OpenCVチュートリアル通りに進めます。
今回の内容はシール点数集計に役立ちそうです。

画像の勾配 — OpenCV-Python Tutorials 1 documentation

Canny法によるエッジ検出 — OpenCV-Python Tutorials 1 documentation


Sobelフィルタ、Laplacianフィルタ

Sobelフィルタ、Laplacianフィルタはいずれもカーネルと画像の畳み込み演算を行い、画像中の輝度勾配の大きさを示す画像を生成します。

Sobelフィルタは、画像の縦方向、横方向それぞれの輝度勾配を計算します。

【画像処理】ソーベルフィルタの原理・特徴・計算式 | 西住工房

Image Filtering — opencv 2.2 (r4295) documentation

Laplacianフィルタは、Laplacian演算子に相当する演算を行います。

【画像処理】ラプラシアンフィルタの原理・特徴・計算式 | 西住工房

これらを、春のパン祭り画像に作用させてみます。
やることは、チュートリアルのまんまです。

import cv2
import numpy as np
img1 = cv2.imread('harupan_200317_1.jpg', cv2.IMREAD_GRAYSCALE)
img1 = cv2.resize(img1, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA)

img1_laplacian = cv2.Laplacian(img1, cv2.CV_64F)
img1_sobelx = cv2.Sobel(img1, cv2.CV_64F, 1, 0, ksize=5)
img1_sobely = cv2.Sobel(img1, cv2.CV_64F, 0, 1, ksize=5)

from matplotlib import pyplot as plt
plt.subplot(2,2,1), plt.imshow(img1, cmap='gray'), plt.title('img1'), plt.xticks([]), plt.yticks([])
plt.subplot(2,2,2), plt.imshow(img1_laplacian, cmap='gray'), plt.title('laplacian'), plt.xticks([]), plt.yti
plt.subplot(2,2,3), plt.imshow(img1_sobelx, cmap='gray'), plt.title('sobelx'), plt.xticks([]), plt.yticks([]
plt.subplot(2,2,4), plt.imshow(img1_sobely, cmap='gray'), plt.title('sobely'), plt.xticks([]), plt.yticks([]
plt.show()

f:id:nokixa:20210428232742p:plain

ラプラシアンだけわかりにくくなってしまった…
これだけで表示してみます。

plt.imshow(img1_laplacian, cmap='gray'), plt.title('laplacian'), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20210429001516p:plain:w400

カーネルサイズを大きくしてやってみます。

img1_laplacian2 = cv2.Laplacian(img1, cv2.CV_64F, ksize=5)
plt.imshow(img1_laplacian2, cmap='gray'), plt.title('laplacian2'), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20210429001805p:plain:w400

エッジがはっきりしました。
一応横に並べて表示。

plt.subplot(1,2,1), plt.imshow(img1_laplacian, cmap='gray'), plt.title('laplacian'), plt.xticks([]), plt.yti
plt.subplot(1,2,2), plt.imshow(img1_laplacian2, cmap='gray'), plt.title('laplacian2'), plt.xticks([]), plt.y
plt.show()

f:id:nokixa:20210429002200p:plain:w400

画像の解像度やエッジの幅に合わせてカーネルサイズも調整する必要がある?
あらかじめノイズ除去や平滑化しておく必要もある?
解像度の変更も必要か?

解像度を縦横それぞれ半分にしてラプラシアンフィルタをかけてみると、元の画像でカーネルサイズ5にしたのと似たような結果が得られました。

img2 = cv2.imread('harupan_200317_1.jpg', cv2.IMREAD_GRAYSCALE)
img2 = cv2.resize(img2, None, fx=0.05, fy=0.05, interpolation=cv2.INTER_AREA)
img2_laplacian = cv2.Laplacian(img2, cv2.CV_64F)
plt.imshow(img2_laplacian, cmap='gray'), plt.title('laplacian'), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20210429002801p:plain:w400


Canny法

Canny法によるエッジ検出もやってみます。

Canny法によるエッジ検出 — OpenCV-Python Tutorials 1 documentation

ざっくり言うと、つながっているエッジをうまく検出するようなアルゴリズムのようです。
結果は2値になるのか?

cv2.Canny()関数には2つのエッジ強度閾値の引数があります。
どれくらいに設定すればいいのか調べてみます。

エッジ画像を作る

Canny法では、5x5のガウシアンフィルタをかけ、2軸方向のSobelフィルタをかけてエッジ画像(勾配の強度)を作ります。
これと同じことをやってみます。

img1_gaussian = cv2.GaussianBlur(img1, (5,5), 0)
img1_1 = cv2.GaussianBlur(img1, (5,5), 0)
img1_2 = cv2.Sobel(img1_1, cv2.CV_64F, 1, 0, ksize=3)
img1_3 = cv2.Sobel(img1_1, cv2.CV_64F, 0, 1, ksize=3)
img1_4 = np.sqrt(img1_2**2 + img1_3**2)
plt.imshow(img1_4, cmap='gray'), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20210501215155p:plain:w400

いい感じにエッジが見えています。
前までのエッジ画像では正負の値があったためか、ベースがグレーで、エッジのところが黒または白となっていましたが、今回は勾配強度の絶対値を取っているので、ベースが黒でエッジが白となっています。

このエッジ画像のヒストグラムを見てみます。

まずは値の範囲を確認。

>>> np.max(img1_4)
411.0985283359696
>>> np.min(img1_4)
0.0

cv2.calcHist()関数を使おうと思いますが、画素値データ型はuint8かfloat32のみとのことなので、変換してから使います。

img1_5 = img1_4.astype(np.float32)
hist = cv2.calcHist([img1_5], [0], None, [500], [0,500])
plt.plot(hist)
plt.show()

f:id:nokixa:20210501225336p:plain:w400

ピークの出ているのがバックグラウンドの黒でしょうか。
値100、200、300ぐらいで2値化してみると、

ret, img1_th1 = cv2.threshold(img1_5, 100, 255, cv2.THRESH_BINARY)
ret, img1_th2 = cv2.threshold(img1_5, 200, 255, cv2.THRESH_BINARY)
ret, img1_th3 = cv2.threshold(img1_5, 300, 255, cv2.THRESH_BINARY)
plt.subplot(131), plt.imshow(img1_th1, cmap='gray'), plt.title('thresh=100'), plt.xticks([]), plt.yticks([])
plt.subplot(132), plt.imshow(img1_th2, cmap='gray'), plt.title('thresh=200'), plt.xticks([]), plt.yticks([])
plt.subplot(133), plt.imshow(img1_th3, cmap='gray'), plt.title('thresh=300'), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20210501231313p:plain:w600

値100で非エッジとエッジの区別ができそうです。
100だとエッジ周辺も拾っているので、200ぐらいがエッジ位置になるかと思います。
300はやり過ぎですね。

Canny法実施

前の結果から、2つの閾値は100と200でやってみます。
結局チュートリアルと同じ値になりますが。

img1_canny = cv2.Canny(img1, 100, 200)
plt.imshow(img1_canny, cmap='gray'), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20210501232508p:plain:w400

シールの外枠、点数文字の境界がとれた感じです。
台紙の細かい文字もエッジとしてとれています。

試しに閾値を変えてみます。

img1_canny2 = cv2.Canny(img1, 200, 300)

f:id:nokixa:20210501233023p:plain:w400

あまり変わらない…
それほど閾値にこだわらなくても大丈夫か?


ここまで

今回はここまでにします。
まだまだチュートリアルに沿って進めます。
次は輪郭の検出をやります。

OpenCVやってみる-7. モルフォロジー変換

今回は、チュートリアル通り、モルフォロジー変換です。

モルフォロジー変換 — OpenCV-Python Tutorials 1 documentation

モルフォロジー

モルフォロジー(morphology)とは、

「一般に、形態、構造をいう。」

モルフォロジーとは - コトバンク

だそうです。
どゆこと?

OpenCVチュートリアルの説明によると、モルフォロジー変換は「画像上に写っている図形に対して作用するシンプルな処理」 とのこと。
チュートリアル内容を読みましたが、うまく端的な表現が思いつかない…
ひとまず、収縮(Erosion)、膨張(Dilation)という2種類の基本的な処理があり、これらを組み合わせてまた別の処理が定義されるようです。
また、処理対象は主に2値画像となります。

種類

チュートリアルでは、7種類の変換が紹介されていますが、今回シール点数集計で使えそうかな、というものだけやってみます。

  • 収縮(Erosion)
  • 膨張(Dilation)
  • オープニング(Opening)

まず2値化

前々回に2値化をやったので、これを元にモルフォロジー変換をやってみます。

その前に、resize関数で、fxfy引数を使っての縮小をやってみます。
今まではsize引数を用意してやっていましたが、ちょっと楽になります。

>>> import cv2
>>> img1 = cv2.imread('harupan_200317_1.jpg')
>>> img1 = cv2.resize(img1, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA)
>>> cv2.imshow('image1', img1)
>>> cv2.waitKey(0)

f:id:nokixa:20210425220826p:plain

前々回の2値化。

>>> img1_hsv = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV)
>>> ret, img1_binary = cv2.threshold(img1_hsv[:,:,0], 160, 255, cv2.THRESH_BINARY)
>>> cv2.imshow('binary', img1_binary)
>>> cv2.waitKey(0)

f:id:nokixa:20210425222219p:plain

モルフォロジー変換

それでは本題に。

収縮(Erosion)

>>> import numpy as np
>>> kernel  = np.ones((5,5), np.uint8)
>>> img1_erosion = cv2.erode(img1_binary, kernel, iterations=1)
>>> cv2.imshow('erosion', img1_erosion)
>>> cv2.waitKey(0)

f:id:nokixa:20210425223409p:plain

何も考えずにやりましたが、これだとだめですね。
シールのピンク領域を白にした2値化画像ですが、どちらかというとピンク領域の内側の数字に着目したい。

反転させてやってみます。

>>> img1_inv = cv2.bitwise_not(img1_binary)
>>> cv2.imshow('inversion', img1_inv)

f:id:nokixa:20210425224955p:plain

>>> img1_erosion_inv = cv2.erode(img1_inv, kernel, iterations=1)
>>> cv2.imshow('erosion_inv', img1_erosion_inv)

f:id:nokixa:20210425225111p:plain

えらく数字の線が細い…
Erosionのカーネルサイズを小さくしてみるか?

>>> kernel = np.ones((3,3), np.uint8)
>>> img1_erosion_inv = cv2.erode(img1_inv, kernel, iterations=1)
>>> cv2.imshow('erosion_inv', img1_erosion_inv)

f:id:nokixa:20210425225303p:plain

数字が分かりやすくなりました。
シールより外側を除けばなお良い感じになったかも。

膨張(Dilation)

反転前の画像でいいかも。
カーネルサイズはさっきと同じ(3,3)で。

>>> img1_dilation = cv2.dilate(img1_binary, kernel, iterations=1)
>>> cv2.imshow('dilation', img1_dilation)

f:id:nokixa:20210425230126p:plain

単純にさっきの反転&収縮の反転になっているようです。

オープニング

ノイズ除去に効果的とのことですが、今回はシール上部の細かい文字を消せるのでは、と期待します。 収縮→膨張の処理をするとのことなので、反転画像に対して処理をかけてみます。
カーネルサイズはまたまたさっきと同じ(3,3)。

>>> img1_opening = cv2.morphologyEx(img1_inv, cv2.MORPH_OPEN, kernel)
>>> cv2.imshow('opening_inv', img1_opening)

f:id:nokixa:20210425230839p:plain

確かにさっきまでの処理画像を見ると少し細かい文字の点が残っていたので、その影響でカーネルサイズ(3,3)のドットが現れています。

(5,5)のカーネルでやってみます。

>>> kernel  = np.ones((5,5), np.uint8)
>>> img1_opening = cv2.morphologyEx(img1_inv, cv2.MORPH_OPEN, kernel)
>>> cv2.imshow('opening_inv', img1_opening)

f:id:nokixa:20210425232239p:plain

これはやりすぎか。
(4,4)のカーネルとかはできるのか?

>>> kernel  = np.ones((4,4), np.uint8)
>>> img1_opening = cv2.morphologyEx(img1_inv, cv2.MORPH_OPEN, kernel)
>>> cv2.imshow('opening_inv', img1_opening)

f:id:nokixa:20210425232408p:plain

処理自体はできましたが、0.5点の文字がうまく取れませんでした。

画像の解像度不足な気がするので、解像度を倍にして試してみる。
カーネルサイズは、今までの画像での(3,3)、(4,4)の中間になるよう、(7,7)にしてみます。

>>> img1 = cv2.imread('harupan_200317_1.jpg')
>>> img1 = cv2.resize(img1, None, fx=0.2, fy=0.2, interpolation=cv2.INTER_AREA)
>>> img1_hsv = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV)
>>> ret, img1_binary = cv2.threshold(img1_hsv[:,:,0], 160, 255, cv2.THRESH_BINARY)
>>> kernel = np.ones((7,7), np.uint8)
>>> img1_inv = cv2.bitwise_not(img1_binary)
>>> img1_opening = cv2.morphologyEx(img1_inv, cv2.MORPH_OPEN, kernel)
>>> cv2.imshow('opening_inv', img1_opening)

f:id:nokixa:20210425233144p:plain

やっぱり0.5点の文字が微妙です。細かい文字はいい感じにほぼ消えましたが。

ここまで

モルフォロジー変換はここまでにします。
色々と処理を組み合わせるといい感じの結果が出せそうです。

もう少しチュートリアル通りに進めて、次はエッジ検出をやってみたいと思います。

OpenCVやってみる-6. 画像の平滑化

前回の続きをやっていきます。
チュートリアルのタイトルは「画像の平滑化」で、内容としては画像にフィルタをかけて平滑化する、というものになります。

画像の平滑化 — OpenCV-Python Tutorials 1 documentation

ターゲット画像

前回と同じ画像を使います。

>>> img1 = cv2.imread('harupan_200317_1.jpg')
>>> v, h = img1.shape[:2]
>>> size = (int(h/10), int(v/10))
>>> img1 = cv2.resize(img1, size, interpolation=cv2.INTER_AREA)

f:id:nokixa:20210416081520p:plain

これに色々処理をかけてみたいと思います。

filter2D()を使う

まずはfilter2D()関数を使っての平均化フィルタ処理。
filter2D()関数自体は任意のカーネルと画像の畳み込みを計算してくれるので、汎用性が高そうです。
ここでは、5x5の領域での平均化フィルタをかけています。

チュートリアルを見ると、matplotlibのsubplotを使って複数画像の表示をしていたので、これを使ってみます。

ただし、OpenCVではカラー画像をBGRフォーマット(チャネル0が青、チャネル1が緑、チャネル2が赤)で扱いますが、matplotlibではRGB(青と赤が逆)になっているので、あらかじめ変換しておきます。cvtColor()を使います。

>>> img1_rgb = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)
>>> import numpy as np
>>> kernel = np.ones((5,5), np.float32)/25
>>> img1_filterD = cv2.filter2D(img1_rgb, -1, kernel)
>>>
>>> from matplotlib import pyplot as plt
>>> plt.subplot(121), plt.imshow(img1_rgb), plt.title('original'), plt.xticks([]), plt.yticks([])
(<AxesSubplot:title={'center':'original'}>, <matplotlib.image.AxesImage object at 0x000001762E327AC8>, Text(0.5, 1.0, 'original'), ([], []), ([], []))
>>> plt.subplot(122), plt.imshow(img1_filter2D), plt.title('filter2D'), plt.xticks([]), plt.yticks([])
(<AxesSubplot:title={'center':'filter2D'}>, <matplotlib.image.AxesImage object at 0x000001762BD7EDC8>, Text(0.5, 1.0, 'filter2D'), ([], []), ([], []))
>>> plt.show()

f:id:nokixa:20210416082610p:plain

しっかりぼけました。

subplot()の詳細は以下を参考にさせてもらいました。

subplot | Python で一枚のプロットに複数グラフを描く方法

subplot()の引数は、左の桁から順に分割行数、分割列数、アクティベートするサブ領域番号となるようです。
カンマで区切っても、3桁の数値として入れてもいいよう。
あとは軸目盛は表示したくないので、xticks(), yticks()で消しておきます。

blur()で平均処理

filter2D()でやったのと同じことを、blur()を使ってやります。

>>> img1_blur = cv2.blur(img1_rgb, (5,5))

f:id:nokixa:20210416083137p:plain

同じような画像になっています。

ガウシアンフィルタ

上記と同じカーネルサイズのガウシアンフィルタをかけてみます。

>>> img1_GaussianBlur = cv2.GaussianBlur(img1_rgb, (5,5), 0)

f:id:nokixa:20210416083344p:plain

ガウシアンフィルタだとカーネルの中央により重みがあるので、ぼけ具合が少し小さくなっています。

バイラテラルフィルタ

エッジを保存しつつノイズを除去するのに適したフィルタとのこと。

python+OpenCVでエッジを保存した平滑化(BilateralFilter, NLMeansFilter) - Qiita

Bilateral Filtering

>>> img1_bilateral = cv2.bilateralFilter(img1_rgb, 9, 75, 75)

f:id:nokixa:20210416083613p:plain

フローリングの細かい模様はなめらかになっていますが、シールの境界、台紙の境界等ははっきりしたままです。
すごいですね。
bilateralFilter()の引数がいくつかありますが、また必要になったら調べようかな。

ここまで

ほぼチュートリアル通りの内容を、自分の画像でやってみただけになりました。
シール点数集計には直接役に立たないかもしれません。
filter2D()は汎用性がありそうなので使えるかな?

次回もチュートリアル通りで、モルフォロジー変換をやろうと思います。

OpenCVやってみる-5. 2値化

画像の2値化(閾値処理)をやってみます。
ヒストグラムも表示してみます。

ヒストグラム

2値化するのに、まずどんな閾値を設定すればいいのか見るために、ヒストグラムを表示してみます。

今回は、以下の2つの画像を使います。
2020年と2021年の春のパン祭り台紙+シールです。シールの模様と色が少し違うみたいです。

>>> import cv2
>>> img1 = cv2.imread('harupan_200317_1.jpg')
>>> img2 = cv2.imread('harupan_210402_1.jpg')
>>> v,h = img1.shape[:2]
>>> size = (int(h/10), int(v/10))
>>> img1 = cv2.resize(img1, size, interpolation=cv2.INTER_AREA)
>>> img2 = cv2.resize(img2, size, interpolation=cv2.INTER_AREA)
>>> import numpy as np
>>> stackedImg = np.hstack((img1, img2))
>>> cv2.imshow('harupan', stackedImg)
>>> cv2.waitKey(0)

f:id:nokixa:20210412090122p:plain

2画像の連結でまとめて表示しました。

Numpy の vstack、hstack で配列を連結-python | コード7区

シールの色が、特定の1色になっているので、HSVフォーマットで色相(Hue)を見てやると良さそうな気がします。

>>> img1_hsv = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV)
>>> img2_hsv = cv2.cvtColor(img2, cv2.COLOR_BGR2HSV)

cv2.calcHist()関数でヒストグラムを計算、matplotlibで表示してみます。importする必要があります。

>>> hist1_hue = cv2.calcHist([img1_hsv], [0], None, [180], [0, 180])
>>> hist2_hue = cv2.calcHist([img2_hsv], [0], None, [180], [0, 180])
>>> from matplotlib import pyplot as plt
>>> plt.plot(hist1_hue, color='b')
[<matplotlib.lines.Line2D object at 0x000002256AF43808>]
>>> plt.plot(hist2_hue, color='g')
[<matplotlib.lines.Line2D object at 0x000002256AF45808>]
>>> plt.xlim([0,180])
(0.0, 180.0)
>>> plt.show()

f:id:nokixa:20210413234456p:plain

結構鋭いピークが出ています。

色相は360°の角度等、ぐるっと回るような値で表され、赤→黄→緑→水色→青→紫→赤といった形で回っていくようです。
今回は0~180の値が割り当てられているので、0が赤、60が緑、120が青、といった形になるよう。

HSV色空間とは : PEKO STEP

今回は、シールのピンクのような、赤のような色を取り出したいと思いますが、どちらかというと紫寄りの色に思えるので、170、180あたりのピークがそれではないかと思われます。
20あたりのピークはフローリングの色かな?

このHue画像を閾値160ぐらいで2値化してやればシール領域(および台紙のピンク領域)が取り出せるのでは?
やってみます。

>>> img1_hsv.shape
(403, 302, 3)
>>> ret, thresh1 = cv2.threshold(img1_hsv[:,:,0], 160, 255, cv2.THRESH_BINARY)
>>> ret, thresh2 = cv2.threshold(img2_hsv[:,:,0], 160, 255, cv2.THRESH_BINARY)
>>> thresh1.shape
(403, 302)
>>> thresh = np.hstack((thresh1, thresh2))
>>> cv2.imshow('thresh', thresh)

f:id:nokixa:20210413235038p:plain

いい感じにシール領域が取れました。

シール台紙領域

ついでに、フローリング領域も分離して、シール台紙領域も検出してみたいと思います。

Hueの値0付近はシール領域な気がするのと、40付近のなだらかなピークはフローリングでの光の反射な気がするので、Hueが10~60のあたりを取ってみたいと思います。

cv2.threshold()では閾値以上、以下の2分類しかできないので、ビット単位処理での組み合わせを使います。

画像の算術演算 — OpenCV-Python Tutorials 1 documentation

>>> ret, thresh1_gt10 = cv2.threshold(img1_hsv[:,:,0], 10, 255, cv2.THRESH_BINARY)
>>> ret, thresh1_lt60 = cv2.threshold(img1_hsv[:,:,0], 60, 255, cv2.THRESH_BINARY_INV)
>>> ret, thresh2_gt10 = cv2.threshold(img2_hsv[:,:,0], 10, 255, cv2.THRESH_BINARY)
>>> ret, thresh2_lt60 = cv2.threshold(img2_hsv[:,:,0], 60, 255, cv2.THRESH_BINARY_INV)
>>> thresh1_10to60 = cv2.bitwise_and(thresh1_gt10, thresh1_lt60)
>>> thresh2_10to60 = cv2.bitwise_and(thresh2_gt10, thresh2_lt60)
>>> thresh = np.hstack((thresh1_10to60, thresh2_10to60))
>>> cv2.imshow('thresh', thresh)

f:id:nokixa:20210414000253p:plain

シール台紙領域も一緒に入ってしまった…

彩度でシール台紙領域

シール台紙領域は白い色になっていますが、白、グレー、黒といった色だとHue(色相)の値は不定になってしまうとのこと。

HSV色空間 - Wikipedia

HSVのS(Saturation:彩度)も使ってみます。

まずはヒストグラム

>>> hist1_sat = cv2.calcHist([img1_hsv], [1], None, [256], [0, 256])
>>> hist2_sat = cv2.calcHist([img2_hsv], [1], None, [256], [0, 256])
>>> plt.plot(hist1_sat, color='b')
[<matplotlib.lines.Line2D object at 0x000002256DD8B8C8>]
>>> plt.plot(hist2_sat, color='g')
[<matplotlib.lines.Line2D object at 0x000002256DDBF448>]
>>> plt.xlim([0, 256])
(0.0, 256.0)
>>> plt.show()

f:id:nokixa:20210414001818p:plain

彩度が低いところに鋭いピークがあります。これがシール台紙領域では?

彩度で2値化してみます。

>>> ret, thresh1_sat = cv2.threshold(img1_hsv[:,:,1], 50, 255, cv2.THRESH_BINARY_INV)
>>> ret, thresh2_sat = cv2.threshold(img2_hsv[:,:,1], 50, 255, cv2.THRESH_BINARY_INV)
>>> thresh = np.hstack((thresh1_sat, thresh2_sat))
>>> cv2.imshow('thresh', thresh)

f:id:nokixa:20210414002312p:plain

シール台紙領域がいい感じに取れたと思います。
ただ、左画像のフローリングの光の反射も拾ってしまった…
これは今のところ除外する方法が思いつきません。要リサーチ。

合成

最後にシール領域、シール台紙領域、その他の領域、と分類した画像を作ってみたいと思います。

>>> thresh_a = np.hstack((thresh1, thresh2))
>>> thresh_b = np.hstack((thresh1_sat_gray, thresh2_sat_gray))
>>> thresh_all = cv2.bitwise_or(thresh_a, thresh_b)
>>> cv2.imshow('thresh_all', thresh_all)
>>> cv2.waitKey(0)

f:id:nokixa:20210414083718p:plain

白い部分がシール領域、グレーの部分がシール台紙領域(シールの白い部分も含む)です。

ここまで

以上、ヒストグラムと2値化をやってみました。
チュートリアルでは適応的閾値処理、大津の2値化というのもありますが、 必要に応じてまた調べながらやってみたいと思います。

次回はチュートリアル通り画像の平滑化、としたいと思います。

OpenCVやってみる-4. 射影変換

今回は、画像の射影変換をやってみたいと思います。

射影変換

春のパン祭り得点集計では、射影変換が活用できそうなのでやってみます。
射影変換が何をやっているかは前に勉強していたのですが、完全に忘れた…

また復習して記事にします。

ひとまず、ある視点から見た画像を、別の視点から見た画像に変換する、というものです。
シール台紙を撮影するとき、多少斜めからの撮影になるので、これを正面からの画像に変換します。

OpenCVチュートリアルの以下を参照。

画像の幾何変換 — OpenCV-Python Tutorials 1 documentation

以下の画像を使いました。

>>> img1 = cv2.imread('harupan_210402_1.jpg')
>>> img2 = cv2.imread('harupan_210402_2.jpg')
>>> h,w = img1.shape[:2]
>>> img1 = cv2.resize(img1, (int(w/10), int(h/10)), interpolation=cv2.INTER_AREA)
>>> img2 = cv2.resize(img2, (int(w/10), int(h/10)), interpolation=cv2.INTER_AREA)
>>> cv2.imshow('image1', img1)
>>> cv2.waitKey(0)
13
>>> cv2.destroyAllWindows()
>>> cv2.imshow('image2', img2)
>>> cv2.waitKey(0)
13

f:id:nokixa:20210410222841p:plain

f:id:nokixa:20210410222932p:plain

射影変換で正面から見た画像に変換したいと思いますが、それには

  • 元画像での4点の座標
  • この4つの点の変換先座標

を指定する必要があります。
元画像の点としてシール台紙の4つの角を指定、変換先の座標としては長方形の4点を指定します。

元画像でのシール台紙の角は、手動で探します。ImageJを使いました。カーソルを当てた位置の座標を表示してくれます。
最終的には自動検出するようにしたい。

ImageJ

f:id:nokixa:20210410234332p:plain

1枚目 : (774,702), (444,3222), (2664,3282), (2394,762)

2枚目 : (978,978), (570,2934), (2190,3138), (2304,918)

※元画像はいずれも横3024pixel、縦4032pixelです。
縦横1/10にした画像で変換を行います。

長方形の4点を指定するためには縦横比が必要なので、実際に測ります。
16.0cm x 11.0cmでした。

正しく射影変換するためには、画像に既知の形状の物体が映っていないとだめということですね。

変換実施

射影変換やってみました。
変換先画像は、320×220の解像度にしました。

>>> import numpy as np
>>> 
>>> pts1_org = np.float32([[77,70],[44,322],[266,328],[239,76]])
>>> pts1_dst = np.float32(([[0,0],[0,160],[110,160],[110,0]]))
>>> M = cv2.getPerspectiveTransform(pts1_org, pts1_dst*2)
>>> img1_dst = cv2.warpPerspective(img1,M,(110*2,160*2))
>>> cv2.imshow('image1_dst', img1_dst)
>>> cv2.waitKey(0)
13
>>> cv2.destroyAllWindows()
>>> pts2_org = np.float32([[98,98],[57,293],[219,314],[230,92]])
>>> pts2_dst = np.float32(([[0,0],[0,160],[110,160],[110,0]]))
>>> M = cv2.getPerspectiveTransform(pts2_org, pts2_dst*2)
>>> img2_dst = cv2.warpPerspective(img2,M,(110*2,160*2))
>>> cv2.imshow('image2_dst', img2_dst)
>>> cv2.waitKey(0)

f:id:nokixa:20210410235510p:plain

f:id:nokixa:20210411000028p:plain

  • およそそれらしく変換できたかと。お手軽ですね。
  • シール台紙の角が少しめくれあがっていて、変換結果が引き伸ばされたようなちょっと微妙な感じに。
  • とは言えシール点数計算には十分な結果では?

ここまで

フィルタ処理をやろうと考えていましたが、OpenCVチュートリアルを見ると先に「幾何変換」の内容が来ていて、今回射影変換をやりました。

チュートリアルを見ると、次は「画像の閾値処理」だったので、これをやってみようと思います。

OpenCVやってみる-3. 色々いじる

今回は、画像に色々手を加えてみたいと思います。

参考 :

OpenCV-Pythonチュートリアル — OpenCV-Python Tutorials 1 documentation

Python OpenCVの基礎 resieで画像サイズを変えてみる - Pythonの学習の過程とか

画像切り出し

シール1枚分の領域を切り出してみました。

>>> img3 = img[500:1000, 500:1000]
>>> img3.shape
(500, 500, 3)
>>> cv2.imshow('image3', img3)
>>> cv2.waitKey(0)

f:id:nokixa:20210319070922p:plain

resize色々

resizeで、補間方法のオプションがあるので試してみます。

>>> v,h = img.shape[:2]
>>> size = (int(h/10), int(v/10))
>>> img4 = cv2.resize(img, size, interpolation=cv2.INTER_NEAREST)
>>> cv2.imshow('image4', img4)
>>> cv2.waitKey(0)
13
>>> img4 = cv2.resize(img, size, interpolation=cv2.INTER_LINEAR)
>>> cv2.imshow('image4', img4)
>>> cv2.waitKey(0)
13
>>>
>>> img4 = cv2.resize(img, size, interpolation=cv2.INTER_AREA)
>>> cv2.imshow('image4', img4)
>>> cv2.waitKey(0)
13
>>> img4 = cv2.resize(img, size, interpolation=cv2.INTER_CUBIC)
>>> cv2.imshow('image4', img4)
>>> cv2.waitKey(0)
13
>>> img4 = cv2.resize(img, size, interpolation=cv2.INTER_LANCZOS4)
>>> cv2.imshow('image4', img4)
>>> cv2.waitKey(0)
13
>>>

f:id:nokixa:20210319073135p:plain
INTER_NEARESTで

f:id:nokixa:20210319073334p:plain
INTER_LINEARで

f:id:nokixa:20210319073528p:plain
INTER_AREAで

f:id:nokixa:20210319073615p:plain
INTER_CUBICで

f:id:nokixa:20210319073719p:plain
INTER_LANCZOS4で

INTER_AREAの設定だと、他の設定に比べてなめらかな画像になっているように見えます。

  • シールの枠や点数の輪郭のぎざぎざが少ない
  • 下のほうの記入枠の細い線がちゃんと見える (他の設定は線の途中が切れている)

ぼかされているとも言えるのかも。

色空間の変換

上記の画像を使います。
まず、最初の読み込みではRGBフォーマットになっています。
読み込みデータの形状は、(垂直サイズ, 水平サイズ, 色)となっています。

>>> img4.shape
(403, 302, 3)

色は、BGRの順番になっているようです。

>>> cv2.imshow('image4_0', img4[:,:,0])
>>> cv2.waitKey(0)
13
>>> cv2.imshow('image4_1', img4[:,:,1])
>>> cv2.waitKey(0)
13
>>> cv2.imshow('image4_2', img4[:,:,2])
>>> cv2.waitKey(0)
13

f:id:nokixa:20210322072622p:plain

色フォーマットの変換をしてみます。
cv2.cvtColor()関数で変換ができます。グレースケールとHSV空間への変換を試してみたいと思います。

>>> img_gray = cv2.cvtColor(img4, cv2.COLOR_BGR2GRAY)
>>> img_gray.shape
(403, 302)
>>> cv2.imshow('Gray image',img_gray)
>>> cv2.waitKey(0)
13
>>> 

f:id:nokixa:20210325075025p:plain

>>> img_hsv = cv2.cvtColor(img4, cv2.COLOR_BGR2HSV)
>>> img_hsv.shape
(403, 302, 3)
>>> cv2.imshow('Hue',img_hsv[:,:,0])
>>> cv2.waitKey(0)
13
>>> cv2.imshow('Saturation',img_hsv[:,:,1])
>>> cv2.waitKey(0)
13
>>> cv2.imshow('Value',img_hsv[:,:,2])
>>> cv2.waitKey(0)
13
>>>

f:id:nokixa:20210325075925p:plain

HSVの各成分の意味は、

HSVの各成分はそれぞれ,Hueが色相,Saturation(Chroma)が彩度,Value(Lightness)が明度を意味します。

とのことです。
今回の画像ではちょっと分かりにくいですね…

labs.eecs.tottori-u.ac.jp

図形の描画

直線、円、矩形を描画してみます。
使うのは、cv2.line()cv2.circle()cv2.rectangle()関数です。

直線

点(10,10)から(290,10)まで、真っ青で3pxの太さの線を描画

  • 座標は、(水平, 垂直)の順、画像の左上が原点となる
  • 上記の座標では、水平な線が描画される
>>> img4_drawing = cv2.line(img4, (10,10), (290,10), (255,0,0),3)

f:id:nokixa:20210331224437p:plain
直線の描画

点(20,20)を中心として半径10、緑色の円を描画

  • 最後の引数は線の太さ、-1を指定すると塗りつぶしになる
>>> img4_drawing = cv2.circle(img4_drawing, (20,20), 10, (0,255,0), -1)

f:id:nokixa:20210331225426p:plain
円の描画

矩形

左上の角の座標が(50,50)、右下の角の座標が(100,100)、赤で5pxの太さの矩形を描画

  • この記事の最初のほうでやったシール1枚の領域を矩形で囲みます。
>>> img4_drawing = cv2.rectangle(img4_drawing, (50,50), (100,100), (0,0,255), 5)

f:id:nokixa:20210331230349p:plain
矩形の描画

ここまで

まだまだやってみたいことはありますが、一旦ここまでにします。
次回は、フィルタをかけたりしてみたい。
だんだん画像処理感が出てくると思います。