#はじめに
この記事はNoteで連載している記事で扱っているCode部分のみを切り出しています。技術背景等にご興味がある方はNoteの記事の方をご参照ください。
今回は前回の続きです。前回でDark shadingを補正するGain Mapが生成できました。このGain mapを用いてDark shading補正をした後の画像を用いて、Noise除去、Edge強調処理をしていきます。大半の処理はOpenCVで実装されているので、それらを利用して関数を構築しています。
#画像修正処理
今回実施する画像修正処理の大まかな流れを簡単に示します。この順番で処理をしています。
- Noise除去(Bilateral filter)
- Edge強調(Unsharp mask)
- 画素値の正規化
###Noise除去
RAW画像は各画素の特性ばらつき等があり、非常にざらついた画像になっています。例えば平な白い壁を撮影しても拡大してよく見るとばらつきの影響で平には見えません。なので、この画素特性ばらつきを除去するためにNoise除去を行います。
理想的なNoise除去は、Noiseのみを除去するのですが実際はそうはいきません。画素特性ばらつきはランダムなばらつきなので、簡単にどのレベルからNoiseという風に判断はできないからです。なので隣接画素の情報を用いてNoiseと思われる部分を抽出して、隣接画素特性からNoise部分の値を予測するという処理をしています。今回はよく使われるBilateralという手法を用いてNoise除去をします。
関数に入力される画像は、他の画像処理との兼ね合いで信号レベルを0.0~1.0のFloatに規格化しています。理由は画像のFormatによっては8bitだったり10bitだったりするため、入力Formatを統一化するためです。本当はgeneric等を用いてするのが良いかと思うのですが、勉強中のため辞めました・・・。
OpenCVで実装されているbilateral filter関数は入力画像として8bitのRGB-3ch画像しか受け付けないので一旦内部で8bitの画像に変換しています。
def _bilateral_filter(self, img: np.ndarray, kernel_size: int=3, disp: int = 75) -> np.ndarray:
""" apply bilateral filter in order to reduce random noise
Parameters
-----------
img: np.ndarray
target image
kernel_size: int
filter kernal size, default size is 3x3
disp: int
dispartion parameter, default value is 75
Returns
-----------
cor_img: np.ndarray
image after bilateral filtering
"""
# convert data from float to 8bit data
img_8_bit = img * 255
img_8_bit_int = img_8_bit.astype(np.uint8)
cor_img = cv2.bilateralFilter(img_8_bit_int, kernel_size, disp, disp)
# return
return cor_img
###Edge強調
Noise除去後の画像は、Filterの影響によりEdgeがボケています。なのでこのEdgeボケを修正する必要があります。色々とEdge強調する方法はありますが、教科書等でよく取り上げられており、かつ実際のカメラ内部でも使用されているUnsharp maskを使用してEdge強調します。
Noise除去をする前の画像をorg_imgとしてNoise除去後の画像をtarget_imgとしています。Original画像にGaussianBlur filterをかけて、Noise除去後の画像との比較することでEdge部分にどれだけWeightをかけるかを計算しています。
悩みどころは、org_imgはNoise除去後の画像で良いのではないか?ということです。この点は色々実験した結果Noise除去前の画像を使う方が安定して良い結果が出たため、この案を採用することにしました。
def _unsharp_mask(self, org_img: np.ndarray, target_img: np.ndarray, sigma: int=2) -> np.ndarray:
""" Apply unsharm mask to enhance object edge
Parameters
-----------
org_img: np.ndarray
original image before noise reduction
target_img: np.ndarray
target image after noise reduction
sigma: int
filter size of blur
Returns
-----------
cor_img: np.ndarray
unsharp-mask image
"""
offset = -0.5
blur_img = cv2.GaussianBlur(org_img, (0, 0), sigma)
cor_image = cv2.addWeighted(target_img, sigma + offset, blur_img, \
offset, 0)
# return
return cor_image
###画素値の正規化
撮影シーンや光源条件により画像間のばらつきが発生します。このばらつきをある程度緩和する処理をしておくと、後の処理が楽になります。機械学習等では一般的に用いられている手法です。ここではこの方法を採用し画像Dataの正規化を行います。
def _equalize_distribution(self, img: np.ndarray, grid_size: int=3, \
sigma: int=32, mean_value: int = 128) -> np.ndarray:
""" Equalize image signal distribution
Parameters
-------------
img: np.ndarray
image data
grid_sie: int
distribution grid size to eqalize the signal distribution
sigma: int
sigma value of Gaussian
mean_value: int
mean value after equalization
Returns
-------------
eq_img: np.ndarray
eqalized image data
"""
# convert RGB -> HSV
hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
# split hsv into h,s and v
h, s, v = cv2.split(hsv)
# equalie the distribution
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(grid_size, grid_size))
result = clahe.apply(v)
# adjust distribution
result_adj = (result - np.mean(result)) / np.std(result) * \
sigma + mean_value
result_adj[result_adj>255] = 255 # inserted max value pinning
result = np.array(result_adj, dtype=np.uint8)
# hsv -> RGB
hsv = cv2.merge((h,s,result))
eq_img = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
# return
return eq_img
###その他の処理
これらの関数は前提としてDark Shafing補正をしたものです。以前の投稿でDark shading処理を受け持つdark classを定義しました。このclassを利用してdark shading補正を実施する関数を定義しておきます。
まずはLeft/RightのGain MapをInitializeする関数です。
def set_dksh_gain_map(self, gain_map: np.ndarray, left: bool = True) -> bool:
""" Setter of gain map for dark shading correction
Parameters
-----------
gain_map: np.ndarray
gain map data
left: bool
location indicater, default is true -> left, false -> right
"""
if left:
self.left_gain_map = gain_map
return True
self.right_gain_map = gain_map
return True
次に実際にGain Mapを画像に適応する関数です。Return前に一旦画像の最大値でNormarizeしています。この方が後々使いやすいからです。
def _correct_dksh(self, img: np.ndarray, gain_map: np.ndarray) -> np.ndarray:
""" correct dark shading by multiplying gain_map to raw image
Parameters
-----------
img: np.ndarray
raw image data
gain_map: np.ndarrray
gain map to correct dark shading
Returns
----------
cor_img: np.ndarray
corrected image
"""
# correct dark shading
gain_map_one = np.ones((self._img_height, self._img_width, 3))
cor_img = img * gain_map
#cor_img = img * gain_map_one
# normarization
max_val = cor_img.max()
cor_img /= max_val
# return
return cor_img
#処理の全体像
以上で個々の処理内容を示しました。次にこれらの関数を使用し処理の全体像を示します。単純に処理に順に書き出していくだけです。
Return前に8bitの範囲を超える値及び0以下となる値をBinningする処理をしています。uint8で処理しているので0~255の範囲を超えるとOverflowで画像がおかしくなるのを避けるためです。
def _core_process(self, img: np.ndarray, gain_map: np.ndarray, \
bi_kernel_size: int=5, bi_disp: int=75, unsharp_sigma: int=2, \
eq_grid_size: int=2, eq_sigma: int=32, eq_mean_value: int=128 ) -> np.ndarray:
""" run all image processes
Parameters
----------
img: np.ndarray
raw image, just read from data
gain_map: np.ndarray
gain map in order to correct dark shading
bi_kernel_size: int
kernel size for bilateral filter, default value is 5
bi_disp: int
dispersion value for bilateral filter, default value is 75
unsharp_sigma: int
sigma value for unsharp mask, default is 2
eq_grid_size: int
grid size for equalization, default value is 2
eq_sigma: int
sigma value for equalization, default value is 32
eq_mean_value: int
mean value for equalization, default value is 128
Returns:
-----------
processed_img: np.ndarray
processed image
"""
# correct dark shading
cor_img = self._correct_dksh(img, gain_map)
# apply bilateral filter to reduce noise
cor_img = self._bilateral_filter(cor_img, bi_kernel_size, bi_disp)
# apply unsharp mask to enhance edge
cor_img = self._unsharp_mask(img, cor_img, unsharp_sigma)
# equalize distribution
cor_img = self._equalize_distribution(cor_img, eq_grid_size, eq_sigma, eq_mean_value)
# level pinning
cor_img[cor_img>255] = 255
cor_img[cor_img<0] = 0
# return
return cor_img
Left/Rightの画像を処理する必要があるので、入力された画像がLeftなのかRightなのかをファイル名で判別しGain Mapを入れ替える必要があります。それらを考慮し実装したものがこちらとなります。どの画像を処理したかを追跡しやすくするために、画像のFolderリストも返すようにしています。
def run_process(self, bi_kernel_size: int=5, bi_disp: int=75, unsharp_sigma: int=2, \
eq_grid_size: int=2, eq_sigma: int=16, eq_mean_value: int=128 ) \
->([str], [np.ndarray]):
""" Run process for all images
Parameters
-----------
bi_kernel_size: int
kernel size for bilateral filter, default value is 5
bi_disp: int
dispersion value for bilateral filter, default value is 75
unsharp_sigma: int
sigma value for unsharp mask, default is 2
eq_grid_size: int
grid size for equalization, default value is 2
eq_sigma: int
sigma value for equalization, default value is 32
eq_mean_value: int
mean value for equalization, default value is 128
Returnes
----------
file_path_list: [str]
processed image file path list
proc_imgs: [np.ndarray]
processed image data
"""
# image data stocker
proc_imgs = []
# run process
for idx, filepath in enumerate(self._filepath_list):
img = self._raw_imgs[idx]
loc = helper.loc_detector_from_name(filepath.split('/')[-1])
if loc:
# left side
proc_img = self._core_process(img, self.left_gain_map, \
bi_kernel_size, bi_disp, unsharp_sigma, eq_grid_size, \
eq_sigma, eq_mean_value)
# stacing
proc_imgs.append(proc_img)
else:
# right side
proc_img = self._core_process(img, self.right_gain_map, \
bi_kernel_size, bi_disp, unsharp_sigma, eq_grid_size, \
eq_sigma, eq_mean_value)
# stacing
proc_imgs.append(proc_img)
# return
return self._filepath_list, proc_imgs
最後にこれらの関数をMethodとして持っているDualPixel classの定義を示して今回は終わりにします。
import helper
import cv2
import numpy as np
class DualPixel(object):
def __init__(self, filepath: str, ext: str, dsize = (2016, 1512)):
""" Initialize DualPixel class
Parameters
----------
filepath :str
pass project folder path to read raw images
ext: str
describe the file extention name
dsize : (width: int, height: int)
raw image size after resizing, not orifinal image size
"""
super().__init__()
self._filepath = filepath
self._ext = ext
# image size
self._img_width, self._img_height = dsize
# read raw image
self._filepath_list = helper.make_file_path_list(filepath, ext)
self._filepath_list.sort()
self._raw_imgs = helper.read_img_from_path(self._filepath_list, self._img_width, self._img_height)
# initalize gain_maps
self.left_gain_map = np.zeros((self._img_height, self._img_width, 3))
self.right_gain_map = np.zeros((self._img_height, self._img_width, 3))
次回は前処理Partの最後となります。