【iPhone イメージセンサ 解析】Sony製IMX503を徹底調査してみよう

ImageSensor

皆さんはスマホで写真を撮ることがありますか。
実は、iPhone イメージセンサ 解析をすると、スマホカメラの進化がよく分かります。
そして、Sony製IMX503はiPhoneのカメラに採用されているセンサの一つです。
そこで今回は、iPhone12に使われているイメージセンサを対象に、暗室での測定方法を交えながら解析してみたいと思います。

iPhoneのイメージセンサを解析する理由

スマホが世に誕生してからしばらく経ちました。
しかし、カメラ機能は驚くほど進化しています。
一方で、暗い場所の撮影ではまだ一眼レフとの性能差が残ることも事実です。
とはいえ、十分に明るい場面では一眼が不要と思うほど綺麗に撮影できます。
さらに、日常的に使うスマホだからこそ、その中核であるiPhone イメージセンサ 解析には大きな意義があります。


iPhone12に使われているイメージセンサ

僕の手持ちのスマホはiPhone12(無印)です。
したがって、解析対象はiPhone12に搭載されているセンサとなります。
そして、いろいろ調べた結果、広角カメラに使用されているのは「IMX503」というSony製イメージセンサのようでした。

しかし、どんなに調べてもデータシートが見当たりません。
おそらくApple専用に開発されたセンサなので、市販品としては公開されていないのでしょう。
一方で、その性能が気になる方は多いでしょうから、ここでiPhone イメージセンサ 解析を試みる価値があります。

イメージセンサの測定方法

今回は、イメージセンサの出力応答とノイズ性能を確認します。
まず、測定で準備するものは以下の通りです。

  1. iPhone (Raw取得できるカメラアプリ)
  2. NDフィルタ15段分
  3. フラットな光源
  4. 暗い空間
  5. PC(Python)

測定でのポイント

暗室での測定
ダーク付近のリニアリティを確保するため、光源以外の余分な光が入らないようにするのが重要です。

Raw画像取得
通常のiPhoneカメラアプリではRawを取得できません。
しかし、シンプルRawカメラなどを使うとDNGファイル形式でRaw撮影できます。

フラットな光源
なるべく均一な明るさが得られるものを使いましょう。
積分球を持っている方はそのまま活用できます。
ない場合は、PCモニタを白一色にして最大輝度にするのも一手です。

NDフィルタ
光量を落とすために必須です。
フィルタがなければシャッタースピードで調整しても構いませんが、ノイズ特性が変わる可能性があります。

 測定の手順

  1. 光源とカメラの位置を固定します。
    (ここで、NDフィルタをすべて入れられるだけの作業スペースを確保)
  2. NDフィルタなしでカメラが飽和するくらい、光源を明るくしてください。
    光源で飽和までいかない場合は、シャッタースピードを長くするなど工夫してみましょう。
  3. 光源を撮影します。
    そして、一番明るい状態から光量を1/2ずつ落としていき、繰り返し撮影してください。
    光量の落とし方は、NDフィルタを1枚ずつ足すかシャッタースピードを調整する方法があります。
  4. 飽和から十分暗い状態まで撮影できたら、データ取得完了です。

注意: 光源とカメラの距離は変えないでください。
距離が変わると光量まで変わるため、正確な結果が得られなくなります。


実際の配置イメージ

暗室、あるいは暗い空間を用意し、
カメラ(iPhone)と光源の間にNDフィルタを挟みます。
このとき、フィルタやカメラを動かすと測定条件が変わる可能性があるため、
しっかりと固定する必要があります。

Rawデータの解析

ここからは、撮影したRawデータを解析します。
しかし、iPhoneのDNGや一眼レフのRAWはメーカー独自形式が多いです。
したがって、汎用Rawへ変換してからPythonで扱うと便利でしょう。

以下が実際の解析コードです。
PythonとNumpy、Matplotlib、Mathライブラリを利用します。

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_det = rgb.reshape((2,2))
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画像を開く
    #***********************************
    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
            else:
                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)

    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_Noise",color = "r")
ax2.plot(x_axis[0,:], img_n[1,:],linestyle="--",label = "Gr_Noise",color = "lightgreen")
ax2.plot(x_axis[0,:], img_n[2,:],linestyle="--",label = "Gb_Noise",color = "forestgreen")
ax2.plot(x_axis[0,:], img_n[3,:],linestyle="--",label = "B_Noise",color = "b")
ax2.set_xscale("log")
ax2.set_yscale("log")
ax2.legend()

plt.show()

コードを実行すると、グラフが表示されます。
一つは信号・ノイズの応答特性、もう一つはSN比の特性です。

解析結果と考察

  • 信号は光量に比例
    当たり前ですが、光量が増えるほどセンサ出力は上昇します。
  • ノイズは信号の平方根にほぼ比例
    これはショットノイズ(フォトンノイズ)の典型的な特性です。
  • SN比は最大約35dB
    さらに、ダイナミックレンジは約70dBとなります。

ダイナミックレンジの定義については、
外部リンク: ImageSensor-info.comの記事
を参照してください。

つまり、iPhone12に搭載されているIMX503センサの大まかな特徴は、これらの結果から読み取れます。
また、この方法はiPhoneに限らず、すべてのカメラで応用可能です。
ぜひ、身近なカメラセンサで挑戦してみてください。


まとめ: iPhone イメージセンサ 解析の魅力

この記事では、iPhone イメージセンサ 解析の具体的な手法をご紹介しました。
実際に暗室での撮影やNDフィルタの活用、PythonでのRaw解析を通して、Sony製IMX503の特性を調べられます。
さらに、スマホカメラの性能がどこまで向上しているかを数値的に確認できるのは、とても興味深いです。

日常使いのスマホカメラの実力を深堀りしたい方は、同じ要領で自前のデータ取得と解析をしてみてください。
一眼レフやミラーレスとも比較すると、性能の違いが一層際立って面白いでしょう。


注意: このコードを使用した際に発生した現象・損害には責任を負いかねます。
あくまで参考情報としてご利用ください。



同一手法は他のスマホカメラでも応用できるため、ぜひ試してみてください。

コメント

タイトルとURLをコピーしました