Skip to content

Take the black level of RAW images from the optical black - #158

Merged
kriboe90 merged 3 commits into
masterfrom
develop_optical_black_level
Oct 27, 2026
Merged

kriboe90 merged 3 commits into
masterfrom
develop_optical_black_level

Conversation

@kriboe90

Copy link
Copy Markdown
Contributor

Split out of the draft #157, which keeps the parts that are still open. This PR holds only what is finished.

1. The black level of RAW images comes from the optical black

Problem. sum_col_val carries an additive offset that does not vanish when the LED dims, so the extinction coefficients saturate in dense smoke. The problem statement of #145 names three sources of it. One is that the black level LEDSA subtracts comes from the RAW metadata, which is a nominal value of the camera model and differs from the actual level of the sensor by a fraction of a count. Integrated over the search area, that fraction becomes a constant offset of the intensity of every LED.

Change. _black_level takes the black level from the mean of the optical black pixels of the image itself. These are masked pixels at the margin of the sensor, read out together with the image, so the estimate also follows the drift of the black level with the sensor temperature. raw.black_level_per_channel[channel] stays as the fallback for sensors whose margin is narrower than MIN_OPTICAL_BLACK_WIDTH (8 px). The package subtracts a black level in exactly one place, _read_img_array_from_raw_file, so the change reaches sum_col_val, bgsub_sum_col_val, sidebg_sum_col_val and bgsub2_sum_col_val alike. JPG and PNG have no black level and are untouched.

It is not an option, because nobody can reasonably prefer a nominal black level to a measured one, and because an option would turn the background correction into black level handling times spatial estimate, which the layout dispatch by column count cannot carry.

The results do change for RAW images, which breaks the "default results are unchanged" rule that #145 was merged under. That is the purpose of the change. The column layout does not show it, so the step 3 header now records the LEDSA version, as the headers of the analysis results already do.

2. The binary data are rewritten instead of appended

Two pre-existing problems next to #92, found while checking what part 1 breaks:

  • create_binary_data wrote all_parameters.h5 with append=True and nothing removed the file of an earlier run, so every LED ended up in the table once per run and the analysis saw each image as often as the step was repeated. The loop always reads the files 1 to N from the start, so appending could only ever duplicate and was never able to extend the table with images it had not already read. The file is now removed and rewritten, and the replacement is reported.
  • The column names are taken from the first result file of a channel and used for all of them, so result files of different width cannot be loaded together. That happens exactly when step 3 is run again for part of the images, which part 1 invites. Until now pandas 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 counts and the re-run of the data extraction.

Backwards compatibility

  • No column is added, no config option, no API change, so every step 3 layout is untouched and old config.ini and config_analysis.ini files run unchanged.
  • The width a result file is compared against is len(columns) - 3, which holds for every branch of _get_column_names, the two legacy ones included (LEGACY_FIT_COLUMNS at 18 columns and _get_old_columns at 15 and 4), so the new check rejects no layout that loaded before.
  • The extra , LEDSA x.y.z on the first header line is inert: read_table skips the comment lines and nothing parses the first one.
  • Installing this alone changes no number. The values live in the step 3 result files, so 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.
  • Delete analysis/channel*/all_parameters.h5 when re-running the analysis on data extracted with an older version. With part 2 that is no longer silent.

Tests

Unit tests pass, 191. New are test_black_level.py for the estimate and the fallback and six tests in test_file_handling.py for the two guards. The fake RAW image of the orientation tests gained the margin sizes, so it now covers the fallback as well.

Not covered: no RAW image is read by any unit test, so the optical black estimate itself still rests on the comparison against BCL on the Warsaw tests V008 and V009, camera 2. The acceptance tests were not run locally. The unit tests ran under numpy 2.2.6 here, outside the numpy ~= 1.26.4 of pyproject.toml, so the CI is the first run on the supported version.

What is not in here

The other half of the correction stays in the draft #157: dropping the clipping at 0, the ob_sum_col_val and ob_noise_sum_col_value columns, the SNR weighting and the image averaging. Dropping the clipping lets the sums go negative and so changes a contract that max_col_value, the fit and the postprocessing rely on, and it needs the nonlinear solver and the stacked analysis to be covered first.

🤖 Generated with Claude Code

kriboe90 and others added 2 commits October 27, 2026 23:17
The black level of the RAW metadata is a nominal value of the camera
model. The optical black pixels are masked pixels at the margin of the
sensor, read out together with the image itself, so their mean is the
black level of this image and follows the drift with the sensor
temperature. The two differ by a fraction of a count, and the
integration over the search area of an LED turns that difference into a
constant offset of its intensity, which stays while the LED dims in
smoke and so saturates the extinction coefficients.

_black_level resolves this for the only place that subtracts a black
level, so it reaches sum_col_val, bgsub_sum_col_val, sidebg_sum_col_val
and bgsub2_sum_col_val alike. The metadata value remains the fallback
for sensors whose optical black margin is narrower than
MIN_OPTICAL_BLACK_WIDTH. JPG and PNG have no black level and are
untouched.

The values of the four columns above therefore change for RAW images,
which the column layout alone does not show, so the step 3 header now
records the LEDSA version as the headers of the analysis results
already do. The change only reaches existing experiments when step 3 is
run again for them.

The fake RAW image of the orientation tests gains the margin sizes and
so covers the fallback.

Unit tests pass, 185, with test_black_level.py for the estimate and the
fallback. Acceptance tests not run, and no RAW image is covered by the
unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
create_binary_data wrote all_parameters.h5 with append=True and nothing
removed the file of an earlier run, so every LED ended up in the table
once per run and the analysis saw each image as often as the step was
repeated. The binary data are derived from the step 3 result files, so
they are now rewritten from scratch and the replacement is reported.

The column names are taken from the first result file of a channel and
used for all of them, so result files of different width cannot be
loaded together. That happens when step 3 is run again for part of the
images after a LEDSA version that writes further columns was installed.
Until now pandas stopped with "Shape of passed values is (n, 12),
indices imply (n, 10)", which says nothing about the cause. The width of
every file is now compared with the one of the first file and the error
names both files, both counts and the re-run of the data extraction.

Unit tests pass, 133, with six new ones in test_file_handling.py. They
write a coordinate file, so they take the same path through
_append_coordinates as a real analysis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CI check for text I/O without an encoding is line based, so the
encoding of a wrapped call reads as missing to it although Python sees
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kriboe90
kriboe90 merged commit 8355d88 into master Oct 27, 2026
5 checks passed
@kriboe90
kriboe90 deleted the develop_optical_black_level branch October 27, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant