Repository navigation
What's Changed
New features
- Images are read in the orientation they were taken in. A camera held on its side stores the pixels as the sensor read them and notes the rotation in the metadata, so a vertical photo used to come out as a horizontal array: the LED array ran across the image and every pixel position step 1 found was in a frame the photographer never saw. Every image is now turned upright while it is read, from
Exif.Image.Orientationfor JPG and PNG and fromsizes.flipof LibRaw for RAW files, which covers the vendor formats that carry no plain EXIF orientation. The Bayer filter array is turned with the pixels, so it keeps naming the colour of each pixel and the channel sums stay unmixed. All eight EXIF orientations are implemented and verified against Pillow'sImageOps.exif_transpose. The new optionimg_rotationin[DEFAULT]ofconfig.ini(auto,0,90,180,270) turns images that were exported without their metadata and overrides a wrong orientation; it governs step 1, the plot of the search areas, step 3 and the postprocessing pixel view alike (#133, #156). - Temperature correction of the LED intensities. An LED loses radiant power as it heats up, so the LEDs in the hot gas layer dim even where no smoke is between them and the camera. A new
[temperature_correction]section ofconfig_analysis.ininames one file of reduction factors per channel (reduction_file_channel_0 = reduction_factors_red.csv,Nonefor no correction), given either as a table over experiment time and LED height or as a list with the columnsheight,timeandreduction_factor. The reference property is divided by the interpolated factor before the reference intensities are averaged, so I0 is corrected as well. The time axis is the experiment time fromstart_timeofconfig.ini; images and LEDs outside the table keep their intensity and are counted in a warning. The result header states which file corrected the channel. Verified against an independent implementation on 2124 images and 143 LEDs to 1.3e-14 1/m (#167). - Several quantities per analysis run.
reference_propertyinconfig_analysis.iniandconfig_stacked.initakes one or more quantities separated by spaces, e.g.sum_col_val bgsub_sum_col_val sidebg_sum_col_val, which one--analysisor-asrun computes one after another. Each quantity writes into a directory of its own,analysis/extinction_coefficients/<solver>/<quantity>/.SimDataandStackedSimDatatake a newreference_propertyargument and still find result files written by earlier versions in the directory of the solver (#160). - The reference images of the normalization can be named and are protected.
ref_img_experiment_idsin[analyse_photo]ofconfig.ininames the images I0 is taken from by their experiment ID. They are analysed even whennum_skip_imgswould skip them, andaverage_imageskeeps each of them as an averaged image of its own, wherever in the experiment they lie.average_imagesandref_img_indicescan now be combined (#61, #168). - The linear solver weights the LEDs by their signal to noise ratio, on by default. Step 3 writes
noise_sum_col_val, the read noise ofsum_col_val, estimated from the optical black pixels. Each LED gets the weightmin(1, SNR / snr_full_weight)withsnr_full_weight = 10, and a layer whose path length weighted mean LED weight stays belowmin_layer_weightbecomes NaN instead of a coefficient that rests on LEDs in the read noise. The weighting switches itself off with a note where there is no noise estimate (JPG, PNG, RAW without an optical black margin, step 3 files of an older version);snr_full_weight = Noneswitches it off by hand. The result header reportsNoise_propertyandSnr_full_weight(#170). - New averaging options.
n_summarizeinconfig_analysis.iniandconfig_stacked.inisets how many imagesaverage_imagessummarizes into one, which was fixed at two (#165).num_moving_avg_imagesapplies a centred moving average to the reference property, where every image keeps its own time; combining it withaverage_imagesis refused (#170).
Bugfixes
- The channel data are no longer clipped at zero. After the black level was subtracted, every negative value was set to 0, which turned the negative half of the read noise into a positive offset of 0.3 to 0.7 % of I0 and made the extinction coefficients saturate in dense smoke. The channel data are now
float32without a lower limit, each Bayer colour is corrected with the black level of its own optical black pixels, and the binary data keepsum_col_valandmax_col_valas floats so the fraction of a count survives. On the Warsaw test V008, the ratio of LEDSA to BCL at 0.81 1/m goes from 0.87 to 0.96.bgsub_sum_col_valsubtracts its background median linearly instead of clipping pixel by pixel, and the linear solver gives an LED with no signal left the weight 0 instead of an optical depth of about 23 (#170). - The black level of RAW images comes from the optical black. It was taken from the RAW metadata, a nominal value of the camera model that differs from the actual level of the sensor by a fraction of a count; integrated over a search area, that fraction is a constant offset on the intensity of every LED, which does not vanish when the LED dims and makes the extinction coefficients saturate in dense smoke. The level is now the mean of the optical black pixels of the image itself, masked pixels at the margin of the sensor that are read out with the image, so the estimate follows the drift of the black level with the sensor temperature as well.
raw.black_level_per_channelremains the fallback for sensors with a margin narrower than 8 px. JPG and PNG carry no black level and are untouched (#158). - The nonlinear solver reaches the minimum of its cost function. It handed TNC no gradient, so the gradient was estimated from finite differences of intensities that fall off exponentially with the coefficients, and the minimisation stopped wherever the line search first failed - at a point that depended on the image before it, since each image starts from the coefficients of its predecessor. Two terms of the cost function pushed it there: the data term divided the residual norm by the number of LEDs instead of its square root, which weighted the regularisation ten times too strongly for 100 LEDs and met the stopping criteria of TNC early, and the curvature penalty summed absolute second differences, whose kinks are exactly where such a penalty drives the solution. The cost function is now
rmse(sigma) + weighting_curvature * mean((D2 sigma)^2) + weighting_preference * mean(sigma), every term a mean, with the gradient of all three terms handed to the minimiser and the forward model as one matrix product. Across nine noise realisations and four starting points per image, the spread between starts drops from 0.8 to 1e-4 and the worst rmse against the input profiles from 0.45 to 0.031, at no more than 264 cost evaluations per image (#134, #155). average_imagesreaches the analysis. The whole averaged path was dead: the file name was a one-element tuple, the averaged binary was written under one name and read under another, and the single-camera analysis never read the option at all, so the analysis silently ran on unaveraged data. The averaged binary now has a file name of its own, is rewritten instead of appended to, and is rebuilt when the extraction results,config_analysis.inior the reference images it was built with have changed (#161, #163, #165, #168).- The binary data are rebuilt when their sources are newer.
all_parameters.h5was only built when it was missing, so re-running step 3 and then the analysis silently analysed the values of the first run. It is now rebuilt when a step 3 result file,led_search_areas_with_coordinates.csvorconfig.iniis newer, and the rebuild is reported (#92, #164). all_parameters.h5is rewritten instead of appended to.create_binary_datawrote the file withappend=Trueand nothing removed the file of an earlier run, so every LED landed in the table once per run and the analysis saw each image as often as the step had been repeated. The file is now replaced, and the replacement is reported (#158).- Five defects that silently produced wrong results (#166):
- The analysis used the images from
first_img_experiment_idinstead offirst_img_analysis_id: a config naming the images 500 to 800 inverted the images 1 to 301. - An LED whose light path the layers do not fully cover produced a row of NaN, so the nonlinear solver returned zeros without a warning. It now raises
InconsistentDistanceError, naming the LED, the camera, the layer range and the option to widen. - Step 1 did not blank out an LED within
search_area_radiusof the top or left image border, so that LED filled every slot and the LEDs behind it were never found. - The colour corrected channel values were cast to
int16and wrapped: asum_col_valof 72000 was stored as 6464. - With
num_skip_imgs, the last image of the range was dropped from the analysis.
- The analysis used the images from
- Step 3 result files of different width are reported by name. The column names were taken from the first result file of a channel and used for all of them, so a channel whose step 3 was re-run for part of its images stopped with
Shape of passed values is (n, 12), indices imply (n, 10), which says nothing about the cause. The error now names both files, both column counts and the re-run that fixes it (#158).
Other
- The first header line of every step 3 result file records the LEDSA version, as the headers of the analysis results already did, so a change of the values behind an unchanged column layout can be told from the file (#158).
- A missing color corrected reference property now names the channel, the available columns and
--ccinstead of raising a bareKeyError(#164). - The README is shortened from 386 to 224 lines, with the details left to the documentation, and the PyPI badge shows the released version (#159, #172). The documentation build is skipped when only the README changes (#171).
- 368 unit tests, among them the new
test_image_orientation.py,test_black_level.py,test_extinction_coefficients_nonlinear.py,test_temperature_correction.py,test_analysis_quantities.py,test_average_images.pyandtest_led_weighting.py. New acceptance tests cover vertical images, several quantities in one run and the averaged analysis.
Needs attention when updating
- Three defaults change the numbers: the channel data are unclipped, the SNR weighting of the linear solver is on (
snr_full_weight = 10;Noneswitches it off), andsidebg_sum_col_valandbgsub2_sum_col_valhave no lower limit (background_floor = None). Where the signal of an LED has vanished in the read noise, the linear solver now returns NaN instead of a number that is too low.bgsub_sum_col_valchanges for Bayer images. - A
config.iniwritten by v0.11.0 keepsbackground_floor = 0.05, because the old template wrote the value into the file. Set it toNonethere to get the new default. - The results of RAW images change, through both the black level and the unclipped data. They move when step 3 is run again, not when the version is installed - re-running only the analysis on existing result files gives the old numbers, and the SNR weighting stays off for them because they carry no noise column.
- Extinction coefficients are written to a directory per quantity,
analysis/extinction_coefficients/<solver>/<quantity>/. Scripts that read the result files from the directory of the solver need the new path;SimDataandStackedSimDatafind both. weighting_curvaturemultiplies the mean squared second difference from now on, so a value tuned for the old term has to be set again; the default moves from 1e-6 to 1e-2 andnum_iterationsfrom 200 to 1000.- A run whose
domain_boundsdo not hold every LED and the camera now stops withInconsistentDistanceErrorinstead of returning zeros. - An analysis with
first_img_analysis_iddifferent fromfirst_img_experiment_id, or withnum_skip_imgs, now covers different images than before, which are the ones the config names. img_rotationis part of reading an image, so step 1 and step 3 have to use the same value: changing it means running the workflow again from-s1, because the search areas no longer fit an image read in another orientation.- Config files of v0.11.0 run without changes: every new option falls back to its default when it is missing.
Full Changelog: v0.11.0...v0.12.0