Files
Jungfraujoch/common/ColorScale.cpp
T
leonarski_fandClaude Opus 5 a29c36600f Beam-stop shadow detection, and a low-resolution limit for scaling
rugnux finds the beam stop and its holder in a projection of 60 images and
marks them in the pixel mask as bit 9 (--detect-beam-stop[=N|off], on by
default). Reflections behind the stop are attenuated but not flagged, so they
integrate low with a plausible sigma and nothing downstream catches them: the
signal-box gate requires 100% valid pixels and shadow pixels are valid, the
background clip is high-side only, and the |zeta| cut applies only to the
space-group search merge.

The detection compares each pixel's background against the typical background
at the same radius on two channels. An azimuthal one (the ring median) finds
the holder arm, which is a minority of its ring; a radial one (the background
just outside) finds the disk, which the ring median cannot see because inside a
fully blocked ring the median is the shadow itself. Pixels are pooled over a
5x5 box and tested only where the background has actually been counted, so
low-background data no longer masks the whole detector. Recorded reflections
are carved back out - a beam stop cannot block a reflection that was measured.

Bit 9 belongs to the run that found it, not to the dataset: it is cleared when
a run starts, so a mask read back from a file that carries one starts clear.
The user mask (bit 8) is left alone.

Scaling and merging gain a low-resolution limit, default 50 A
(--scaling-low-resolution <num>, 0 removes it), applied per observation before
scaling so it also protects the per-frame scale fit and the space-group search.
50 A is the value XDS configurations use; rugnux_vs_xds.py now matches both of
XDS's resolution limits instead of only the high one, so the lowest shell is
the same shell in the two programs.

The viewer draws the detected shadow in coral with a "Show beam stop" switch in
the side panel, exposes the low-resolution limit in the settings dock, and
offers detection in its processing jobs. Adding an image marker meant giving
the reader a MIN_REAL_PXL_VALUE, because several places classify a pixel by
range rather than by equality and would otherwise read the new marker as a very
negative intensity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:05:31 +02:00

139 lines
3.9 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <cmath>
#include "ColorScale.h"
#include "JFJochException.h"
static inline float Clamp01(float x) {
return (x < 0.0f) ? 0.0f : (x > 1.0f ? 1.0f : x);
}
// Gamma-mapped green (recommended gamma = 0.7)
static inline rgb GreenGamma(float f, float gamma = 0.7f) {
f = Clamp01(f);
const float g = std::pow(f, gamma);
const uint8_t G = static_cast<uint8_t>(std::lround(255.0f * g));
return {.r = 0, .g = G, .b = 0};
}
// Asinh-mapped green (recommended k = 8.0)
static inline rgb GreenAsinh(float f, float k = 8.0f) {
f = Clamp01(f);
const float g = std::asinh(k * f) / std::asinh(k);
const uint8_t G = static_cast<uint8_t>(std::lround(255.0f * g));
return {.r = 0, .g = G, .b = 0};
}
float luminance(rgb input) {
return 0.2126f * input.r + 0.7152f * input.g + 0.0722f * input.b;
}
ColorScale::ColorScale() : lut_(kLutSize) {
CalcLUT();
}
void ColorScale::Select(ColorScaleEnum val) {
current = val;
CalcLUT();
}
const std::vector<rgb> &ColorScale::LUTData() const {
return lut_;
}
void ColorScale::CalcLUT() const {
const std::vector<rgb>* map = nullptr;
switch (current) {
case ColorScaleEnum::Viridis: map = &viridis_colormap; break;
case ColorScaleEnum::Heat: map = &heat_colormap;
break;
case ColorScaleEnum::Indigo: map = &white_to_indigo_colormap;
break;
case ColorScaleEnum::BW: map = &white_to_black_colormap;
break;
case ColorScaleEnum::WB: map = &black_to_white_colormap;
break;
case ColorScaleEnum::Green:
map = &green_colormap;
break;
case ColorScaleEnum::Magma:
map = &magma_colormap; break;
case ColorScaleEnum::Inferno:
map = &inferno_colormap; break;
default:
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Color scale unknown");
}
for (size_t i = 0; i < kLutSize; ++i) {
const float f = static_cast<float>(i) / static_cast<float>(kLutSize - 1);
lut_[i] = Apply(f, *map);
}
}
rgb ColorScale::Apply(float input, const std::vector<rgb> &map) {
size_t num_colors = map.size();
if (num_colors < 2) {
throw std::invalid_argument("Colormap must have at least two colors.");
}
float scaled_value = input * (num_colors - 1);
size_t lower_idx = static_cast<size_t>(scaled_value);
size_t upper_idx = std::min(lower_idx + 1, num_colors - 1);
float t = scaled_value - lower_idx; // Fraction for interpolation
rgb lower = map[lower_idx];
rgb upper = map[upper_idx];
return {
.r = static_cast<uint8_t>(lower.r + t * (upper.r - lower.r)),
.g = static_cast<uint8_t>(lower.g + t * (upper.g - lower.g)),
.b = static_cast<uint8_t>(lower.b + t * (upper.b - lower.b))
};
}
rgb ColorScale::Apply(ColorScaleSpecial input) const {
switch (input) {
case ColorScaleSpecial::Gap:
return gap;
case ColorScaleSpecial::BeamStop:
return beam_stop;
default:
case ColorScaleSpecial::BadPixel:
return bad;
}
}
rgb ColorScale::Apply(float input, float min, float max) const {
if (!std::isfinite(input))
return gap;
float f;
if (input <= min)
f = 0.0f;
else if (input >= max)
f = 1.0f;
else
f = (input - min) / (max - min);
const size_t idx = static_cast<size_t>(f * static_cast<float>(kLutSize - 1));
return lut_[idx];
}
rgb ColorScale::ApplyLUTIndex(size_t idx) const {
if (idx >= kLutSize)
return lut_[kLutSize-1];
return lut_[idx];
}
ColorScale &ColorScale::Gap(rgb input) {
gap = input;
return *this;
}
ColorScale &ColorScale::BadPixel(rgb input) {
bad = input;
return *this;
}