2024-2I プログラミング1 第21回 講義資料

2024年11月29日(金)1・2時限

1 概要・連絡

1.1 連絡事項

1.2 今回講義の達成目標

今回の講義では OpenCV を利用した画像処理の基礎について学びます。

1.3 プログラミング3の作品共有

3年後期のプログラミング3(ウェブアプリ開発)の 課題1 オリジナルTodoアプリの開発 の学生作品を共有します。ウェブ系のエンジニアを目指す学生は、授業を待たずに先取りして学んでください。

1.4 参考: データ処理と可視化の例

前回、前々回で学んだ numpy、pandas、matplotlib の組み合わせの応用例としてAmbientからダウンロードしたCSV (=Comma Separated Values) を対象とした データ処理可視化 (グラフ化) のサンプルを共有しておきます。

2 コンピュータにおける画像の扱いについて

コンピュータにおける画像の表現や扱いについては、1年生の「情報1」、2年生の「情報2」「メディアデザイン入門」で既に学んでいる内容ですが、改めて復習しておきます。

2.1 画像形式 (ラスター形式とベクター形式)

コンピュータで扱う画像の「形式」には、大きく分けてラスター形式 (ビットマップ形式)ベクター形式 があります。

ラスター形式 (ビットマップ形式) は、格子状に配置したピクセル (点) の集合として表現した形式で、各ピクセルの色や透明度の集合として構成されます。写真などの複雑な画像にも適した形式となっています。具体的なフォーマットとしてはJPEG,PNG,HEIF/HEIC,GIF,BMPなどがあります。

img

ベクター形式 は、点、線、図形などを定義した幾何学的な形状情報(例えば、図形の頂点座標や線幅、端点の形状などの情報)で構成される形式で、拡大や縮小などの操作をしても画質が劣化しないこと が特徴となっています。そのため、主に「CAD図面」や「テクニカルイラスト」などに使われる形式となっています。また、「フォント」もベクター形式のデータと言えます。具体的なフォーマットとしてはSVG,PDF,EPS,AIなどがあります。

なお、ベクター形式であっても実際にディスプレイに表示する際にはコンピュータの内部でラスター画像(ピクセルベース)に変換 されています。この変換のことを ラスタライゼーション といいます。

代表的なラスター形式の画像フォーマット

2.2 ラスター形式における色情報の管理

ラスター形式の画像において「カラー画像」の表現には、RGB色モデル (RGBカラーモデル) が一般的に用いられます。このモデルでは「赤 (Red)」「緑 (Green)」「青 (Blue)」という 光の三原色 の輝度 (光の強度) の組み合わせで、様々な「色」をつくりだします。一般に「フルカラー」と呼ばれる画像では、各色の輝度を 8ビット (=1バイト) 、つまり 0 から 255 までの 256諧調 で表現して、その組み合わせとして 約1670万色 を表現 (発色) します。

\[ 256\times256\times256 = 16,777,216\]

このようなことから、圧縮せずにカラー画像を扱う場合、1ピクセルにつき「3バイト」の情報が必要になります。また、256諧調の グレースケール画像 では、1ピクセルにつき「1バイト」の情報が必要になります。

なお、多くの場合、カラー画像は「RGB (赤・緑・青)」の順に輝度を並べて色情報を扱いますが、OpenCV においてはとある理由 から「BGR (青・緑・赤)」 という順 (RGBとは逆順) に並べて色情報を管理しています。この点には十分に注意してください。

img

HSV色モデル

2年後期の「メディアデザイン入門」でも学んでいるように、色の表現としては「HSV色モデル」もあります。HSVは「色相 (Hue)」「彩度 (Saturation)」「明度 (Value)」の3つの要素の組み合わせで色を表現します。

肌色の検出のような画像処理タスクでは、RGBモデルよりも、HSVモデルが特に有効です。肌色 (=ペールオレンジ、ベージュ) は特定の色相 (Hue) の範囲にあるため、HSVモデルであれば主に色相の情報を使って比較的容易に肌色を検出することができます。以下は、OpenCVでHSVモデルをベースに、HSVの範囲フィルタによって「肌色抽出」を実行した例です。床や机などの似た色が多いなかで、概ね肌色を検出できていることが分かると思います。なお、以下の左側画像Bing Image Creatorにより作成しています。

img

なお、「HSVモデル」と「RGBモデル」は相互変換可能であり、OpenCVを含めて一般的な画像処理ソフトでは両方のモデルをサポートしています。

3 OpenCVを利用した画像処理

OpenCV を利用した「画像処理」の基礎について学んでいきます。ここで学んだ内容は「知能情報実験実習1」の後期テーマのなかでも使用します。

3.1 OpenCVとは

OpenCVOpen Source Computer Vision Library)は 「画像処理」「コンピュータビジョン」 に特化したオープンソースのライブラリです。1999年に Intel によって開発され、リアルタイムでの高速画像処理を可能にするために最適化されています。また、その「汎用性」と「拡張性」により幅広いプラットフォームで使用できる点にも特徴があります。

OpenCVは、本科目で学んでいる Python の他、C++Java などの 様々なプログラミング言語 に対応しており、また、Windows、Linux、Mac OSといった主要 OS、Raspberry PiNVIDIA Jetson などのアーキテクチャにも対応しています。このため「組込みシステム」や「IoTデバイス」でも使用されることがあります。

組込みシステムとは

組込みシステム (Embedded System) とは、パソコンやスマートフォンとは異なり、特定の機能を実行するためだけに設計されたコンピュータシステムを指します。例えば、家庭で使われる「洗濯機」「テレビのリモコン」「車のエンジンを制御するコンピュータ」「ゲーム機」などが 組込みシステム の典型的な例となります。このほか「信号機」や「自動販売機」「産業用加工機械」「エレベータ」なども組込みシステムの一例となります。これらの製品には、センサーやアクチュエータと連動して動作を制御するための 小型コンピュータが組み込まれており、この特徴から「組込みシステム」と呼ばれています。

また、OpenCVは 画像の取得 (読み込み)処理分析、顔認識、物体検出などの幅広い機能を提供しており、これらの機能はセキュリティ、自動運転車、ロボティクス、医療画像解析など多岐にわたる分野で応用されています。

OpenCV の「基礎」を習得しておくことは、3年生以降に開講される「マルチメディア情報処理(画像処理)」や「人工知能 (画像分類や物体認識)」を学ぶうえでも非常に役立ちます。これらの分野は、座学として学ぶだけではなく実際に手を動かしてプログラムを実装することで理解が深まり、また楽しく学ぶことができます。

「画像処理」と「コンピュータビジョン」の違い

画像処理」と「コンピュータビジョン」は、いずれも画像に関連する分野・技術ですが、その目的や、焦点としているところが違っています。

画像処理」は、画像の品質を向上させたり、特定の情報を抽出することに焦点を置いています。具体的には、画像のコントラストを調整したり、ノイズを除去して画像をシャープにしたり、色を補正するなど、画像に対する変更や操作が行われます。画像処理の主な目的は、画像をより分析しやすくしたり、人間やコンピュータに対してより見やすくすることにあります。

一方で「コンピュータビジョン (CV)」は「画像や動画から情報を理解・解釈すること」に焦点が置かれています。画像から物体を検出・識別・追跡したり、シーンの3D構造を理解するなど、コンピュータが人間のように視覚情報を「理解」し、それをもとに意思決定・行動をできるようにすること を目的としています。具体例としては、顔認識、物体検出、自動運転などがあります。

なお、実際の応用では、まず「画像処理」によって画像が最適化され、その後、「コンピュータビジョン」によって画像から情報を分析するという関係になっています。

3.2 ライブラリのインストール

OpenCVは、Pythonの標準ライブラリには含まれていませんが、GoogleColab環境においてはデフォルトでインストールされています。GoogleColabのコードセルで以下の Linuxコマンド を実行することで、インストールされている OpenCV のバージョン情報の確認ができます。なお、grep指定した文字列を含む行を表示 するための Linuxコマンド です。

!pip list | grep opencv

2024年11月27時点での実行結果は、次のようになります。opencv-python が標準パッケージで、opencv-contrib-python は標準パッケージに加えて各種追加モジュールも含んだパッケージになります。またopencv-python-headless は、GUI関連の機能を含まないパッケージになります。

pencv-contrib-python              4.10.0.84
opencv-python                     4.10.0.84
opencv-python-headless            4.10.0.84

一方で、ローカル環境では 手動で OpenCV をインストールする必要 があります。Python の仮想環境を有効化し、pip を最新版にアップグレードしてから、OpenCV をインストールしてください。今回の講義ではローカル環境も利用するため、全員がインストールしてください。環境構築の詳細は第09回講義を参照してください。

.venv/Scripts/Activate.ps1
python -m pip install --upgrade pip
pip install opencv-python

Windows環境 (PowerShell) では、以下のコマンドで特定パッケージ (ライブラリ) のバージョンを確認できます。

pip list | Select-String "opencv"

正常にインストールできたかどうかは、import cv2 を含んだ Python プログラムを実行することで確認できます。実行して ModuleNotFoundError: No module named 'cv2' が発生しなければ、正常にインストールができています。

3.3 画像の読込みとデータ型の確認

Python から OpenCV を利用する場合の特徴として、画像データNumPy の ndarrayオブジェクト として扱われることが挙げられます。

具体的には、横640px縦480pxの「カラー画像」は shape(480,640,3) のndarrayオブジェクト、簡単に言えば 3次元配列 となります。ここで、(640,480,3) ではなく (480,640,3) であることに注意してください (順番に注意してください)。

また「グレースケール画像」は shape(480,640) の ndarrayオブジェクト、簡単に言えば「2次元配列」となります。

実際に、ローカル環境 (非Jupyter環境) で、これらを確認してみます。次の画像 img-01.jpg をダウンロードして、プログラムと同じにフォルダに配置してください。この画像は 横640px縦480px のJPEG形式のカラー画像になります。

img

なお、第19回講義で学んだように、次のようなプログラムでで ウェブからファイルをダウンロードしてカレントフォルダに保存すること もできます (GoogleColab環境では、この方法のほうが便利です)。

import requests 

fn = 'img-01.jpg'
url = 'https://takeshiwada1980.github.io/Programming1-2024/figs/21/img-01.jpg'
res = requests.get(url)

if res.status_code != 200:
  raise Exception(f'ファイルのDLに失敗。強制終了します。Code:{res.status_code}')
else :
  with open(fn,'wb') as file: 
    file.write(res.content)
  print(f'カレントフォルダに {fn} を保存しました')

カレントフォルダに配置した img-01.jpg について確認するために、次のプログラムを作成して実行してください。

import cv2

fn = 'img-01.jpg'
img = cv2.imread(fn)
print(f'type  => {type(img)}')
print(f'ndim  => {img.ndim}')
print(f'dtype => {img.dtype}')
print(f'shape => {img.shape}')

実行結果は次のようになります。

type  => <class 'numpy.ndarray'>
ndim  => 3
dtype => uint8
shape => (480, 640, 3)

ここで dtypeuint8 は「符号なし8ビット整数 (Unsigned 8-bit Integer)」を意味します。この uint8 型では 0 から 255 までの整数値 を使うことができます。

次に「グレースケール画像」の場合にどのようになるかを試してみます。OpenCV では cv2.imread 関数でファイルからデータを読み込む際に、引数で「カラー形式を指定」できます。ここでは、グレースケール画像として読み込むために、第2引数に cv2.IMREAD_GRAYSCALE を指定します。先のプログラムの 第04行目img = cv2.imread(fn,cv2.IMREAD_GRAYSCALE) に書き換えてプログラムを実行してみてください。

実行結果は次のようになります。

type  => <class 'numpy.ndarray'>
ndim  => 2
dtype => uint8
shape => (480, 640)

グレースケール画像として読み込むことで、img2次元配列になっていることが確認できます。

3.4 画像の表示

OpenCV では cv2.imshow 関数により、以下の画像のように ウィンドウ を立ち上げて画像を表示させることができます。しかし、この cv2.imshowGUI 関連の処理 を含むため ローカルに構築した Jupyter環境 や GoogleColab環境 では利用すること ができません。また、プログラム終了とともにウィンドウも消えるため、あとから画像を確認したいような場合にも適しません (画像処理の前後を比較したい場合などに非常に不便です)。

img

cv2.imshow には以上のような「使いにくさ」があるため、Jupyter環境 や GoogleColab環境 なども含めて画像を表示する方法について解説します。いずれの方法も 一長一短 です。各自で目的や環境・状況にあわせて使い分けられるようになってください

3.4.1 表示方法1

cv2.imshow を使ってウィンドウに画像を表示する方法です。この方法は Jupyter環境 や GoogleColab環境 では使用できません。この方法では、ウィンドウが閉じられるまでプログラムをブロックします。つまり、ウィンドウが閉じられるまで my_imshow 関数以降のプログラムが実行されません

実際に試してみてください。特に、ウィンドウが閉じられるまでほげほげという文字列が出力されない (=プログラムがブロックされること) を確認してください。

import cv2

def my_imshow(img,wn='Image'):
  cv2.imshow(wn, img)
  while True:
    # ESCキーが押下されたら閉じる
    if cv2.waitKey(10) == 27:
      break
    # 閉じるボタンが押されたら閉じる
    if cv2.getWindowProperty(wn,cv2.WND_PROP_VISIBLE) < 1:
      break
  cv2.destroyAllWindows()

img = cv2.imread('img-01.jpg')
my_imshow(img) # ■■■ 画像の表示 ■■■
print('ほげほげ')

3.4.2 表示方法2

この方法も Jupyter環境 や GoogleColab環境 では使用できません方法1 との違いは「プログラムがブロックされないこと」と「プログラムが終了してもウィンドウが残っていること」です。

import cv2
from PIL import Image  # 要import

def my_imshow(img):
  if img.ndim == 2:
    im_pil = Image.fromarray(img)
  else:
    tmp = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
    im_pil = Image.fromarray(tmp)
  im_pil.show()

img = cv2.imread('img-01.jpg')
my_imshow(img) # ■■■ 画像の表示 ■■■
print('ほげほげ')

実行結果は次のようになります。もし、ModuleNotFoundError が発生した場合は、pip で必要なライブラリをインストールしてください (エラーメッセージをそのまま検索すれば、解決法が見つかります)。

img

この 表示方法2 では、PNGファイルに関連づいている「既定のアプリ」が起動して、そこに画像が表示されます。デフォルトの Windows環境 では標準アプリの「フォト」が起動します。「既定のアプリ」は、Windowsの「スタートボタン」を右クリックして「設定」-「アプリ」から確認することができます。

img

3.4.3 表示方法3

この方法は、可視化ライブラリである Matplotlib を利用したもので、環境を問わずに、非Jupyter環境、Jupyter環境、GoogleColab環境 のいずれでも使用することができます。この方法のデメリットとしては「処理が重いこと」「Matplotlibによって加工された画像になってしまうこと」などがあります。この方法により表示される画像は、もともとの画像とは異なる解像度 (縦横のピクセル数) になってしまいます。

import cv2
import matplotlib.pyplot as plt  # 要import

def my_imshow(img):
  if img.ndim == 2:
    plt.imshow(img,cmap='gray')
  else:
    tmp = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
    plt.imshow(tmp)
  plt.show()

img = cv2.imread('img-01.jpg')
my_imshow(img) # ■■■ 画像の表示 ■■■
print('ほげほげ')

ローカルの非Jupyter環境 で実行した場合は、次のようなウィンドウが表示され、そこで 拡大縮小などの操作 もできます。ただし、このウィンドウを閉じるまではプログラムはブロックされます。一方で、GoogleColab環境 および Jupyter環境 で実行しているときは、プログラムはブロックされません。

img

なお、この方法では Matplotlib の知識とスキルがあれば、以下のように 分析や考察に役立つ情報を付与 した画像を出力することもできます (これは論文や報告書を作成する際に非常に有用です)。

img

上記のようなグリッドは my_imshow 関数を次のように書き換えることで表示させることができます。

def my_imshow(img):
  fig,ax = plt.subplots()
  if img.ndim == 2:
    ax.imshow(img,cmap='gray')
  else:
    tmp = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
    ax.imshow(tmp)
  ax.set_xticks(range(0,img.shape[1]+1,20),minor=True)
  ax.set_yticks(range(0,img.shape[0]+1,20),minor=True)
  ax.grid(which='major',lw=0.5, c='#333')
  ax.grid(which='minor',lw=0.5, c='#888')
  plt.show()

3.4.4 表示方法4

この方法は GoogleColab環境だけ で利用できる方法です。第06行目cv2.imshow ではなく cv2_imshow となっていること、第03行目 のようなインポートが必要なことに注意してください。

%reset -f
import cv2
from google.colab.patches import cv2_imshow  # 要import

img = cv2.imread('img-01.jpg')
cv2_imshow(img)

3.4.5 表示方法5

この方法は GoogleColab環境 および Jupyter環境 だけで利用できる方法です。画像を一時ファイルに保存して、それを読込み、IPython.display.Image を使って「出力セル」に表示させる方法です。GoogleColab環境 では、cv2_imshow を使ったほうが処理が高速であり、この方法を使用するメリットはありません。

%reset -f
import cv2
from IPython.display import Image   # 要import

img = cv2.imread('img-01.jpg')
cv2.imwrite('tmp.jpg',img)          # 'tmp.jpg' に保存
Image(open('tmp.jpg','rb').read())  # 'tmp.jpg' を出力セルに表示

演習:「表示方法1」から「表示方法5」について実際に実行して結果を確認せよ。特に img = cv2.imread('img-01.jpg',cv2.IMREAD_GRAYSCALE) のように「グレースケール画像」として読み込んだ場合 (imgndim2 の場合) も問題なく機能することを確認せよ。

3.5 画像の保存

画像の保存は cv2.imwrite 関数を使用します。第1引数に「ファイル名」、第2引数に「ndarrayオブジェクト」を指定します。ファイル形式は、ファイル名に応じて自動的に決定されます。

import cv2
fn = 'img-01.jpg'
img = cv2.imread(fn)
img = cv2.flip(img,1)              # 左右反転
cv2.imwrite('img-01-flip.jpg',img) # 保存処理

このプログラムを実行すると、次のような画像 img-01-flip.jpg が出力されます。また、第05行目cv2.imwrite('img-01-flip.png',img) とすれば PNG形式の画像が出力されます。その他、出力可能な画像形式についてはリファレンスを参照してください。

img

演習: cv2.flip の第2引数を 0 および -1 に変更したときの結果について確認せよ。

3.6 カラー画像の構成と色成分の取得

OpenCVにおいて、カラー画像は (*,*,3) という3次元配列となります。実際に詳しくみていきます。まずは、画像の左上座標 X=0, Y=0 のピクセルの色を取得して、その値を出力してみます。

import cv2
fn = 'img-01.jpg'
img = cv2.imread(fn)
p = img[0,0] 
print(f'type => {type(p)}')
print(f'p    => {p}')

実行結果は次のようになります。なお、第04行目は、C言語の配列に対するアクセスのように p = img[0][0] と書くこともできますが、Python では p = img[0,0] と書いたほうが高速にアクセスが可能です。

type => <class 'numpy.ndarray'>
p    => [ 44 136  77]

既に述べたように、OpenCVでは BGR という順番に色情報が格納されるので、画像左上の X=0, Y=0 のピクセルは「青44」「緑136」「赤77」という色成分を持っていることがわかります。このことは、画像の左上が「草むら」であり、緑成分が大きいであろうという視覚的な情報からも確認できます。


演習1: 第04行目p = img[0][0] に書き換えても、問題なく実行できることを確認せよ。

演習2: 画像の右端の中央、つまり、X=639, Y=240 の色成分を確認せよ。また、この位置は「濃い茶色の毛の部分」であり、赤の成分が最も大きくなることを確認せよ。ヒント:配列に対しては数学と同じく、まずは「行」、次に「列」を指定することに注意してください。つまり、X=639, Y=240 の位置には img[639, 240] ではなく img[240,639] でアクセスします。

3.7 画像 (ピクセルの色情報) の直接編集

次に、指定範囲のピクセルの色情報を上書きし、次の画像のように「目隠し」を加えてみたいと思います。具体的には、X軸範囲を 130~219、Y軸範囲を 210~229 として、これらの範囲 (長方形) を 黒色 に塗りつぶしていきます。

img

これは、次のプログラムによって実行することができます。第12行目には、実行環境にあわせて img を表示する処理を追加してください (必要に応じて import や 関数の定義が必要になります)。

import cv2

fn = 'img-01.jpg'
img = cv2.imread(fn)

for x in range(130,219):
  for y in range(210,229):
    img[y,x,0] = 0  # B
    img[y,x,1] = 0  # G
    img[y,x,2] = 0  # R
      
# ■■ img を表示する処理を追加 ■■

上記のプログラムは、二重ループによって指定範囲のピクセルを黒色に書き換えています。しかし、このようなループを使って多数のデータを書き換えるような処理は、Pythonの特性上、非常に時間がかることが知られています

そこで、次のように NumPy の「スライス」と「ブロードキャスト機能」を組み合わせる方法が推奨されています。スライスについては第12回講義を参照、ブロードキャスト機能については第20回講義を参照してください。

import cv2
import numpy as np

fn = 'img-01.jpg'
img = cv2.imread(fn)

img[210:230,130:220] = np.array([0,0,0],dtype=np.uint8)
      
# ■■ img を表示する処理を追加 ■■

2つの方法の処理時間について timeit を使って比較した結果を示します。方法の違いで、処理時間に 約60倍 の差がでることが確認できます。


演習: 「紫色」の目隠しになるようにプログラムを修正せよ。

処理時間の計測

Jupyter環境 もしくは GoogleColab環境 では、マジックコマンド %timeit を使用することで比較的簡単に平均処理時間の計測ができます。%timeit は、特定の1行のプログラムの平均処理時間を計測する機能を持っています。複数行のコードから成る処理を測定したい場合は、それらの処理を関数にまとめて %timeit を使用します。

例えば、今回のケースでは次のようにして処理時間の計測をしました。

%reset -f
import cv2

def func(img):
  for x in range(130,220):
    for y in range(210,230):
      img[y,x,0] = 0
      img[y,x,1] = 0
      img[y,x,2] = 0

fn = 'img-01.jpg'
img = cv2.imread(fn)

%timeit func(img) # ここに注目
%reset -f
import cv2
import numpy as np

def func(img):
  img[210:230,130:220] = np.array([0,0,0],dtype=np.uint8)

fn = 'img-01.jpg'
img = cv2.imread(fn)

%timeit func(img) # ここに注目

3.8 画像の直接編集 (グレースケール画像の生成)

「カラー画像」から「グレースケール画像」を生成するためには cv2.cvtColor 関数を利用することができます。具体的には、次のようにして カラー画像 img から、グレースケール画像 img2 を生成することができます。

import cv2

fn = 'img-01.jpg'
img = cv2.imread(fn)
assert img.shape == (480,640,3)

img2 = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)  # グレースケール変換 
assert img2.shape == (480,640)

# ■■ img2 を表示する処理を追加 ■■

カラー画像では shape(480,640,3) であり、グレースケール画像では shape(480,640) となる点に注意してください。

ここでは、OpenCV の画像操作について理解を深めるために、cv2.cvtColor 関数を使わずに 多次元配列の直接編集によってグレースケール画像を生成することも行ないます。「情報2」の第05回講義の資料で学んだように、グレースケール画像は次のような計算で作成することができます。

\[ \mathrm{Gray} =\mathrm{Blue}\cdot0.11 + \mathrm{Green}\cdot0.59 + \mathrm{Red}\cdot 0.3 \]

ここで青・緑・赤によって重み (係数) が違っているのは 人間の視覚特性 を考慮しているためです。上記の係数は標準テレビジョン放送等のうちデジタル放送に関する送信の標準方式(平成二十三年総務省令第八十七号)に基づいたものになります。

この計算処理について愚直にプログラムを書くとすれば、次のようになります。

import cv2
import numpy as np

fn = 'img-01.jpg'
img = cv2.imread(fn)
assert img.shape == (480,640,3)

# すべての要素が 0 で大きさが (480,640) の配列 img2 を生成
img2 = np.zeros((480,640),dtype=np.uint8)
assert img2.shape == (480,640)

# グレースケール画像の手動生成
for x in range(img.shape[1]):
  for y in range(img.shape[0]):
    img2[y,x] = img[y,x,0]*0.11 + img[y,x,1]*0.59 + img[y,x,2]*0.3 

# ■■ img2 を表示する処理を追加 ■■

第09行目np.zeros は、第1引数で指定する大きさ (形状) の多次元配列 (=ndarrayオブジェクト) を生成する関数です。配列の要素はすべて 0 で初期化されます。同様の関数に np.ones があり、こちらは要素がすべて 1 で初期化されます。なお、すべての要素を任意の値、例えば 255 で初期化したい場合は np.ones((480,640),dtype=np.uint8)*255 のようにします。

既に解説したように Python の特性上、多重ループを構成して値を編集するような操作パフォーマンスの重大な低下 を引き起こします。そのため、上記のプログラムは、スライスを利用して以下のように記述することが推奨されます。

import cv2
import numpy as np

fn = 'img-01.jpg'
img = cv2.imread(fn)
assert img.shape == (480,640,3)

# すべての要素が 0 で大きさが (480,640) の配列 img2 を生成
img2 = np.zeros((480,640),dtype=np.uint8)
assert img2.shape == (480,640)

# グレースケール画像の手動生成【修正】
img2 = img[:,:,0]*0.11 + img[:,:,1]*0.59 + img[:,:,2]*0.3
assert img2.dtype == np.float64
img2 = img2.astype(np.uint8) # dtype を float64 から uint8 に変換
assert img2.dtype == np.uint8

# ■■ img2 を表示する処理を追加 ■■

変更されているのは 第13行目 となります。ここで img[:,:,0]img[0:480,0:640,0] の省略表記になります。スライスについて、いまいち分からないという場合は「numpy スライス 解説」などで検索して解説記事を読んでください。

なお、第13行目 の計算には小数が含まれるため、計算結果の ndarrayオブジェクト の要素の型 (dtype) は float64 になります。dtypefloat64 の ndarrayオブジェクト は cv2.imshow で画像として表示ができません。そのため 第15行目 で 要素の型 (dtype) を uint8 に変換しています。

演習1: img[:,:,0]img[0:480,0:640,0] ように書き換え、同様に img[:,:,1]img[:,:,2] も書き換え、問題なく動作することを確認せよ。

演習2: BGRの係数 (重み) を、すべて 0.3 とした場合の結果と比較せよ。また、係数を適当に変えて、その結果について確認せよ。

演習3: 次のプログラムを実行して オーバーフロー (桁あふれ) が生じるかどうかを確認せよ。

import numpy as np

arr1 = np.ones((3,3),dtype=np.uint8)*50
arr1[1,:] = 100
arr1[2,:] = 200
print(f'arr1 =\n{arr1}',end='\n\n')

arr2 = np.ones((3,3),dtype=np.uint8)*100
print(f'arr2 =\n{arr2}',end='\n\n')

arr3 = arr1 + arr2
assert arr3.dtype == np.uint8
print(f'arr1+ arr2 = arr3 =\n{arr3}')

演習4: 上記の 演習3 でオーバーフローが生じると仮定する。このとき、加算結果が 255 を越える場合は、その値を uint8 の最大値である 255 に制限したい。どのようにすればよいかを考え、また、ウェブ検索やChatGPTなどを利用して解決せよ (調べた方法や提案された方法は、実際に試して確認すること)。

解答例1: arr3 = np.where(arr1.astype(int) + arr2.astype(int) > 255, 255, arr1 + arr2)

解答例2: arr3 = np.clip(arr1*1.0 + arr2*1.0,0,255).astype(np.uint8)

3.9 画像の直接編集 (セピアカラー画像の生成)

グレースケール画像の応用として、次のような「セピアカラーの画像」を生成していきます。

img

この画像は、次のようなプログラムで生成することができます。

import cv2
import numpy as np

fn = 'img-01.jpg'
img = cv2.imread(fn)

img2 = img[:,:,0]*0.33 + img[:,:,1]*0.33 + img[:,:,2]*0.33
assert img2.shape == (480,640)

img3 = np.zeros_like(img)
assert img3.shape == (480,640,3)
img3[:,:,0] = img2*0.55  # Blue
img3[:,:,1] = img2*0.8   # Green
img3[:,:,2] = img2*1.0   # Red

# ■■ img3 を表示する処理を追加 ■■

第10行目np.zeros_like 関数は、引数に指定した多次元配列と同じ大きさの多次元配列を作成する関数です。要素はすべて0 で初期化されます。

また、次のように ノイズを発生させて加算する処理第08行目第10行目 の間に入れることで、より雰囲気のある画像を生成することもできます。np.random.normal の第2引数の値を変化させることで、ノイズの適用量を変えることができます。

tmp = img2 + np.random.normal(0,10,img2.shape)
assert tmp.dtype == np.float64
img2 = np.clip(tmp,0,255).astype(np.uint8)

上記の 第01行目 では 正規分布 (「情報1」の第12回講義の p.196 参照) に従うノイズを発生させて img2 に加算しています。np.random.normal で生成される多次元配列は dtypenp.float64 です。そのため、第02行目 でアサート文で確認しているように tmpdtypenp.float64 になります。このままでは画像として利用できないので、第03行目で、最小値を0、最大値を255 にクリップして、さらに astype メソッドで型を np.uint8 に変換しています。

img

演習1: np.random.normal について調べよ。また、第2引数の値を変化させて、その結果を確認せよ。

演習2: np.random.normal の第2引数を 20 程度に設定して、クリップ処理をしない場合、どのような結果になるか確認せよ。つまり、img2 = np.clip(tmp,0,255).astype(np.uint8)img2 = tmp.astype(np.uint8) にすると、画像に対してどのような影響が生じるかを確認せよ。

4 OpenCVを利用したカメラ操作

OpenCV では、PCに内蔵もしくは外部接続したカメラに接続して、そこから画像を得ることができます。ここでは、カメラに接続して静止画像を得る方法、また、連続的に静止画像を取得することで実質的に動画を得る方法 について紹介します。

なお、この処理には Pythonが実行されるコンピュータに「カメラ」が接続されている必要があるため、GoogleColab環境 では実行できません (既に説明していますが、GoogleColab環境において Pythonプログラム が実行されているのは Googleのデータセンタのなかに構築される仮想マシン であり、皆さんの PC ではありません)。

4.1 カメラから画像を取得

PCに接続されたカメラから撮影した画像を ndarrayオブジェクト として取得、表示、保存するサンプルプログラムは次のようになります。ファイルから読み込んだ画像も、カメラから読み込んだ画像も、同様に ndarrayオブジェクト (NumPyの3次元配列) として操作や処理 をすることができます。

import cv2
import numpy as np
import time
from datetime import datetime as dt

# 画像表示用関数
def my_imshow(img,wn='Image'):
  cv2.imshow(wn, img)
  while True:
    # ESCキーが押下されたら閉じる
    if cv2.waitKey(10) == 27:
      break
    # 閉じるボタンが押されたら閉じる
    if cv2.getWindowProperty(wn,cv2.WND_PROP_VISIBLE) < 1:
      break
  cv2.destroyAllWindows()

# PCに接続されているカメラと接続
cap = cv2.VideoCapture(0)

# カメラと接続できたかを確認
if not cap.isOpened():
  raise IOError('カメラと接続できませんでした。強制終了します。')

# カウントダウン
for i in range(3,0,-1):
  ret, img = cap.read() # 調整のための仮撮影。
  print(f'撮影{i}秒前...')
  time.sleep(1)

# カメラ映像を取得して img に格納。成功の可否を ret に格納
ret, img = cap.read()
assert type(ret) == bool
assert type(img) == np.ndarray

# キャプチャした画像を表示・保存
if ret:
  print(f'撮影ッ!!')
  my_imshow(img)
  fn = dt.now().strftime('photo-%Y%m%d-%H%M%S.jpg')
  cv2.imwrite(fn,img)
  print(f'写真を {fn} に保存しました。')
else:
  print('カメラから画像を取得できませんでした。')

# カメラと接続を解除 (カメラリソースの解放)
cap.release()

第19行目cv2.VideoCapture(0) の引数 (この例では 0) では、接続するカメラを選択しています。例えば、PCにフロントカメラとリアカメラがあって cv2.VideoCapture(0) では使いたいほうのカメラが利用できない場合は、cv2.VideoCapture(1) を指定します。同様に、USBカメラなどを接続していて、そちらに切り替えたい場合は、引数の数値を変えることで切り替えが可能になります。存在しないカメラを指定した場合は、第23行目 の例外処理により、プログラムが強制終了します。

第27行目 では、カメラ側でのオートフォーカスや、自動明るさ調整をカメラにさせるために仮撮影をしています。カメラの種類によっては、第27行目 を実行しなくても (コメントアウトしてしまっても) 問題ありません。

もし、撮影した画像に対して画像処理を行いたい場合は、第38行目 以降に img に対する処理を記述します。

演習: 第27行目 をコメントアウトした場合の結果について確認せよ。

ローカルPCのJupyter環境で実行

先述のように、カメラ関連の処理を GoogleColab環境 で実行することはできませんが、ローカルPCに構築した Jupyter環境であれば実行可能です。開発段階では Jupyter環境 のほうが便利なことが多いです。

%reset -f
import cv2
import numpy as np
import time
from datetime import datetime as dt
from IPython.display import Image   # 要import

# PCに接続されているカメラと接続
cap = cv2.VideoCapture(0)

# カメラと接続できたかを確認
if not cap.isOpened():
  raise IOError('カメラと接続できませんでした。強制終了します。')

# カウントダウン
for i in range(3,0,-1):
  ret, img = cap.read() # 調整のための仮撮影。
  print(f'撮影{i}秒前...')
  time.sleep(1)

# カメラ映像を取得して img に格納。成功の可否を ret に格納
ret, img = cap.read()
assert type(ret) == bool
assert type(img) == np.ndarray

# キャプチャした画像を表示・保存
fn = 'dummy.jpg'
if ret:
  print(f'撮影ッ!!')
  fn = dt.now().strftime('photo-%Y%m%d-%H%M%S.jpg')
  cv2.imwrite(fn,img)
  print(f'写真を {fn} に保存しました。')
else:
  print('カメラから画像を取得できませんでした。')

# カメラと接続を解除 (カメラリソースの解放)
cap.release()

Image(open(fn,'rb').read()) # 出力セルに表示

4.2 カメラから連続的に画像を取得して処理

カメラから連続的に画像を取得して処理したい場合は、次のようにプログラムを構成します (このプログラムは Jupyter環境では動作しません)。

import numpy as np
import cv2

# PCに接続されているカメラと接続
cap = cv2.VideoCapture(0)

# カメラと接続できたかを確認
if not cap.isOpened():
  raise IOError('カメラと接続できませんでした。強制終了します。')

wn = 'Capture'  # ウィンドウの識別名の設定

# 無限ループ
while True:

  # カメラ映像を取得して img に格納。成功の可否を ret に格納
  ret, img = cap.read()

  if not ret:
    print('カメラから画像を取得できませんでした。')
    break

  # 画像処理


  # 表示 (ウィンドウの表示内容の更新)
  cv2.imshow(wn,img)

  # ESCキーが押下されたらループから離脱
  if cv2.waitKey(1) == 27:
    print('[ESC]が押下されました')
    break
  
  # ウィンドウが閉じられたらループから離脱
  if cv2.getWindowProperty(wn,cv2.WND_PROP_VISIBLE) < 1:
    print('ウィンドウが閉じられました')
    break

# カメラと接続を解除 (カメラリソースの解放)
cap.release()
cv2.destroyAllWindows()
print('プログラムを正常終了')

例えば、リアルタイムに画像処理をして、それを反映させたい場合は 第24行目 以降に img に対する操作を組み込みます。

演習: 上記のプログラムに「セピアカラー処理」と「ノイズ追加処理」を追加せよ。