Skip to content

fix(algorithms): consolidate makeuint8/makeuint16 into a shared helper - #138

Merged
ehennestad merged 3 commits into
devfrom
consolidate-makeuint-functions
Sep 17, 2026
Merged

ehennestad merged 3 commits into
devfrom
consolidate-makeuint-functions

Conversation

@ehennestad

@ehennestad ehennestad commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Consolidates stack.makeuint8 and stack.makeuint16, which duplicated the same intensity-rescaling logic, into a shared private helper, normalizeIntensity. Both public functions are now thin wrappers with arguments blocks and full help text. Adds a parameterized unit test class covering both.

Four bugs in the original code are fixed along the way:

  • makeuint16 cast its output to uint8, so its only caller, the uint16 export path in MotionCorrection, received saturated 8-bit data instead of 16-bit output.
  • makeuint16 scaled by 2^16-1 without parentheses, so operator precedence produced an off-by-one error across most of the range.
  • The float-class check ~isa(x,'single') || ~isa(x,'double') is always true, so double input was always downcast to single.
  • A custom tolerance was silently discarded whenever bLims was empty, because the guard checked the wrong variable.

Details on each are in the fix commit message.

The documented 1x2xn bLims form for 4-D multi-channel input was already broken before this change and is left as is, since nothing in the codebase calls it.

🤖 Generated with Claude Code

ehennestad and others added 2 commits September 16, 2026 16:57
makeuint8 and makeuint16 duplicated the same intensity-rescaling logic
end to end, differing only in the target integer class. Extract that
logic into a new private helper, stack.normalizeIntensity, parameterized
by target class, and make both public functions thin wrappers around it
with arguments blocks and full help text.

Consolidating the two implementations surfaced four defects that were
present in the prior code:

- makeuint16 cast its result with `uint8(...)` instead of `uint16(...)`,
  returning uint8-classed data from a function whose entire purpose is
  to produce uint16 output. This looks like a copy-paste-from-makeuint8
  artifact: the docstring, the 2^16 scale factor, and even a "[ ]
  Combine with makeuint8" to-do comment were updated for a 16-bit
  function, but this one line was not. The only caller in the codebase,
  MotionCorrection's `dataTypeOut == "uint16"` export path, received
  uint8-classed, almost entirely saturated data instead of properly
  scaled uint16 output whenever a user configured 16-bit motion-
  correction export with brightness recasting enabled. Verified against
  realistic 16-bit-range input: 100% of output pixels saturated to 255,
  collapsing the corrected stack to a single constant value.
- makeuint16 additionally scaled with `normalized .* 2^16 - 1`. Operator
  precedence makes `.*` bind tighter than binary `-`, so this computed
  `(normalized .* 2^16) - 1` rather than the intended
  `normalized .* (2^16 - 1)`, an off-by-about-one error across most of
  the output range, independent of the class-cast defect above. The
  shared helper scales by intmax(targetClass) instead, which removes
  the magic number and the ambiguity for both classes.
- The float-class guard `~isa(x,'single') || ~isa(x,'double')` is a
  tautology: by De Morgan's law it evaluates true for every class, so
  double-precision input was unconditionally downcast to single before
  rescaling, even though the code's intent was clearly to skip that
  cast for inputs already in a floating-point class.
- The tolerance default was applied whenever bLims was empty, because
  the guard checked `isempty(bLims)` instead of `isempty(tolerance)`.
  Any tolerance a caller supplied was silently discarded and replaced
  with 0.0005 whenever bLims was not also given.

The 1x2xn bLims convention documented for 4-D, multi-color-channel
input was already broken before this change (confirmed against the
prior implementation with the same input). It remains unfixed here:
no caller in the codebase exercises it, and correcting it needs a
confirmed contract for which dimension holds color, which is not
available from the current code or callers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the shared rescaling behavior of both functions in one
parameterized test class: explicit vs. estimated limits, saturation
outside bLims, output size/class, NaN exclusion during limit
estimation, cropAmount, and argument validation.

Also adds regression tests for the two behavioral bugs fixed in the
prior commit: a custom tolerance now actually changes the estimated
limits instead of being silently discarded, and makeuint16's midpoint
output now lands exactly on the correct value instead of the
operator-precedence error's off-by-about-one result. A further test
checks makeuint8 and makeuint16 agree on relative scaling now that
they share one implementation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results

333 tests   333 ✅  30s ⏱️
 24 suites    0 💤
  1 files      0 ❌

Results for commit b61b835.

@ehennestad
ehennestad merged commit cebde04 into dev Sep 17, 2026
@ehennestad
ehennestad deleted the consolidate-makeuint-functions branch September 17, 2026 12:24
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