ITS: update tools/dng_noise_model.py with improvements
Use rawStats instead of raw captures to speed things up.
Use a smaller tile size to reduce low freq image changes
Increase exposure bracketing stops from 4 to 8
Minor gpylint cleanups for alignment and spacing.
bug: 158028170
Change-Id: I6e2007bb8f350071b719af68a051858dc7c80af1
diff --git a/apps/CameraITS/tools/dng_noise_model.py b/apps/CameraITS/tools/dng_noise_model.py
index b490d19..db37deb 100644
--- a/apps/CameraITS/tools/dng_noise_model.py
+++ b/apps/CameraITS/tools/dng_noise_model.py
@@ -1,4 +1,4 @@
-# Copyright 2014 The Android Open Source Project
+# Copyright 2014 The Android Open Source Project. Lint as python2
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@
import its.image
import its.objects
-import matplotlib
from matplotlib import pylab
import matplotlib.pyplot as plt
import numpy as np
@@ -29,61 +28,24 @@
import scipy.stats
BAYER_LIST = ['R', 'GR', 'GB', 'B']
+BRACKET_MAX = 8 # Exposure bracketing range in stops
+COLORS = 'rygcbm' # Colors used for plotting the data for each exposure.
+MAX_SCALE_FUDGE = 1.1
+MAX_SIGNAL_VALUE = 0.25 # Maximum value to allow mean of the tiles to go.
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
+STEPS_PER_STOP = 2 # How many sensitivities per stop to sample.
+# How large of tiles to use to compute mean/variance.
+# Large tiles may have their variance corrupted by low frequency
+# image changes (lens shading, scene illumination).
+TILE_SIZE = 32
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)
+ bracket_factor = math.pow(2, BRACKET_MAX)
with its.device.ItsSession() as cam:
props = cam.get_camera_properties()
@@ -95,28 +57,28 @@
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)
+ 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)
+ 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
+ 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']
+ 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")
+ 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")
+ raise its.error.Error('Scene is too dark to properly expose '
+ 'at the lowest sensitivity')
# Start the sensitivities at the minimum.
s = sens_min
@@ -138,57 +100,56 @@
plot.set_ylabel('Variance')
samples_s = [[], [], [], []]
- for b in range(0, bracket_stops):
+ for b in range(BRACKET_MAX):
# 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']
+ fmt_raw = {'format': 'rawStats',
+ 'gridWidth': TILE_SIZE,
+ 'gridHeight': TILE_SIZE}
+ cap = cam.do_capture(req, fmt_raw)
+ mean_image, var_image = its.image.unpack_rawstats_capture(cap)
+ idxs = its.image.get_canonical_cfa_order(props)
+ means = [mean_image[:, :, i] for i in idxs]
+ vars_ = [var_image[:, :, i] for i in idxs]
+
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):
+ for pidx in range(len(means)):
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)
+ pidx, props, cap['metadata'])
+ means_p = (means[pidx] - black_level)/(white_level - black_level)
+ vars_p = vars_[pidx]/((white_level - black_level)**2)
- # Use our high pass filter to filter this plane.
- hp = scipy.signal.sepfir2d(p, f, f).astype('float32')
+ # TODO(dsharlet): It should be possible to account for low
+ # frequency variation by looking at neighboring means, but I
+ # have not been able to make this work.
- means_tiled = \
- np.mean(tile(p, tile_size), axis=(0, 1)).flatten()
- vars_tiled = \
- np.var(tile(hp, tile_size), axis=(0, 1)).flatten()
+ means_p = np.asarray(means_p).flatten()
+ vars_p = np.asarray(vars_p).flatten()
samples_e = []
- for (mean, var) in zip(means_tiled, vars_tiled):
+ for (mean, var) in zip(means_p, vars_p):
# Don't include the tile if it has samples that might
# be clipped.
- if mean + 2*math.sqrt(var) < max_signal_level:
+ if mean + 2*math.sqrt(max(var, 0)) < MAX_SIGNAL_VALUE:
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)
+ means_e, vars_e, COLORS[b%len(COLORS)] + '.',
+ alpha=0.5, markersize=1)
samples_s[pidx].extend(samples_e)
for (pidx, p) in enumerate(samples_s):
@@ -200,15 +161,12 @@
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")
+ color_plane_plots[s_int][pidx].plot(
+ [0, MAX_SIGNAL_VALUE], [O, O + S*MAX_SIGNAL_VALUE],
+ '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
+ xmax = max([max([x for (x, _) in p]) for p in samples_s])*MAX_SCALE_FUDGE
+ ymax = (O + S*xmax)*MAX_SCALE_FUDGE
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()
@@ -218,7 +176,7 @@
plots.append([s_int, fig])
# Move to the next sensitivity.
- s *= math.pow(2, 1.0/steps_per_stop)
+ 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))
@@ -242,28 +200,23 @@
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]])
+ gains = gains.flatten()
+ means = means.flatten()
+ vars_ = vars_.flatten()
# 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)" % \
+ assert np.all(digital_gains == 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_
+ # Divide the whole system by gains*means.
+ f = lambda x, a, b, c, d: (c*(x[0]**2) + d + (x[1])*a*x[0] + (x[1])*b)/(x[0])
+ [result, _] = scipy.optimize.curve_fit(f, (gains, means), vars_/(gains))
- # 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_p, B_p, C_p, D_p] = result[0:4]
A.append(A_p)
B.append(B_p)
C.append(C_p)
@@ -276,16 +229,17 @@
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")
-
+ 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")
+ 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))
+ fig.savefig('%s.png' % (NAME))
# add models to subplots and re-save
for [s, fig] in plots: # re-step through figs...
@@ -294,9 +248,9 @@
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].plot(
+ [0, MAX_SIGNAL_VALUE], [O, O + S*MAX_SIGNAL_VALUE],
+ '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))