# Copyright 2014 The Android Open Source Project
|
#
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
# you may not use this file except in compliance with the License.
|
# You may obtain a copy of the License at
|
#
|
# http://www.apache.org/licenses/LICENSE-2.0
|
#
|
# Unless required by applicable law or agreed to in writing, software
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# See the License for the specific language governing permissions and
|
# limitations under the License.
|
|
import math
|
import os.path
|
import textwrap
|
|
import its.caps
|
import its.device
|
import its.image
|
import its.objects
|
|
import matplotlib
|
from matplotlib import pylab
|
import matplotlib.pyplot as plt
|
import numpy as np
|
import scipy.signal
|
import scipy.stats
|
|
BAYER_LIST = ['R', 'GR', 'GB', 'B']
|
NAME = os.path.basename(__file__).split('.')[0]
|
RTOL_EXP_GAIN = 0.97
|
|
|
def tile(a, tile_size):
|
"""Convert a 2D array to 4D w/ dims [tile_size, tile_size, row, col] where row, col are tile indices.
|
"""
|
tile_rows, tile_cols = a.shape[0]/tile_size, a.shape[1]/tile_size
|
a = a.reshape([tile_rows, tile_size, tile_cols, tile_size])
|
a = a.transpose([1, 3, 0, 2])
|
return a
|
|
|
def main():
|
"""Capture a set of raw images with increasing analog gains and measure the noise.
|
"""
|
|
# How many sensitivities per stop to sample.
|
steps_per_stop = 2
|
# How large of tiles to use to compute mean/variance.
|
tile_size = 64
|
# Exposure bracketing range in stops
|
bracket_stops = 4
|
# How high to allow the mean of the tiles to go.
|
max_signal_level = 0.25
|
# Colors used for plotting the data for each exposure.
|
colors = 'rygcbm'
|
|
# Define a first order high pass filter to eliminate low frequency
|
# signal content when computing variance.
|
f = np.array([-1, 1]).astype('float32')
|
# Make it a higher order filter by convolving the first order
|
# filter with itself a few times.
|
f = np.convolve(f, f)
|
f = np.convolve(f, f)
|
|
# Compute the normalization of the filter to preserve noise
|
# power. Let a be the normalization factor we're looking for, and
|
# Let X and X' be the random variables representing the noise
|
# before and after filtering, respectively. First, compute
|
# Var[a*X']:
|
#
|
# Var[a*X'] = a^2*Var[X*f_0 + X*f_1 + ... + X*f_N-1]
|
# = a^2*(f_0^2*Var[X] + f_1^2*Var[X] + ... + (f_N-1)^2*Var[X])
|
# = sum(f_i^2)*a^2*Var[X]
|
#
|
# We want Var[a*X'] to be equal to Var[X]:
|
#
|
# sum(f_i^2)*a^2*Var[X] = Var[X] -> a = sqrt(1/sum(f_i^2))
|
#
|
# We can just bake this normalization factor into the high pass
|
# filter kernel.
|
f /= math.sqrt(np.dot(f, f))
|
|
bracket_factor = math.pow(2, bracket_stops)
|
|
with its.device.ItsSession() as cam:
|
props = cam.get_camera_properties()
|
props = cam.override_with_hidden_physical_camera_props(props)
|
|
# Get basic properties we need.
|
sens_min, sens_max = props['android.sensor.info.sensitivityRange']
|
sens_max_analog = props['android.sensor.maxAnalogSensitivity']
|
sens_max_meas = sens_max_analog
|
white_level = props['android.sensor.info.whiteLevel']
|
|
print "Sensitivity range: [%f, %f]" % (sens_min, sens_max)
|
print "Max analog sensitivity: %f" % (sens_max_analog)
|
|
# Do AE to get a rough idea of where we are.
|
s_ae, e_ae, _, _, _ = \
|
cam.do_3a(get_results=True, do_awb=False, do_af=False)
|
# Underexpose to get more data for low signal levels.
|
auto_e = s_ae*e_ae/bracket_factor
|
# Focus at zero to intentionally blur the scene as much as possible.
|
f_dist = 0.0
|
|
# If the auto-exposure result is too bright for the highest
|
# sensitivity or too dark for the lowest sensitivity, report
|
# an error.
|
min_exposure_ns, max_exposure_ns = \
|
props['android.sensor.info.exposureTimeRange']
|
if auto_e < min_exposure_ns*sens_max_meas:
|
raise its.error.Error("Scene is too bright to properly expose \
|
at the highest sensitivity")
|
if auto_e*bracket_factor > max_exposure_ns*sens_min:
|
raise its.error.Error("Scene is too dark to properly expose \
|
at the lowest sensitivity")
|
|
# Start the sensitivities at the minimum.
|
s = sens_min
|
|
samples = [[], [], [], []]
|
plots = []
|
measured_models = [[], [], [], []]
|
color_plane_plots = {}
|
while int(round(s)) <= sens_max_meas:
|
s_int = int(round(s))
|
print 'ISO %d' % s_int
|
fig, [[plt_r, plt_gr], [plt_gb, plt_b]] = plt.subplots(2, 2, figsize=(11, 11))
|
fig.gca()
|
color_plane_plots[s_int] = [plt_r, plt_gr, plt_gb, plt_b]
|
fig.suptitle('ISO %d' % s_int, x=0.54, y=0.99)
|
for i, plot in enumerate(color_plane_plots[s_int]):
|
plot.set_title('%s' % BAYER_LIST[i])
|
plot.set_xlabel('Mean signal level')
|
plot.set_ylabel('Variance')
|
|
samples_s = [[], [], [], []]
|
for b in range(0, bracket_stops):
|
# Get the exposure for this sensitivity and exposure time.
|
e = int(math.pow(2, b)*auto_e/float(s))
|
print 'exp %.3fms' % round(e*1.0E-6, 3)
|
req = its.objects.manual_capture_request(s_int, e, f_dist)
|
cap = cam.do_capture(req, cam.CAP_RAW)
|
planes = its.image.convert_capture_to_planes(cap, props)
|
e_read = cap['metadata']['android.sensor.exposureTime']
|
s_read = cap['metadata']['android.sensor.sensitivity']
|
s_err = 's_write: %d, s_read: %d, RTOL: %.2f' % (
|
s, s_read, RTOL_EXP_GAIN)
|
assert (1.0 >= s_read/float(s_int) >= RTOL_EXP_GAIN), s_err
|
print 'ISO_write: %d, ISO_read: %d' % (s_int, s_read)
|
|
for (pidx, p) in enumerate(planes):
|
plot = color_plane_plots[s_int][pidx]
|
|
p = p.squeeze()
|
|
# Crop the plane to be a multiple of the tile size.
|
p = p[0:p.shape[0] - p.shape[0]%tile_size,
|
0:p.shape[1] - p.shape[1]%tile_size]
|
|
# convert_capture_to_planes normalizes the range
|
# to [0, 1], but without subtracting the black
|
# level.
|
black_level = its.image.get_black_level(
|
pidx, props, cap["metadata"])
|
p *= white_level
|
p = (p - black_level)/(white_level - black_level)
|
|
# Use our high pass filter to filter this plane.
|
hp = scipy.signal.sepfir2d(p, f, f).astype('float32')
|
|
means_tiled = \
|
np.mean(tile(p, tile_size), axis=(0, 1)).flatten()
|
vars_tiled = \
|
np.var(tile(hp, tile_size), axis=(0, 1)).flatten()
|
|
samples_e = []
|
for (mean, var) in zip(means_tiled, vars_tiled):
|
# Don't include the tile if it has samples that might
|
# be clipped.
|
if mean + 2*math.sqrt(var) < max_signal_level:
|
samples_e.append([mean, var])
|
|
if samples_e:
|
means_e, vars_e = zip(*samples_e)
|
color_plane_plots[s_int][pidx].plot(
|
means_e, vars_e, colors[b%len(colors)] + '.',
|
alpha=0.5)
|
samples_s[pidx].extend(samples_e)
|
|
for (pidx, p) in enumerate(samples_s):
|
[S, O, R, _, _] = scipy.stats.linregress(samples_s[pidx])
|
measured_models[pidx].append([s_int, S, O])
|
print "Sensitivity %d: %e*y + %e (R=%f)" % (s_int, S, O, R)
|
|
# Add the samples for this sensitivity to the global samples list.
|
samples[pidx].extend([(s_int, mean, var) for (mean, var) in samples_s[pidx]])
|
|
# Add the linear fit to subplot for this sensitivity.
|
# pylab.subplot(2, 2, pidx+1)
|
#pylab.plot([0, max_signal_level], [O, O + S*max_signal_level], 'rgkb'[pidx]+'--',
|
#label="Linear fit")
|
color_plane_plots[s_int][pidx].plot([0, max_signal_level],
|
[O, O + S*max_signal_level], 'rgkb'[pidx]+'--',
|
label="Linear fit")
|
|
xmax = max([max([x for (x, _) in p]) for p in samples_s])*1.25
|
ymax = max([max([y for (_, y) in p]) for p in samples_s])*1.25
|
color_plane_plots[s_int][pidx].set_xlim(xmin=0, xmax=xmax)
|
color_plane_plots[s_int][pidx].set_ylim(ymin=0, ymax=ymax)
|
color_plane_plots[s_int][pidx].legend()
|
pylab.tight_layout()
|
|
fig.savefig('%s_samples_iso%04d.png' % (NAME, s_int))
|
plots.append([s_int, fig])
|
|
# Move to the next sensitivity.
|
s *= math.pow(2, 1.0/steps_per_stop)
|
|
# do model plots
|
(fig, (plt_S, plt_O)) = plt.subplots(2, 1, figsize=(11, 8.5))
|
plt_S.set_title("Noise model")
|
plt_S.set_ylabel("S")
|
plt_O.set_xlabel("ISO")
|
plt_O.set_ylabel("O")
|
|
A = []
|
B = []
|
C = []
|
D = []
|
for (pidx, p) in enumerate(measured_models):
|
# Grab the sensitivities and line parameters from each sensitivity.
|
S_measured = [e[1] for e in measured_models[pidx]]
|
O_measured = [e[2] for e in measured_models[pidx]]
|
sens = np.asarray([e[0] for e in measured_models[pidx]])
|
sens_sq = np.square(sens)
|
|
# Use a global linear optimization to fit the noise model.
|
gains = np.asarray([s[0] for s in samples[pidx]])
|
means = np.asarray([s[1] for s in samples[pidx]])
|
vars_ = np.asarray([s[2] for s in samples[pidx]])
|
|
# Define digital gain as the gain above the max analog gain
|
# per the Camera2 spec. Also, define a corresponding C
|
# expression snippet to use in the generated model code.
|
digital_gains = np.maximum(gains/sens_max_analog, 1)
|
digital_gain_cdef = "(sens / %d.0) < 1.0 ? 1.0 : (sens / %d.0)" % \
|
(sens_max_analog, sens_max_analog)
|
|
# Find the noise model parameters via least squares fit.
|
ad = gains*means
|
bd = means
|
cd = gains*gains
|
dd = digital_gains*digital_gains
|
a = np.asarray([ad, bd, cd, dd]).T
|
b = vars_
|
|
# To avoid overfitting to high ISOs (high variances), divide the system
|
# by the gains.
|
a /= (np.tile(gains, (a.shape[1], 1)).T)
|
b /= gains
|
|
[A_p, B_p, C_p, D_p], _, _, _ = np.linalg.lstsq(a, b)
|
A.append(A_p)
|
B.append(B_p)
|
C.append(C_p)
|
D.append(D_p)
|
|
# Plot the noise model components with the values predicted by the
|
# noise model.
|
S_model = A_p*sens + B_p
|
O_model = \
|
C_p*sens_sq + D_p*np.square(np.maximum(sens/sens_max_analog, 1))
|
|
plt_S.loglog(sens, S_measured, 'rgkb'[pidx]+'+', basex=10, basey=10,
|
label="Measured")
|
plt_S.loglog(sens, S_model, 'rgkb'[pidx]+'x', basex=10, basey=10, label="Model")
|
|
plt_O.loglog(sens, O_measured, 'rgkb'[pidx]+'+', basex=10, basey=10,
|
label="Measured")
|
plt_O.loglog(sens, O_model, 'rgkb'[pidx]+'x', basex=10, basey=10, label="Model")
|
plt_S.legend()
|
plt_O.legend()
|
|
fig.savefig("%s.png" % (NAME))
|
|
# add models to subplots and re-save
|
for [s, fig] in plots: # re-step through figs...
|
dg = max(s/sens_max_analog, 1)
|
fig.gca()
|
for (pidx, p) in enumerate(measured_models):
|
S = A[pidx]*s + B[pidx]
|
O = C[pidx]*s*s + D[pidx]*dg*dg
|
color_plane_plots[s][pidx].plot([0, max_signal_level],
|
[O, O + S*max_signal_level], 'rgkb'[pidx]+'-',
|
label="Model", alpha=0.5)
|
color_plane_plots[s][pidx].legend(loc='upper left')
|
fig.savefig('%s_samples_iso%04d.png' % (NAME, s))
|
|
# Generate the noise model implementation.
|
A_array = ",".join([str(i) for i in A])
|
B_array = ",".join([str(i) for i in B])
|
C_array = ",".join([str(i) for i in C])
|
D_array = ",".join([str(i) for i in D])
|
noise_model_code = textwrap.dedent("""\
|
/* Generated test code to dump a table of data for external validation
|
* of the noise model parameters.
|
*/
|
#include <stdio.h>
|
#include <assert.h>
|
double compute_noise_model_entry_S(int plane, int sens);
|
double compute_noise_model_entry_O(int plane, int sens);
|
int main(void) {
|
for (int plane = 0; plane < %d; plane++) {
|
for (int sens = %d; sens <= %d; sens += 100) {
|
double o = compute_noise_model_entry_O(plane, sens);
|
double s = compute_noise_model_entry_S(plane, sens);
|
printf("%%d,%%d,%%lf,%%lf\\n", plane, sens, o, s);
|
}
|
}
|
return 0;
|
}
|
|
/* Generated functions to map a given sensitivity to the O and S noise
|
* model parameters in the DNG noise model. The planes are in
|
* R, Gr, Gb, B order.
|
*/
|
double compute_noise_model_entry_S(int plane, int sens) {
|
static double noise_model_A[] = { %s };
|
static double noise_model_B[] = { %s };
|
double A = noise_model_A[plane];
|
double B = noise_model_B[plane];
|
double s = A * sens + B;
|
return s < 0.0 ? 0.0 : s;
|
}
|
|
double compute_noise_model_entry_O(int plane, int sens) {
|
static double noise_model_C[] = { %s };
|
static double noise_model_D[] = { %s };
|
double digital_gain = %s;
|
double C = noise_model_C[plane];
|
double D = noise_model_D[plane];
|
double o = C * sens * sens + D * digital_gain * digital_gain;
|
return o < 0.0 ? 0.0 : o;
|
}
|
""" % (len(A), sens_min, sens_max, A_array, B_array, C_array, D_array, digital_gain_cdef))
|
print noise_model_code
|
for i, _ in enumerate(BAYER_LIST):
|
read_noise = C[i] * sens_min * sens_min + D[i]
|
e_msg = '%s model min ISO noise < 0! C: %.4e, D: %.4e, rn: %.4e' % (
|
BAYER_LIST[i], C[i], D[i], read_noise)
|
assert read_noise > 0, e_msg
|
assert C[i] > 0, '%s model slope is negative. slope=%.4e' % (
|
BAYER_LIST[i], C[i])
|
text_file = open("noise_model.c", "w")
|
text_file.write("%s" % noise_model_code)
|
text_file.close()
|
|
if __name__ == '__main__':
|
main()
|