125
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Systemi(株式会社システムアイ)Advent Calendar 2024

Day 25

画像処理初学者が画像処理と向き合ってみた

Last updated at Posted at 2024-12-24

はじめに

メリークリスマス :santa: :christmas_tree:

最近画像処理に触れる機会があったのですが
バタバタしてたので学び直しの記事を書こうと思いました。

画像処理は学生時代にモザイク画像を作ってみよう!みたいなことをして以来触れてなかったのでなかなか新鮮で面白かったです。

今回はpythonを使用します。私はjupyterを使用して動作確認を行なってます。

この記事では

画像処理は最近では機械学習やAIを用いることが多いと聞きましたが、さまざまな都合がありそれらは行いませんでした。
この記事で触れるのは次のような内容になります。

  • 画像データとは
  • OpenCVを用いた画像処理

画像処理やってみての感想

楽しかったです。またぜひ挑戦したいです。

画像データとは

画像データの説明では次の画像を使用させていただきます。

画像が持っているデータは、主に以下のような要素から構成されています。

  • ピクセルデータ: 画像は「ピクセル」と呼ばれる小さな点の集まりでできています
  • 解像度: 解像度は、画像のサイズを示します
  • カラーモデル: 画像は色を表現するために「カラーモデル」を使います(RGB、CMYKなど)
  • メタデータ: 画像ファイルには、撮影日時やカメラの設定、位置情報、著作権情報など
  • 圧縮形式: 画像データは、さまざまな形式で保存されます(JPEG、PNGなど)

実際にコードで理解を進めます

画像を読み込むコード
import numpy as np
from PIL import Image

# 画像を読み込む
image_path = 'images/shigoto_zaitaku_cat_woman.png'  # ここに画像のパスを指定
image = Image.open(image_path)

# 画像のピクセルデータを取得
pixels = np.array(image)

# 画像の解像度を取得
width, height, channels = pixels.shape

:thinking: なぜimageをnp.arrayするのか

画像をNumPy配列に変換することは、画像処理を効率的に行うための重要なステップです。これにより、さまざまな画像処理ライブラリとの互換性が得られ、計算が高速化され、データの操作が容易になります。

ピクセルデータと解像度

  • ピクセルデータ: 画像は「ピクセル」と呼ばれる小さな点の集まりでできています
  • 解像度: 解像度は、画像のサイズを示します

次のコードを動かして理解を深めます。

ピクセルデータと解像度を表示する
import matplotlib.pyplot as plt

print(f"Image Resolution: {width}x{height}")
print(f"Original Image Shape: {pixels.shape}")
print(f"Pixel Data(100x100): {pixels[100:200, 100:200].shape}")
print(f"Pixel Data(15x15): {pixels[100:115, 100:115].shape}")
# 画像の表示
plt.figure(figsize=(12, 6))

# 1. 元の画像
plt.subplot(1, 3, 1)
plt.imshow(image)
plt.title('Original Image')
plt.axis('off')

# 2. 100x100ピクセルの部分画像
plt.subplot(1, 3, 2)
plt.imshow(pixels[100:200, 100:200],aspect='equal')
plt.title('Pixel Data(100x100)')
plt.axis('off')

# 3. 15x15ピクセルの部分画像
plt.subplot(1, 3, 3)
plt.imshow(pixels[100:115, 100:115],aspect='equal')
plt.title('Pixel Data(15x15)')
plt.axis('off')

plt.tight_layout()
plt.show()

コードの出力結果は次のとおりです。

Image Resolution: 400x400
Original Image Shape: (400, 400, 4)
Pixel Data(100x100): (100, 100, 4)
Pixel Data(15x15): (15, 15, 4)

image.png

説明

ピクセルデータと解像度についての詳細は次のとおりです。

  • ピクセルデータ:
    • 画像は、ピクセル(画素)と呼ばれる小さな点の集合で構成されています。各ピクセルは、色や明るさの情報を持っています
    • NumPy表現: ndarray(多次元配列)
    • 画像はNumPyの配列として表現され、各ピクセルの色や明るさの情報が格納されます。例えば、カラー画像は形状が (高さ, 幅, チャンネル数) となります
  • 解像度:
    • 画像の解像度は、画像のサイズを示すもので、通常は横×縦のピクセル数で表されます
    • NumPy表現: shape
    • 例えば、pixels.shape が (1080, 1920, 3) の場合、解像度は1920×1080ピクセルです

コードの出力結果を見てみると、例の画像は解像度が400x400です。

print(f"Image Resolution: {width}x{height}")

Image Resolution: 400x400

print(f"Original Image Shape: {pixels.shape}")

Original Image Shape: (400, 400, 4)

ピクセルデータの400x400をそのまま出したのがOriginal Imageです。
pixel_data.png

ピクセルデータの範囲指定

print(f"Pixel Data(100x100): {pixels[100:200, 100:200].shape}")
print(f"Pixel Data(15x15): {pixels[100:115, 100:115].shape}")
plt.imshow(pixels[100:200, 100:200],aspect='equal')
plt.imshow(pixels[100:115, 100:115],aspect='equal')

Pixel Data(100x100): (100, 100, 4)
Pixel Data(15x15): (15, 15, 4)

ピクセルデータのpixels[100:115, 100:115]は、NumPy配列pixelsから特定の範囲のピクセルを選択していることを示しています。

  • pixels[100:115, 100:115]:
    • 最初のスライス 100:115 は、配列の行(高さ)を指定しています
    • 2番目のスライス 100:115 は、配列の列(幅)を指定しています

pixel_spec_data.png

上の画像だと、実際にピクセルデータで切り取られてるのがわかりづらいので、軸を表示してみます。

import matplotlib.pyplot as plt

print(f"Image Resolution: {width}x{height}")
print(f"Original Image Shape: {pixels.shape}")
print(f"Pixel Data(100x100): {pixels[100:200, 100:200].shape}")
print(f"Pixel Data(15x15): {pixels[100:115, 100:115].shape}")
# 画像の表示
plt.figure(figsize=(12, 6))

# 1. 元の画像
plt.subplot(1, 3, 1)
plt.imshow(image)
plt.title('Original Image')
plt.axis([0, width, height, 0])

# 2. 100x100ピクセルの部分画像
plt.subplot(1, 3, 2)
plt.imshow(pixels,aspect='equal')
plt.title('Pixel Data(100x100)')
plt.axis([100, 200,  200, 100])

# 3. 15x15ピクセルの部分画像
plt.subplot(1, 3, 3)
plt.imshow(pixels,aspect='equal')
plt.title('Pixel Data(15x15)')
plt.axis([100, 115,  115, 100])

plt.tight_layout()
plt.show()

image.png

ピクセルのRGBA値

実際どのようなデータが格納されているかが気になりました。

# 例: (100, 100)の位置にあるピクセルのRGBA値を表示
print(f'ピクセル(100, 100)のRGBA値: {pixels[100, 100]}')  # 出力: ピクセル(100, 100)のRGBA値: [R, G, B, A]

ピクセル(100, 100)のRGBA値: [ 80 127 180 255]

100, 100の位置は結構青っぽく見えます。

image.png

100,200の位置のRGBA値も見てます。
高さ100,横200はちょうど女性の横顔がなので肌色になると予想してます。

# 例: (100, 200)の位置にあるピクセルのRGBA値を表示
print(f'ピクセル(100, 200)のRGBA値: {pixels[100, 200]}')  # 出力: ピクセル(100, 200)のRGBA値: [R, G, B, A]

ピクセル(100, 200)のRGBA値: [255 207 165 255]

image.png

こんな感じで、400x400 = 160,000画素のRGBA値をpixelsが保持していることがわかりました。

画像を生成することもできます

# 100x100のRGBA画像を作成(0-255の範囲のランダムな整数値)
pixels = np.random.randint(0, 256, (100, 100, 4), dtype=np.uint8)

plt.imshow(pixels)
plt.axis('off')
plt.show()

image.png

カラーモデル

  • NumPy表現: channels
  • カラーモデルは、配列の最後の次元(チャンネル数)で表現されます
  • 例えば、RGB画像の場合、channels は3(赤、緑、青)になります
  • shape は (高さ, 幅, 3) です
  • 一般的なカラーモデルには、以下のようなものがあります:
    • RGB: 赤、緑、青の3色を使うモデル
    • CMYK: 印刷用のモデルで、シアン、マゼンタ、イエロー、黒の4色を使います
    • HSV: 色相(色の種類)、彩度(色の鮮やかさ)、明度(明るさ)で色を表現します
# カラーモデルの表示
if channels == 4:
    color_model = 'RGBA'
elif channels == 3:
    color_model = 'RGB'
elif channels == 1:
    color_model = 'Grayscale'
else:
    color_model = 'Unknown'

print(f"Color Model: {color_model}")

# または

print(f"Color Model: {image.mode}")

本題

ここからはOpenCVを利用して画像処理を行なっていきます。

画像処理・画像解析および機械学習等の機能を持つライブラリで、pythonがオリジナルなんだと勝手に思ってたんですが全然違くてC言語から始まってました。

いくつか学び直しも兼ねて記載していきます。
この記事で記載するのはいくつかの方法のうちのひとつです。

グレースケール

グレースケール化をすると、次のようなメリットがあります。

  • データの軽量化
    • カラー画像は通常、各ピクセルに対して赤、緑、青の3つの色成分を持っていますが、グレースケール画像は1つの成分(輝度)だけで表現されるため、データサイズが小さくなります。これにより、ストレージの節約や処理速度の向上が期待できます
  • 処理の簡素化
    • グレースケール画像は、カラー画像に比べて処理が簡単です。多くの画像処理アルゴリズム(エッジ検出、フィルタリングなど)は、グレースケール画像に対してより効率的に実行できます
  • コントラストの強調
    • グレースケール化により、色の情報が排除されるため、明暗のコントラストが強調されることがあります

実際の処理を見ていきます。

import cv2
gray_pixels = cv2.cvtColor(pixels, cv2.COLOR_BGR2GRAY)

# 情報の表示
print(f"Grayscale Image Shape: {gray_pixels.shape}")

# 100,100のピクセル値を表示
print(f"ピクセル(100, 100)の値: {gray_pixels[100, 100]}")

# 画像の表示
plt.imshow(gray_pixels, cmap='gray')
plt.axis('off')
plt.show()

Grayscale Image Shape: (400, 400)
ピクセル(100, 100)の値: 137

image.png

グレースケール後の値

Grayscale Image Shape: (400, 400)

グレースケール後はshapeが(高さ, 幅)

ピクセル(100, 100)の値: 137

ピクセルの値が輝度のみとなっていることが確認できました。

ヒストグラム平坦化

次のようなメリットがあるらしいです。

  • コントラストの向上: ヒストグラム平坦化は、画像の暗い部分や明るい部分の詳細を強調し、全体的なコントラストを向上させます。これにより、視覚的により明瞭で鮮明な画像が得られます
  • 詳細の強調: 特に暗い領域や明るい領域に隠れている詳細を明らかにすることができます。これにより、画像内の重要な特徴やパターンをより容易に識別できるようになります
  • 均一な輝度分布: ヒストグラム平坦化は、画像の輝度値を均一に分布させるため、特定の輝度範囲に偏りがある場合でも、全体的にバランスの取れた画像を生成します
  • 視覚的な魅力の向上: コントラストが改善されることで、画像がより魅力的に見えることがあります。特に、写真や映像の品質を向上させるために使用されることが多いです

ただ個人的に正直この処理はピンときてませんでした。
状況的に私に必要な処理ではなかったのでピンときてなかったんですが、勉強のため書こうと思います。

この処理が活きる場面

記事書いてる間に巡り会えたページをご紹介します。

画像が鮮明に見えるようになるいい例が載っています :bow:

BGRAの画像をLABに変換してヒストグラム平坦化を行う

# 画像をBGRAからBGRに変換
bgr_image = cv2.cvtColor(pixels, cv2.COLOR_BGRA2BGR)

# BGR画像をLAB色空間に変換
lab_image = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2LAB)

# LAB画像をL、A、Bチャンネルに分割
l_channel, a_channel, b_channel = cv2.split(lab_image)

# LチャンネルにCLAHE(コントラスト制限適応ヒストグラム平坦化)を適用
clahe = cv2.createCLAHE(clipLimit=4, tileGridSize=(4, 4))
cl_channel = clahe.apply(l_channel)

# CLAHEで強調されたLチャンネルをAおよびBチャンネルと再結合
enhanced_lab_image = cv2.merge((cl_channel, a_channel, b_channel))

# LAB画像をBGR色空間に戻す
enhanced_bgr_image = cv2.cvtColor(enhanced_lab_image, cv2.COLOR_LAB2BGR)

# 元の画像と強調された画像を表示 
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(bgr_image)
plt.title('Original Image')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(enhanced_bgr_image)
plt.title('Enhanced Image')
plt.axis('off')
plt.show()

# ヒストグラムを表示
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.hist(bgr_image.ravel(), bins=256, range=[0, 256])
plt.title('Original Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.subplot(1, 2, 2)
plt.hist(enhanced_bgr_image.ravel(), bins=256, range=[0, 256])
plt.title('Enhanced Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.show()

image.png

背景が透過背景だったので、BGRAからBGRに変換した際に背景が黒くなりました。

image.png

ヒストグラムで表示されているデータは、画像のピクセル値の分布を示しています
x軸がピクセル値で、y軸が頻度です。

処理後はピクセル値ごとの頻度がなだらかな感じになっています。

パラメータについて

  • clipLimit は、ヒストグラムの最大値を制限するための閾値です。具体的には、各タイル(後述の tileGridSize で定義される領域)内のヒストグラムの値がこの閾値を超える場合、その超過分がクリップ(切り捨て)されます
  • tileGridSize は、CLAHEが適用される領域のサイズを指定します。これは、画像を小さなタイル(グリッド)に分割し、それぞれのタイルに対してヒストグラム平坦化を行うためのパラメータです

それぞれの値をいじってみました。

image.png
image.png

image.png
image.png

image.png
image.png

2値化

みていただいた方が早いかもしれません。

max_value = 255
adaptive_method = cv2.ADAPTIVE_THRESH_GAUSSIAN_C
threshold_type = cv2.THRESH_BINARY
block_size = 11
c = 9

threshould_image = cv2.adaptiveThreshold(gray_pixels, max_value, adaptive_method, threshold_type, block_size, c)

# 画像とヒストグラムを表示する
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(threshould_image, cmap='gray')
plt.title('Threshold Image')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.hist(threshould_image.ravel(), bins=256, range=[0, 256])
plt.title('Threshold Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.show()

image.png

こんな感じで、画像が2つの値(白と黒)だけになります。
adaptiveThresholdは、OpenCVライブラリにおける画像処理の手法の一つで、画像の二値化を行うための方法です。特に、画像の明るさやコントラストが不均一な場合に効果的です。

通常の二値化では、全体の画像に対して単一の閾値を使用しますが、adaptiveThresholdでは、画像の各ピクセルに対して異なる閾値を計算します。これにより、局所的な明るさの変化に対応できます。

パラメータの説明

  • src: 入力画像(グレースケール画像)
  • maxValue: 出力画像の最大値(通常255)
  • adaptiveMethod: 適応的な閾値計算の方法(cv2.ADAPTIVE_THRESH_MEAN_C または cv2.ADAPTIVE_THRESH_GAUSSIAN_C)
  • thresholdType: 二値化のタイプ(通常は cv2.THRESH_BINARY)
  • blockSize: 閾値を計算するための領域のサイズ(奇数でなければなりません)
  • C: 計算された閾値から引く定数。これにより、閾値を調整できます

例えば、 block_size = 9 c = 4にした場合、次のような画像になります。

image.png

通常の2値化について

threshold = 150
thresh_value, thresh_img = cv2.threshold(gray_pixels, threshold, 255, cv2.THRESH_BINARY)

# 画像とヒストグラムを表示する
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(thresh_img, cmap='gray')
plt.title('Threshold Image')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.hist(thresh_img.ravel(), bins=256, range=[0, 256])
plt.title('Threshold Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.show()

image.png

これはこれで版画みたいで素敵なんですがちょっと元の画像の線とかは消えちゃってますね...
閾値thresholdを50にすると次のようになります。
結構調整は大変な感じです。

image.png

thresholdの処理である程度ノイズが落ちたりするんですが、閾値に左右される部分があり、汎用的に1つの処理でいくつかの画像を扱いたい場合にはなかなか難しいです。

ノイズ除去

ノイズ除去は fastNlMeansDenoising がいい感じでした。

# ノイズ除去
h=9
templateWindowSize=7
searchWindowSize=21
denoise_image = cv2.fastNlMeansDenoising(gray_pixels, None, h, templateWindowSize, searchWindowSize)

# オリジナル画像とノイズ除去後の画像を表示
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(gray_pixels, cmap='gray')
plt.title('Original Image')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(denoise_image, cmap='gray')
plt.title('Denoised Image')
plt.axis('off')
plt.show()

# ノイズ除去前後のヒストグラムを表示
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.hist(gray_pixels.ravel(), bins=256, range=[0, 256])
plt.title('Original Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.ylim(0, 5000)
plt.subplot(1, 2, 2)
plt.hist(denoise_image.ravel(), bins=256, range=[0, 256])
plt.title('Denoised Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.ylim(0, 5000)
plt.show()

image.png

image.png

左の元画像に比べて右は色が均一になってます。
(猫の輪郭とかはちょっとぼやけちゃってますが...)

これにさらに先ほどの2値化を行うと

max_value = 255
adaptive_method = cv2.ADAPTIVE_THRESH_GAUSSIAN_C
threshold_type = cv2.THRESH_BINARY
block_size = 19
c = 1.1

threshould_image = cv2.adaptiveThreshold(denoise_image, max_value, adaptive_method, threshold_type, block_size, c)

# 画像とヒストグラムを表示する
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(threshould_image, cmap='gray')
plt.title('Threshold Image')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.hist(threshould_image.ravel(), bins=256, range=[0, 256])
plt.title('Threshold Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.show()

image.png

こんな感じの画像が出来上がります。
文字が書いてある画像とかだと、文字部分がいい感じに残ります。

例として私が文字を書いた画像です。
閾値はちょっといじりました。

h=17
templateWindowSize=7
searchWindowSize=21
block_size = 11
c = 4

image.png

image.png

最後に

ここまで読んでくださりありがとうございます。
他にもシャープネス調整とかコントラスト調整とかいろいろあったんですがあんまり長くなってもアレなのでここで終わろうと思います。
良いお年を :snake:

おまけ

以下は記事書いてる間に気になり調べた内容です。

:robot: dpiと解像度の関係性 DPI(Dots Per Inch)と解像度は、画像や印刷物の品質を評価するための重要な指標ですが、異なる概念です。以下に、それぞれの定義と関係性について説明します。

DPI(Dots Per Inch)
定義: DPIは「1インチあたりのドット数」を表します。これは、印刷物やディスプレイ上での解像度を示す単位で、特に印刷において重要です。
意味: DPIが高いほど、1インチの中により多くのドット(ピクセル)が配置されるため、画像がより詳細で鮮明に見えます。例えば、300 DPIの印刷物は、1インチの中に300個のドットがあることを意味します。
解像度
定義: 解像度は、画像のサイズを示す指標で、通常は「幅 × 高さ」の形式で表されます。例えば、1920×1080ピクセルの解像度は、横に1920ピクセル、縦に1080ピクセルの画像を意味します。
意味: 解像度は、画像の詳細度や鮮明さを示すもので、一般的に解像度が高いほど、より多くの画素が含まれ、より詳細な画像になります。
DPIと解像度の関係性
印刷における関係:
DPIは、印刷物の物理的なサイズと解像度の関係を示します。例えば、解像度が300 DPIの画像を印刷する場合、画像のピクセル数に基づいて印刷サイズが決まります。
例えば、解像度が300 DPIで、画像のサイズが600×900ピクセルの場合、印刷サイズは次のように計算できます:
幅: 600ピクセル ÷ 300 DPI = 2インチ
高さ: 900ピクセル ÷ 300 DPI = 3インチ
したがって、この画像は2インチ×3インチのサイズで印刷されます。
デジタル表示における関係:
デジタルディスプレイでは、DPIはあまり重要ではなく、ピクセル数(解像度)が主に考慮されます。ディスプレイの解像度が高いほど、より多くのピクセルが表示され、詳細な画像が得られます。
まとめ
DPI: 印刷物の物理的な解像度を示す指標で、1インチあたりのドット数を表します。
解像度: 画像のサイズ(幅 × 高さ)を示す指標で、ピクセル数で表されます。
関係性: DPIは解像度と印刷サイズの関係を示し、特に印刷において重要です。解像度が高く、DPIも高い場合、より詳細で鮮明な印刷物が得られます。

125
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
125
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?