皆さんはスマホで写真を撮ることがありますか?僕はたまに撮ります。スマホが世に誕生してからしばらく経ちますが、カメラはかなり進化してきていますね。今では、一眼が必要ないんじゃないかと思うくらいきれいに写真を撮ることができます。(暗いところではかなり差がありますが。。)
そんなスマホのに使われるイメージセンサの実力を解析してみたいと思います!!
iPhoneに使われているイメージセンサ
僕の手持ちのスマホがiPhone12の無印なので、解析するのはiPhone12に使用されるイメージセンサになります。
まずは、iPhone12に使用されているイメージセンサを調査してみます。いろいろ調べてみるとこんなサイトにたどり着きました。ここの情報によるとiPhone12の広角カメラに使用されているのは、IMX503というSony製のイメージセンサのようです。IMX503ってどんなセンサなんだろと思って調べてみたんですが、どれだけ調べてもデータシートが出てきません。おそらく市販品などではなくApple専用に開発されたセンサなんだと思います。
イメージセンサの測定方法
それでは、本題のイメージセンサの解析方法です。今回はセンサの出力応答とノイズ性能の確認をしていきます。準備するものは以下の通りです。
- iPhone(Raw取得できるカメラ)
- NDフィルタ15段分
- フラットな光源
- 暗い空間
- PC(Python)
イメージセンサの解析をするにあたって、Raw画像を取得する必要があります。iPhoneユーザーの方は通常のカメラでは、Rawを取得することができません。iPhoneでRawを取得する場合はこのアプリ(シンプルRawカメラ)を使用してみてください。Rawの方式はDNGファイルになります。
光源はなるべくフラットなものにしてください。積分球を持っているマニアックな方は、積分球で大丈夫です。フラットな光源なんて持っていないという方は、PCのモニタを最大に明るくし白一色にするのもありだと思います。
NDフィルタは当然ですが、光量を落とすために使います。NDフィルタを用意できない場合はシャッタースピードを使って光量を変更してみるとよいと思います。(ただし、ノイズ特性は変わるかもしれません。)
ここが一番大事ですが測定は必ず真っ暗な空間で行ってください。ダーク付近のリニアリティが確保できなくなるためです。
ここからは、実際の測定方法です。
- 光源とカメラの位置を固定します。(NDフィルタが全部入る距離)
- NDフィルタなしでカメラが飽和するくらい、光源を明るくしてください。
光源で飽和までいかない場合は、シャッタースピードを長くしてみましょう。 - 光源を撮影します。一番明るい状態から光量を1/2ずつ落として繰り返し撮影します。
光量の落とし方は、NDを挟むかシャッタースピードを変更してください。 - 飽和から十分くらい状態まで撮影できたら、データ取り終了です。
注意事項は、光源とカメラの距離は変えないでください。光源を遠くすれば、当然暗くなるからです。配置のイメージはこんな感じです。下
Rawデータの解析
まず、iPhoneや一眼レフで取得したRaw画像は各社特有のRaw画像になっているので、汎用Rawに変換してから解析を始めてください。
取得したデータの解析を行います。今回はPythonを使用して解析を行いたいと思います。
今回使用するのは、こちらのライブラリです。
import numpy as np
import matplotlib.pyplot as plt
import math
コード全体の流れです。少し長いですが、最後までお付き合いください。
- Raw画像読み込み設定
- 画像処理に使うNumpy配列の設定
- 複数枚のRaw画像を扱う設定
- ダークを減算(今回は528)
- 信号、ノイズ、SN比の計算
- グラフの表示
になります。以下が実際のコードになります。
import numpy as np
import matplotlib.pyplot as plt
import math
#***********************************
#1.画像設定
#***********************************
width=4032
height=3024
width_roi = 100
height_roi = 100
image_num = 15
bit = 12
dark = 528 #
half_width = int(width/2)
half_height = int(height/2)
half_width_roi = int(width_roi/2)
half_height_roi = int(height_roi/2)
#***********************************
#2.各種配列作成
#***********************************
img_roi=np.empty((height_roi, width_roi),dtype = np.uint16) #空配列
img_s = np.empty((4,image_num),dtype = np.float32)
img_n = np.empty((4,image_num),dtype = np.float32)
img_sn = np.empty((4,image_num),dtype = np.float32)
rgb = np.array(range(4)) #RGB振り分け用
rgb_det = rgb.reshape((2,2)) #RGB振り分け用
img_roi_r=np.empty((half_height_roi, half_width_roi),dtype = np.uint16)
img_roi_gr=np.empty((half_height_roi, half_width_roi),dtype = np.uint16)
img_roi_gb=np.empty((half_height_roi, half_width_roi),dtype = np.uint16)
img_roi_b=np.empty((half_height_roi, half_width_roi),dtype = np.uint16)
x_axis = np.empty((1,image_num),dtype = np.uint16)
#***********************************
#3.複数枚のRaw処理
#***********************************
for i in range(image_num):
x_axis[0][i] = 2**i
rawpath = "DNG_4032x3024_dark=528_12bit_lin"+str(i)+".raw" #Raw画像の名前
#***********************************
#raw画像を開く
#***********************************
fd = open(rawpath, 'rb')
f = np.fromfile(fd, dtype=np.uint16, count=height*width)
img = f.reshape((height,width))
fd.close()
#***********************************
#ROI切り出し
#***********************************
width_roi_start = int((width-width_roi)/2)
height_roi_start = int((height-height_roi)/2)
for x in range(width_roi_start,width_roi_start+width_roi):
for y in range(height_roi_start,height_roi_start+height_roi):
img_roi[y-height_roi_start][x-width_roi_start] = img[y][x]
#***********************************
#ダーク除去
#***********************************
for x in range(width_roi):
for y in range(height_roi):
if img_roi[y][x] > dark:
img_roi[y][x] = img_roi[y][x]-dark
elif img_roi[y][x] <= dark:
img_roi[y][x] = 0
#***********************************
#SN計算
#***********************************
for x in range(width_roi):
for y in range(height_roi):
color = rgb_det[y % 2][x % 2]
if color == 0: #R
y_r = int(y/2)
x_r = int(x/2)
img_roi_r[y_r][x_r] = img_roi[y][x]
elif color == 1: #Gr
y_gr = int(y/2)
x_gr = int((x-1)/2)
img_roi_gr[y_gr][x_gr] = img_roi[y][x]
elif color == 2: #Gb
y_gb = int((y-1)/2)
x_gb = int(x/2)
img_roi_gb[y_gb][x_gb] = img_roi[y][x]
else: #B
y_gb = int((y-1)/2)
x_gb = int((x-1)/2)
img_roi_b[y_gb][x_gb] = img_roi[y][x]
#信号
img_s[0][i] = np.mean(img_roi_r)
img_s[1][i] = np.mean(img_roi_gr)
img_s[2][i] = np.mean(img_roi_gb)
img_s[3][i] = np.mean(img_roi_b)
#ノイズ
img_n[0][i] = np.std(img_roi_r)
img_n[1][i] = np.std(img_roi_gr)
img_n[2][i] = np.std(img_roi_gb)
img_n[3][i] = np.std(img_roi_b)
#SN比
img_sn[0][i] = 20*(math.log10(img_s[0][i]/img_n[0][i]))
img_sn[1][i] = 20*(math.log10(img_s[1][i]/img_n[1][i]))
img_sn[2][i] = 20*(math.log10(img_s[2][i]/img_n[2][i]))
img_sn[3][i] = 20*(math.log10(img_s[3][i]/img_n[3][i]))
#グラフ表示
fig1 = plt.figure()
ax1 = fig1.add_subplot(111)
ax1.plot(x_axis[0,:], img_sn[0,:],label = "R",color = "r")
ax1.plot(x_axis[0,:], img_sn[1,:],label = "Gr",color = "lightgreen")
ax1.plot(x_axis[0,:], img_sn[2,:],label = "Gb",color = "forestgreen")
ax1.plot(x_axis[0,:], img_sn[3,:],label = "B",color = "b")
ax1.set_xscale("log") #軸対数化
ax1.legend()
fig2 = plt.figure()
ax2 = fig2.add_subplot(111)
ax2.plot(x_axis[0,:], img_s[0,:],label = "R",color = "r")
ax2.plot(x_axis[0,:], img_s[1,:],label = "Gr",color = "lightgreen")
ax2.plot(x_axis[0,:], img_s[2,:],label = "Gb",color = "forestgreen")
ax2.plot(x_axis[0,:], img_s[3,:],label = "B",color = "b")
ax2.plot(x_axis[0,:], img_n[0,:],linestyle="--",label = "R",color = "r")
ax2.plot(x_axis[0,:], img_n[1,:],linestyle="--",label = "Gr",color = "lightgreen")
ax2.plot(x_axis[0,:], img_n[2,:],linestyle="--",label = "Gb",color = "forestgreen")
ax2.plot(x_axis[0,:], img_n[3,:],linestyle="--",label = "B",color = "b")
ax2.set_xscale("log") #軸対数化
ax2.set_yscale("log") #軸対数化
ax2.legend()
plt.show()
これを実行するとグラフが表示されます。
左のグラフが信号とノイズの光量の応答特性です。右がSN比の特性になります。
この結果からIMX503のおおざっぱな分析をすると、
- 当然ですが信号が光量に比例しています。
- ノイズは約信号の平方根に比例
- SNは最大約35dB
- ダイナミックレンジは約70dB
であることがわかりました!ダイナミックレンジの定義は以下の記事からご確認ください。
ということで、身近にあるイメージセンサの簡単な解析をしてみました。iPhoneに限らずすべてのカメラで使える方法なので、皆さん挑戦してみてください!
コメント