mirror of
https://github.com/hyperion-project/hyperion.ng.git
synced 2023-10-10 13:36:59 +02:00
Feature: Temporal Color Smoothing with variable decay-rate for long period average windows (#1043)
* Feature: Weighted Moving Average Smoothing with Decay * fix assign * try fix MSVC error related to always inline on static * use proper imports for windows * crossplatform inline declaration
This commit is contained in:
parent
83455441fa
commit
0dd8e45364
@ -282,6 +282,7 @@
|
||||
"edt_conf_enum_hsv": "HSV",
|
||||
"edt_conf_enum_left_right": "Left to right",
|
||||
"edt_conf_enum_linear": "Linear",
|
||||
"edt_conf_enum_decay": "Decay",
|
||||
"edt_conf_enum_logdebug": "Debug",
|
||||
"edt_conf_enum_logsilent": "Silent",
|
||||
"edt_conf_enum_logverbose": "Verbose",
|
||||
@ -375,6 +376,14 @@
|
||||
"edt_conf_smooth_type_title": "Type",
|
||||
"edt_conf_smooth_updateDelay_expl": "Delay the output in case your ambient light is faster than your TV.",
|
||||
"edt_conf_smooth_updateDelay_title": "Update delay",
|
||||
"edt_conf_smooth_interpolationRate_expl": "Speed of the calculation of smooth intermediate frames.",
|
||||
"edt_conf_smooth_interpolationRate_title": "Interpolation Rate",
|
||||
"edt_conf_smooth_outputRate_title": "Output Rate",
|
||||
"edt_conf_smooth_outputRate_expl": "The output speed to your led controller.",
|
||||
"edt_conf_smooth_decay_title": "Decay-Power",
|
||||
"edt_conf_smooth_decay_expl": "The speed of decay. 1 is linear, greater values are have stronger effect.",
|
||||
"edt_conf_smooth_dithering_title": "Dithering",
|
||||
"edt_conf_smooth_dithering_expl": "Improve color accuracy at high output speeds by alternating between adjacent colors.",
|
||||
"edt_conf_smooth_updateFrequency_expl": "The output speed to your led controller.",
|
||||
"edt_conf_smooth_updateFrequency_title": "Update frequency",
|
||||
"edt_conf_v4l2_blueSignalThreshold_expl": "Darkens low blue values (recognized as black)",
|
||||
|
@ -533,7 +533,7 @@ function createJsonEditor(container,schema,setconfig,usePanel,arrayre)
|
||||
{
|
||||
for(var key in editor.root.editors)
|
||||
{
|
||||
editor.getEditor("root."+key).setValue( window.serverConfig[key] );
|
||||
editor.getEditor("root."+key).setValue(Object.assign({}, editor.getEditor("root."+key).value, window.serverConfig[key] ));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,10 @@
|
||||
"type" : "linear",
|
||||
"time_ms" : 200,
|
||||
"updateFrequency" : 25.0000,
|
||||
"interpolationRate" : 25.0000,
|
||||
"outputRate" : 25.0000,
|
||||
"decay" : 1,
|
||||
"dithering" : false,
|
||||
"updateDelay" : 0,
|
||||
"continuousOutput" : true
|
||||
},
|
||||
|
@ -6,6 +6,39 @@
|
||||
#include <hyperion/Hyperion.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
/// The number of microseconds per millisecond = 1000.
|
||||
const int64_t MS_PER_MICRO = 1000;
|
||||
|
||||
#if defined(COMPILER_GCC)
|
||||
#define ALWAYS_INLINE inline __attribute__((__always_inline__))
|
||||
#elif defined(COMPILER_MSVC)
|
||||
#define ALWAYS_INLINE __forceinline
|
||||
#else
|
||||
#define ALWAYS_INLINE inline
|
||||
#endif
|
||||
|
||||
/// Clamps the rounded values to the byte-interval of [0, 255].
|
||||
ALWAYS_INLINE long clampRounded(const floatT x) {
|
||||
return std::min(255l, std::max(0l, std::lroundf(x)));
|
||||
}
|
||||
|
||||
/// The number of bits that are used for shifting the fixed point values
|
||||
const int FPShift = (sizeof(uint64_t)*8 - (12 + 9));
|
||||
|
||||
/// The number of bits that are reduce the shifting when converting from fixed to floating point. 8 bits = 256 values
|
||||
const int SmallShiftBis = sizeof(uint8_t)*8;
|
||||
|
||||
/// The number of bits that are used for shifting the fixed point values plus SmallShiftBis
|
||||
const int FPShiftSmall = (sizeof(uint64_t)*8 - (12 + 9 + SmallShiftBis));
|
||||
|
||||
const char* SETTINGS_KEY_SMOOTHING_TYPE = "type";
|
||||
const char* SETTINGS_KEY_INTERPOLATION_RATE = "interpolationRate";
|
||||
const char* SETTINGS_KEY_OUTPUT_RATE = "outputRate";
|
||||
const char* SETTINGS_KEY_DITHERING = "dithering";
|
||||
const char* SETTINGS_KEY_DECAY = "decay";
|
||||
|
||||
using namespace hyperion;
|
||||
|
||||
@ -22,11 +55,13 @@ LinearColorSmoothing::LinearColorSmoothing(const QJsonDocument& config, Hyperion
|
||||
, _settlingTime(DEFAUL_SETTLINGTIME)
|
||||
, _timer(new QTimer(this))
|
||||
, _outputDelay(DEFAUL_OUTPUTDEPLAY)
|
||||
, _smoothingType(SmoothingType::Linear)
|
||||
, _writeToLedsEnable(false)
|
||||
, _continuousOutput(false)
|
||||
, _pause(false)
|
||||
, _currentConfigId(0)
|
||||
, _enabled(false)
|
||||
, tempValues(std::vector<uint64_t>(0, 0l))
|
||||
{
|
||||
// init cfg 0 (default)
|
||||
addConfig(DEFAUL_SETTLINGTIME, DEFAUL_UPDATEFREQUENCY, DEFAUL_OUTPUTDEPLAY);
|
||||
@ -34,13 +69,15 @@ LinearColorSmoothing::LinearColorSmoothing(const QJsonDocument& config, Hyperion
|
||||
selectConfig(0, true);
|
||||
|
||||
// add pause on cfg 1
|
||||
SMOOTHING_CFG cfg = {true, 0, 0, 0};
|
||||
SMOOTHING_CFG cfg = {SmoothingType::Linear, 0, 0, 0, 0, 0, false};
|
||||
_cfgList.append(cfg);
|
||||
|
||||
// listen for comp changes
|
||||
connect(_hyperion, &Hyperion::compStateChangeRequest, this, &LinearColorSmoothing::componentStateChange);
|
||||
// timer
|
||||
connect(_timer, &QTimer::timeout, this, &LinearColorSmoothing::updateLeds);
|
||||
|
||||
Info(_log, "LinearColorSmoothing sizeof floatT == %d", (sizeof(floatT)));
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::handleSettingsUpdate(settings::type type, const QJsonDocument &config)
|
||||
@ -56,11 +93,25 @@ void LinearColorSmoothing::handleSettingsUpdate(settings::type type, const QJson
|
||||
|
||||
_continuousOutput = obj["continuousOutput"].toBool(true);
|
||||
|
||||
SMOOTHING_CFG cfg = {false,
|
||||
static_cast<int64_t>(obj["time_ms"].toInt(DEFAUL_SETTLINGTIME)),
|
||||
static_cast<int64_t>(1000.0/obj["updateFrequency"].toDouble(DEFAUL_UPDATEFREQUENCY)),
|
||||
static_cast<unsigned>(obj["updateDelay"].toInt(DEFAUL_OUTPUTDEPLAY))
|
||||
};
|
||||
SMOOTHING_CFG cfg = {SmoothingType::Linear,true, 0, 0, 0, 0, 0, false, 1};
|
||||
|
||||
const QString typeString = obj[SETTINGS_KEY_SMOOTHING_TYPE].toString();
|
||||
|
||||
if(typeString == "linear") {
|
||||
cfg.smoothingType = SmoothingType::Linear;
|
||||
} else if(typeString == "decay") {
|
||||
cfg.smoothingType = SmoothingType::Decay;
|
||||
}
|
||||
|
||||
cfg.pause = false;
|
||||
cfg.settlingTime = static_cast<int64_t>(obj["time_ms"].toInt(DEFAUL_SETTLINGTIME));
|
||||
cfg.updateInterval = static_cast<int64_t>(1000.0 / obj["updateFrequency"].toDouble(DEFAUL_UPDATEFREQUENCY));
|
||||
cfg.outputRate = obj[SETTINGS_KEY_OUTPUT_RATE].toDouble(DEFAUL_UPDATEFREQUENCY);
|
||||
cfg.interpolationRate = obj[SETTINGS_KEY_INTERPOLATION_RATE].toDouble(DEFAUL_UPDATEFREQUENCY);
|
||||
cfg.outputDelay = static_cast<unsigned>(obj["updateDelay"].toInt(DEFAUL_OUTPUTDEPLAY));
|
||||
cfg.dithering = obj[SETTINGS_KEY_DITHERING].toBool(false);
|
||||
cfg.decay = obj[SETTINGS_KEY_DECAY].toDouble(1.0);
|
||||
|
||||
//Debug( _log, "smoothing cfg_id %d: pause: %d bool, settlingTime: %d ms, interval: %d ms (%u Hz), updateDelay: %u frames", _currentConfigId, cfg.pause, cfg.settlingTime, cfg.updateInterval, unsigned(1000.0/cfg.updateInterval), cfg.outputDelay );
|
||||
_cfgList[0] = cfg;
|
||||
|
||||
@ -79,15 +130,18 @@ void LinearColorSmoothing::handleSettingsUpdate(settings::type type, const QJson
|
||||
|
||||
int LinearColorSmoothing::write(const std::vector<ColorRgb> &ledValues)
|
||||
{
|
||||
_targetTime = QDateTime::currentMSecsSinceEpoch() + _settlingTime;
|
||||
_targetTime = micros() + (MS_PER_MICRO * _settlingTime);
|
||||
_targetValues = ledValues;
|
||||
|
||||
rememberFrame(ledValues);
|
||||
|
||||
// received a new target color
|
||||
if (_previousValues.empty())
|
||||
{
|
||||
// not initialized yet
|
||||
_previousTime = QDateTime::currentMSecsSinceEpoch();
|
||||
_previousWriteTime = micros();
|
||||
_previousValues = ledValues;
|
||||
_previousInterpolationTime = micros();
|
||||
|
||||
//Debug( _log, "Start Smoothing timer: settlingTime: %d ms, interval: %d ms (%u Hz), updateDelay: %u frames", _settlingTime, _updateInterval, unsigned(1000.0/_updateInterval), _outputDelay );
|
||||
QMetaObject::invokeMethod(_timer, "start", Qt::QueuedConnection, Q_ARG(int, _updateInterval));
|
||||
@ -110,49 +164,340 @@ int LinearColorSmoothing::updateLedValues(const std::vector<ColorRgb>& ledValues
|
||||
return retval;
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::updateLeds()
|
||||
void LinearColorSmoothing::intitializeComponentVectors(const size_t ledCount)
|
||||
{
|
||||
int64_t now = QDateTime::currentMSecsSinceEpoch();
|
||||
int64_t deltaTime = _targetTime - now;
|
||||
// (Re-)Initialize the color-vectors that store the Mean-Value
|
||||
if (_ledCount != ledCount)
|
||||
{
|
||||
_ledCount = ledCount;
|
||||
|
||||
//Debug(_log, "elapsed Time [%d], _targetTime [%d] - now [%d], deltaTime [%d]", now -_previousTime, _targetTime, now, deltaTime);
|
||||
if (deltaTime < 0)
|
||||
const size_t len = 3 * ledCount;
|
||||
|
||||
meanValues = std::vector<floatT>(len, 0.0f);
|
||||
residualErrors = std::vector<floatT>(len, 0.0f);
|
||||
tempValues = std::vector<uint64_t>(len, 0l);
|
||||
}
|
||||
|
||||
// Zero the temp vector
|
||||
std::fill(tempValues.begin(), tempValues.end(), 0l);
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::writeDirect()
|
||||
{
|
||||
const int64_t now = micros();
|
||||
_previousValues = _targetValues;
|
||||
_previousTime = now;
|
||||
_previousWriteTime = now;
|
||||
|
||||
queueColors(_previousValues);
|
||||
_writeToLedsEnable = _continuousOutput;
|
||||
}
|
||||
else
|
||||
|
||||
|
||||
void LinearColorSmoothing::writeFrame()
|
||||
{
|
||||
_writeToLedsEnable = true;
|
||||
const int64_t now = micros();
|
||||
_previousWriteTime = now;
|
||||
queueColors(_previousValues);
|
||||
_writeToLedsEnable = _continuousOutput;
|
||||
}
|
||||
|
||||
//std::cout << "LinearColorSmoothing::updateLeds> _previousValues: "; LedDevice::printLedValues ( _previousValues );
|
||||
|
||||
float k = 1.0f - 1.0f * deltaTime / (_targetTime - _previousTime);
|
||||
|
||||
int reddif = 0, greendif = 0, bluedif = 0;
|
||||
|
||||
for (size_t i = 0; i < _previousValues.size(); ++i)
|
||||
ALWAYS_INLINE int64_t LinearColorSmoothing::micros() const
|
||||
{
|
||||
const auto now = std::chrono::high_resolution_clock::now();
|
||||
return (std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch())).count();
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::assembleAndDitherFrame()
|
||||
{
|
||||
if (meanValues.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The number of leds present in each frame
|
||||
const size_t N = _targetValues.size();
|
||||
|
||||
for (size_t i = 0; i < N; ++i)
|
||||
{
|
||||
// Add residuals for error diffusion (temporal dithering)
|
||||
const floatT fr = meanValues[3 * i + 0] + residualErrors[3 * i + 0];
|
||||
const floatT fg = meanValues[3 * i + 1] + residualErrors[3 * i + 1];
|
||||
const floatT fb = meanValues[3 * i + 2] + residualErrors[3 * i + 2];
|
||||
|
||||
// Convert to to 8-bit value
|
||||
const long ir = clampRounded(fr);
|
||||
const long ig = clampRounded(fg);
|
||||
const long ib = clampRounded(fb);
|
||||
|
||||
// Update the colors
|
||||
ColorRgb &prev = _previousValues[i];
|
||||
ColorRgb & target = _targetValues[i];
|
||||
prev.red = (uint8_t)ir;
|
||||
prev.green = (uint8_t)ig;
|
||||
prev.blue = (uint8_t)ib;
|
||||
|
||||
reddif = target.red - prev.red;
|
||||
greendif = target.green - prev.green;
|
||||
bluedif = target.blue - prev.blue;
|
||||
// Determine the component errors
|
||||
residualErrors[3 * i + 0] = fr - ir;
|
||||
residualErrors[3 * i + 1] = fg - ig;
|
||||
residualErrors[3 * i + 2] = fb - ib;
|
||||
}
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::assembleFrame()
|
||||
{
|
||||
if (meanValues.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The number of leds present in each frame
|
||||
const size_t N = _targetValues.size();
|
||||
|
||||
for (size_t i = 0; i < N; ++i)
|
||||
{
|
||||
// Convert to to 8-bit value
|
||||
const long ir = clampRounded(meanValues[3 * i + 0]);
|
||||
const long ig = clampRounded(meanValues[3 * i + 1]);
|
||||
const long ib = clampRounded(meanValues[3 * i + 2]);
|
||||
|
||||
// Update the colors
|
||||
ColorRgb &prev = _previousValues[i];
|
||||
prev.red = (uint8_t)ir;
|
||||
prev.green = (uint8_t)ig;
|
||||
prev.blue = (uint8_t)ib;
|
||||
}
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void LinearColorSmoothing::aggregateComponents(const std::vector<ColorRgb>& colors, std::vector<uint64_t>& weighted, const floatT weight) {
|
||||
// Determine the integer-scale by converting the weight to fixed point
|
||||
const uint64_t scale = (1l<<FPShift) * static_cast<double>(weight);
|
||||
|
||||
const size_t N = colors.size();
|
||||
|
||||
for (size_t i = 0; i < N; ++i)
|
||||
{
|
||||
const ColorRgb &color = colors[i];
|
||||
|
||||
// Scale the colors
|
||||
const uint64_t red = scale * color.red;
|
||||
const uint64_t green = scale * color.green;
|
||||
const uint64_t blue = scale * color.blue;
|
||||
|
||||
// Accumulate in the vector
|
||||
weighted[3 * i + 0] += red;
|
||||
weighted[3 * i + 1] += green;
|
||||
weighted[3 * i + 2] += blue;
|
||||
}
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::interpolateFrame()
|
||||
{
|
||||
const int64_t now = micros();
|
||||
|
||||
// The number of leds present in each frame
|
||||
const size_t N = _targetValues.size();
|
||||
|
||||
intitializeComponentVectors(N);
|
||||
|
||||
/// Time where the frame has been shown
|
||||
int64_t frameStart;
|
||||
|
||||
/// Time where the frame display would have ended
|
||||
int64_t frameEnd = now;
|
||||
|
||||
/// Time where the current window has started
|
||||
const int64_t windowStart = now - (MS_PER_MICRO * _settlingTime);
|
||||
|
||||
/// The total weight of the frames that were included in our window; sum of the individual weights
|
||||
floatT fs = 0.0f;
|
||||
|
||||
// To calculate the mean component we iterate over all relevant frames;
|
||||
// from the most recent to the oldest frame that still clips our moving-average window given by time (now)
|
||||
for (auto it = _frameQueue.rbegin(); it != _frameQueue.rend() && frameEnd > windowStart; ++it)
|
||||
{
|
||||
// Starting time of a frame in the window is clipped to the window start
|
||||
frameStart = std::max(windowStart, it->time);
|
||||
|
||||
// Weight the current frame relative to the overall window based on start and end times
|
||||
const floatT weight = _weightFrame(frameStart, frameEnd, windowStart);
|
||||
fs += weight;
|
||||
|
||||
// Aggregate the RGB components of this frame's LED colors using the individual weighting
|
||||
aggregateComponents(it->colors, tempValues, weight);
|
||||
|
||||
// The previous (earlier) frame display has ended when the current frame stared to show,
|
||||
// so we can use this as the frame-end time for next iteration
|
||||
frameEnd = frameStart;
|
||||
}
|
||||
|
||||
/// The inverse scaling factor for the color components, clamped to (0, 1.0]; 1.0 for fs < 1, 1 : fs otherwise
|
||||
const floatT inv_fs = ((fs < 1.0f) ? 1.0f : 1.0f / fs) / (1 << SmallShiftBis);
|
||||
|
||||
// Normalize the mean component values for the window (fs)
|
||||
for (size_t i = 0; i < 3 * N; ++i)
|
||||
{
|
||||
meanValues[i] = (tempValues[i] >> FPShiftSmall) * inv_fs;
|
||||
}
|
||||
|
||||
_previousInterpolationTime = now;
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::performDecay(const int64_t now) {
|
||||
/// The target time when next frame interpolation should be performed
|
||||
const int64_t interpolationTarget = _previousInterpolationTime + _interpolationIntervalMicros;
|
||||
|
||||
/// The target time when next write operation should be performed
|
||||
const int64_t writeTarget = _previousWriteTime + _outputIntervalMicros;
|
||||
|
||||
/// Whether a frame interpolation is pending
|
||||
const bool interpolatePending = now > interpolationTarget;
|
||||
|
||||
/// Whether a write is pending
|
||||
const bool writePending = now > writeTarget;
|
||||
|
||||
// Check whether a new interpolation frame is due
|
||||
if (interpolatePending)
|
||||
{
|
||||
interpolateFrame();
|
||||
++_interpolationCounter;
|
||||
|
||||
// Assemble the frame now when no dithering is applied
|
||||
if(!_dithering) {
|
||||
assembleFrame();
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether to frame output is due
|
||||
if (writePending)
|
||||
{
|
||||
// Dither the frame to diffuse rounding errors
|
||||
if(_dithering) {
|
||||
assembleAndDitherFrame();
|
||||
}
|
||||
|
||||
writeFrame();
|
||||
++_renderedCounter;
|
||||
}
|
||||
|
||||
// Check for sleep when no operation is pending.
|
||||
// As our QTimer is not capable of sub 1ms timing but instead performs spinning -
|
||||
// we have to do µsec-sleep to free CPU time; otherwise the thread would consume 100% CPU time.
|
||||
if(_updateInterval <= 0 && !(interpolatePending || writePending)) {
|
||||
const int64_t nextActionExpected = std::min(interpolationTarget, writeTarget);
|
||||
const int64_t microsTillNextAction = nextActionExpected - now;
|
||||
const int64_t SLEEP_MAX_MICROS = 1000l; // We want to use usleep for up to 1ms
|
||||
const int64_t SLEEP_RES_MICROS = 100l; // Expected resolution is >= 100µs on stock linux
|
||||
|
||||
if(microsTillNextAction > SLEEP_RES_MICROS) {
|
||||
const int64_t wait = std::min(microsTillNextAction - SLEEP_RES_MICROS, SLEEP_MAX_MICROS);
|
||||
//usleep(wait);
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(wait));
|
||||
}
|
||||
}
|
||||
|
||||
// Write stats every 30 sec
|
||||
if ((now > (_renderedStatTime + 30 * 1000000)) && (_renderedCounter > _renderedStatCounter))
|
||||
{
|
||||
Info(_log, "decay - rendered frames [%d] (%f/s), interpolated frames [%d] (%f/s) in [%f ms]"
|
||||
, _renderedCounter - _renderedStatCounter
|
||||
, (1.0f * (_renderedCounter - _renderedStatCounter) / ((now - _renderedStatTime) / 1000000.0f))
|
||||
, _interpolationCounter - _interpolationStatCounter
|
||||
, (1.0f * (_interpolationCounter - _interpolationStatCounter) / ((now - _renderedStatTime) / 1000000.0f))
|
||||
, (now - _renderedStatTime) / 1000.0f
|
||||
);
|
||||
_renderedStatTime = now;
|
||||
_renderedStatCounter = _renderedCounter;
|
||||
_interpolationStatCounter = _interpolationCounter;
|
||||
}
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::performLinear(const int64_t now) {
|
||||
const int64_t deltaTime = _targetTime - now;
|
||||
const float k = 1.0f - 1.0f * deltaTime / (_targetTime - _previousWriteTime);
|
||||
const size_t N = _previousValues.size();
|
||||
|
||||
for (size_t i = 0; i < N; ++i)
|
||||
{
|
||||
const ColorRgb &target = _targetValues[i];
|
||||
ColorRgb &prev = _previousValues[i];
|
||||
|
||||
const int reddif = target.red - prev.red;
|
||||
const int greendif = target.green - prev.green;
|
||||
const int bluedif = target.blue - prev.blue;
|
||||
|
||||
prev.red += (reddif < 0 ? -1:1) * std::ceil(k * std::abs(reddif));
|
||||
prev.green += (greendif < 0 ? -1:1) * std::ceil(k * std::abs(greendif));
|
||||
prev.blue += (bluedif < 0 ? -1:1) * std::ceil(k * std::abs(bluedif));
|
||||
}
|
||||
_previousTime = now;
|
||||
|
||||
//std::cout << "LinearColorSmoothing::updateLeds> _targetValues: "; LedDevice::printLedValues ( _targetValues );
|
||||
|
||||
queueColors(_previousValues);
|
||||
writeFrame();
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::updateLeds()
|
||||
{
|
||||
const int64_t now = micros();
|
||||
const int64_t deltaTime = _targetTime - now;
|
||||
|
||||
//Debug(_log, "elapsed Time [%d], _targetTime [%d] - now [%d], deltaTime [%d]", now -_previousWriteTime, _targetTime, now, deltaTime);
|
||||
if (deltaTime < 0)
|
||||
{
|
||||
writeDirect();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (_smoothingType)
|
||||
{
|
||||
case Decay:
|
||||
performDecay(now);
|
||||
break;
|
||||
|
||||
case Linear:
|
||||
// Linear interpolation is default
|
||||
default:
|
||||
performLinear(now);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::rememberFrame(const std::vector<ColorRgb> &ledColors)
|
||||
{
|
||||
//Info(_log, "rememberFrame - before _frameQueue.size() [%d]", _frameQueue.size());
|
||||
|
||||
const int64_t now = micros();
|
||||
|
||||
// Maintain the queue by removing outdated frames
|
||||
const int64_t windowStart = now - (MS_PER_MICRO * _settlingTime);
|
||||
|
||||
int p = -1; // Start with -1 instead of 0, so we keep the last frame at least partially clipping the window
|
||||
|
||||
// As the frames are ordered chronologically we scan from the front (oldest) till we find the first fresh frame
|
||||
for (auto it = _frameQueue.begin(); it != _frameQueue.end() && it->time < windowStart; ++it)
|
||||
{
|
||||
++p;
|
||||
}
|
||||
|
||||
if (p > 0)
|
||||
{
|
||||
//Info(_log, "rememberFrame - erasing %d frames", p);
|
||||
_frameQueue.erase(_frameQueue.begin(), _frameQueue.begin() + p);
|
||||
}
|
||||
|
||||
// Append the latest frame at back of the queue
|
||||
const REMEMBERED_FRAME frame = REMEMBERED_FRAME(now, ledColors);
|
||||
_frameQueue.push_back(frame);
|
||||
|
||||
//Info(_log, "rememberFrame - after _frameQueue.size() [%d]", _frameQueue.size());
|
||||
}
|
||||
|
||||
|
||||
void LinearColorSmoothing::clearRememberedFrames()
|
||||
{
|
||||
_frameQueue.clear();
|
||||
|
||||
_ledCount = 0;
|
||||
meanValues.clear();
|
||||
residualErrors.clear();
|
||||
tempValues.clear();
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::queueColors(const std::vector<ColorRgb> &ledColors)
|
||||
@ -196,6 +541,8 @@ void LinearColorSmoothing::clearQueuedColors()
|
||||
_previousValues.clear();
|
||||
|
||||
_targetValues.clear();
|
||||
|
||||
clearRememberedFrames();
|
||||
}
|
||||
|
||||
void LinearColorSmoothing::componentStateChange(hyperion::Components component, bool state)
|
||||
@ -230,7 +577,17 @@ void LinearColorSmoothing::setPause(bool pause)
|
||||
|
||||
unsigned LinearColorSmoothing::addConfig(int settlingTime_ms, double ledUpdateFrequency_hz, unsigned updateDelay)
|
||||
{
|
||||
SMOOTHING_CFG cfg = {false, settlingTime_ms, int64_t(1000.0/ledUpdateFrequency_hz), updateDelay};
|
||||
SMOOTHING_CFG cfg = {
|
||||
SmoothingType::Linear,
|
||||
false,
|
||||
settlingTime_ms,
|
||||
int64_t(1000.0 / ledUpdateFrequency_hz),
|
||||
ledUpdateFrequency_hz,
|
||||
ledUpdateFrequency_hz,
|
||||
updateDelay,
|
||||
false,
|
||||
1
|
||||
};
|
||||
_cfgList.append(cfg);
|
||||
|
||||
//Debug( _log, "smoothing cfg %d: pause: %d bool, settlingTime: %d ms, interval: %d ms (%u Hz), updateDelay: %u frames", _cfgList.count()-1, cfg.pause, cfg.settlingTime, cfg.updateInterval, unsigned(1000.0/cfg.updateInterval), cfg.outputDelay );
|
||||
@ -242,7 +599,16 @@ unsigned LinearColorSmoothing::updateConfig(unsigned cfgID, int settlingTime_ms,
|
||||
unsigned updatedCfgID = cfgID;
|
||||
if (cfgID < static_cast<unsigned>(_cfgList.count()))
|
||||
{
|
||||
SMOOTHING_CFG cfg = {false, settlingTime_ms, int64_t(1000.0/ledUpdateFrequency_hz), updateDelay};
|
||||
SMOOTHING_CFG cfg = {
|
||||
SmoothingType::Linear,
|
||||
false,
|
||||
settlingTime_ms,
|
||||
int64_t(1000.0 / ledUpdateFrequency_hz),
|
||||
ledUpdateFrequency_hz,
|
||||
ledUpdateFrequency_hz,
|
||||
updateDelay,
|
||||
false,
|
||||
1};
|
||||
_cfgList[updatedCfgID] = cfg;
|
||||
}
|
||||
else
|
||||
@ -266,9 +632,45 @@ bool LinearColorSmoothing::selectConfig(unsigned cfg, bool force)
|
||||
//Debug( _log, "selectConfig FORCED - _currentConfigId [%u], force [%d]", cfg, force);
|
||||
if (cfg < (unsigned)_cfgList.count())
|
||||
{
|
||||
_smoothingType = _cfgList[cfg].smoothingType;
|
||||
_settlingTime = _cfgList[cfg].settlingTime;
|
||||
_outputDelay = _cfgList[cfg].outputDelay;
|
||||
_pause = _cfgList[cfg].pause;
|
||||
_outputRate = _cfgList[cfg].outputRate;
|
||||
_outputIntervalMicros = int64_t(1000000.0 / _outputRate); // 1s = 1e6 µs
|
||||
_interpolationRate = _cfgList[cfg].interpolationRate;
|
||||
_interpolationIntervalMicros = int64_t(1000000.0 / _interpolationRate);
|
||||
_dithering = _cfgList[cfg].dithering;
|
||||
_decay = _cfgList[cfg].decay;
|
||||
_invWindow = 1.0f / (MS_PER_MICRO * _settlingTime);
|
||||
|
||||
// Set _weightFrame based on the given decay
|
||||
const float decay = _decay;
|
||||
const floatT inv_window = _invWindow;
|
||||
|
||||
// For decay != 1 use power-based approach for calculating the moving average values
|
||||
if(std::abs(decay - 1.0f) > std::numeric_limits<float>::epsilon()) {
|
||||
// Exponential Decay
|
||||
_weightFrame = [inv_window,decay](const int64_t fs, const int64_t fe, const int64_t ws) {
|
||||
const floatT s = (fs - ws) * inv_window;
|
||||
const floatT t = (fe - ws) * inv_window;
|
||||
|
||||
return (decay + 1) * (std::pow(t, decay) - std::pow(s, decay));
|
||||
};
|
||||
} else {
|
||||
// For decay == 1 use linear interpolation of the moving average values
|
||||
// Linear Decay
|
||||
_weightFrame = [inv_window](const int64_t fs, const int64_t fe, const int64_t ws) {
|
||||
// Linear weighting = (end - start) * scale
|
||||
return static_cast<floatT>((fe - fs) * inv_window);
|
||||
};
|
||||
}
|
||||
|
||||
_renderedStatTime = micros();
|
||||
_renderedCounter = 0;
|
||||
_renderedStatCounter = 0;
|
||||
_interpolationCounter = 0;
|
||||
_interpolationStatCounter = 0;
|
||||
|
||||
if (_cfgList[cfg].updateInterval != _updateInterval)
|
||||
{
|
||||
@ -290,6 +692,9 @@ bool LinearColorSmoothing::selectConfig(unsigned cfg, bool force)
|
||||
// DebugIf( enabled() && !_pause, _log, "set smoothing cfg: %u settlingTime: %d ms, interval: %d ms, updateDelay: %u frames", _currentConfigId, _settlingTime, _updateInterval, _outputDelay );
|
||||
// DebugIf( _pause, _log, "set smoothing cfg: %d, pause", _currentConfigId );
|
||||
|
||||
const float thalf = (1.0-std::pow(1.0/2, 1.0/_decay))*_settlingTime;
|
||||
Info( _log, "%s - Time: %d ms, outputRate %f Hz, interpolationRate: %f Hz, timer: %d ms, Dithering: %d, Decay: %f -> HalfTime: %f ms", _smoothingType == SmoothingType::Decay ? "decay" : "linear", _settlingTime, _outputRate, _interpolationRate, _updateInterval, _dithering ? 1 : 0, _decay, thalf);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
// STL includes
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
// Qt includes
|
||||
#include <QVector>
|
||||
@ -13,14 +14,62 @@
|
||||
// settings
|
||||
#include <utils/settings.h>
|
||||
|
||||
// The type of float
|
||||
#define floatT float // Select double, float or __fp16
|
||||
|
||||
class QTimer;
|
||||
class Logger;
|
||||
class Hyperion;
|
||||
|
||||
/// The type of smoothing to perform
|
||||
enum SmoothingType {
|
||||
/// "Linear" smoothing algorithm
|
||||
Linear,
|
||||
|
||||
/// Decay based smoothing algorithm
|
||||
Decay,
|
||||
};
|
||||
|
||||
/// Linear Smooting class
|
||||
///
|
||||
/// This class processes the requested led values and forwards them to the device after applying
|
||||
/// a linear smoothing effect. This class can be handled as a generic LedDevice.
|
||||
/// a smoothing effect to LED colors. This class can be handled as a generic LedDevice.
|
||||
///
|
||||
/// Currently, two types of smoothing are supported:
|
||||
///
|
||||
/// - Linear: A linear smoothing effect that interpolates the previous to the target colors.
|
||||
/// - Decay: A temporal smoothing effect that uses a decay based algorithm that interpolates
|
||||
/// colors based on the age of previous frames and a given decay-power.
|
||||
///
|
||||
/// The smoothing is performed on a history of relevant LED-color frames that are
|
||||
/// incorporated in the smoothing window (given by the configured settling time).
|
||||
///
|
||||
/// For each moment, all ingress frames that were received during the smoothing window
|
||||
/// are reduced to the concrete color values using a weighted moving average. This is
|
||||
/// done by applying a decay-controlled weighting-function to individual the colors of
|
||||
/// each frame.
|
||||
///
|
||||
/// Decay
|
||||
/// =====
|
||||
/// The decay-power influences the weight of individual frames based on their 'age'.
|
||||
///
|
||||
/// * A decay value of 1 indicates linear decay. The colors are given by the moving average
|
||||
/// with a weight that is strictly proportionate to the fraction of time each frame was
|
||||
/// visible during the smoothing window. As a result, equidistant frames will have an
|
||||
/// equal share when calculating an intermediate frame.
|
||||
///
|
||||
/// * A decay value greater than 1 indicates non-linear decay. With higher powers, the
|
||||
/// decay is stronger. I.e. newer frames in the smoothing window will have more influence
|
||||
/// on colors of intermediate frames than older ones.
|
||||
///
|
||||
/// Dithering
|
||||
/// =========
|
||||
/// A temporal dithering algorithm is used to minimize rounding errors, when downsampling
|
||||
/// the average color values to the 8-bit RGB resolution of the LED-device. Effectively,
|
||||
/// this performs diffusion of the residual errors across multiple egress frames.
|
||||
///
|
||||
///
|
||||
|
||||
class LinearColorSmoothing : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -96,7 +145,6 @@ private slots:
|
||||
void componentStateChange(hyperion::Components component, bool state);
|
||||
|
||||
private:
|
||||
|
||||
/**
|
||||
* Pushes the colors into the output queue and popping the head to the led-device
|
||||
*
|
||||
@ -134,15 +182,45 @@ private:
|
||||
std::vector<ColorRgb> _targetValues;
|
||||
|
||||
/// The timestamp of the previously written led data
|
||||
int64_t _previousTime;
|
||||
int64_t _previousWriteTime;
|
||||
|
||||
/// The timestamp of the previously data interpolation
|
||||
int64_t _previousInterpolationTime;
|
||||
|
||||
/// The previously written led data
|
||||
std::vector<ColorRgb> _previousValues;
|
||||
|
||||
/// The number of updates to keep in the output queue (delayed) before being output
|
||||
unsigned _outputDelay;
|
||||
|
||||
/// The output queue
|
||||
std::list<std::vector<ColorRgb> > _outputQueue;
|
||||
std::deque<std::vector<ColorRgb>> _outputQueue;
|
||||
|
||||
/// A frame of led colors used for temporal smoothing
|
||||
class REMEMBERED_FRAME
|
||||
{
|
||||
public:
|
||||
/// The time this frame was received
|
||||
int64_t time;
|
||||
|
||||
/// The led colors
|
||||
std::vector<ColorRgb> colors;
|
||||
|
||||
REMEMBERED_FRAME ( REMEMBERED_FRAME && ) = default;
|
||||
REMEMBERED_FRAME ( const REMEMBERED_FRAME & ) = default;
|
||||
REMEMBERED_FRAME & operator= ( const REMEMBERED_FRAME & ) = default;
|
||||
|
||||
REMEMBERED_FRAME(const int64_t time, const std::vector<ColorRgb> colors)
|
||||
: time(time)
|
||||
, colors(colors)
|
||||
{}
|
||||
};
|
||||
|
||||
/// The type of smoothing to perform
|
||||
SmoothingType _smoothingType;
|
||||
|
||||
/// The queue of temporarily remembered frames
|
||||
std::deque<REMEMBERED_FRAME> _frameQueue;
|
||||
|
||||
/// Prevent sending data to device when no intput data is sent
|
||||
bool _writeToLedsEnable;
|
||||
@ -153,17 +231,146 @@ private:
|
||||
/// Flag for pausing
|
||||
bool _pause;
|
||||
|
||||
/// The rate at which color frames should be written to LED device.
|
||||
double _outputRate;
|
||||
|
||||
/// The interval time in microseconds for writing of LED Frames.
|
||||
int64_t _outputIntervalMicros;
|
||||
|
||||
/// The rate at which interpolation of LED frames should be performed.
|
||||
double _interpolationRate;
|
||||
|
||||
/// The interval time in microseconds for interpolation of LED Frames.
|
||||
int64_t _interpolationIntervalMicros;
|
||||
|
||||
/// Whether to apply temproral dithering to diffuse rounding errors when downsampling to 8-bit RGB colors.
|
||||
bool _dithering;
|
||||
|
||||
/// The decay power > 0. A value of exactly 1 is linear decay, higher numbers indicate a faster decay rate.
|
||||
double _decay;
|
||||
|
||||
/// Value of 1.0 / settlingTime; inverse of the window size used for weighting of frames.
|
||||
floatT _invWindow;
|
||||
|
||||
struct SMOOTHING_CFG
|
||||
{
|
||||
bool pause;
|
||||
int64_t settlingTime;
|
||||
int64_t updateInterval;
|
||||
unsigned outputDelay;
|
||||
};
|
||||
/// The type of smoothing to perform
|
||||
SmoothingType smoothingType;
|
||||
|
||||
/// Whether to pause output
|
||||
bool pause;
|
||||
|
||||
/// The time of the smoothing window.
|
||||
int64_t settlingTime;
|
||||
|
||||
/// The interval time in millisecons of the timer used for scheduling LED update operations. A value of 0 indicates sub-millisecond timing.
|
||||
int64_t updateInterval;
|
||||
|
||||
// The rate at which color frames should be written to LED device.
|
||||
double outputRate;
|
||||
|
||||
/// The rate at which interpolation of LED frames should be performed.
|
||||
double interpolationRate;
|
||||
|
||||
/// The number of frames the output is delayed
|
||||
unsigned outputDelay;
|
||||
|
||||
/// Whether to apply temproral dithering to diffuse rounding errors when downsampling to 8-bit RGB colors. Improves color accuracy.
|
||||
bool dithering;
|
||||
|
||||
/// The decay power > 0. A value of exactly 1 is linear decay, higher numbers indicate a faster decay rate.
|
||||
double decay;
|
||||
};
|
||||
/// smooth config list
|
||||
QVector<SMOOTHING_CFG> _cfgList;
|
||||
|
||||
unsigned _currentConfigId;
|
||||
bool _enabled;
|
||||
|
||||
/// Pushes the colors into the frame queue and cleans outdated frames from memory.
|
||||
///
|
||||
/// @param ledColors The next colors to queue
|
||||
void rememberFrame(const std::vector<ColorRgb> &ledColors);
|
||||
|
||||
/// Frees the LED frames that were queued for calculating the moving average.
|
||||
void clearRememberedFrames();
|
||||
|
||||
/// (Re-)Initializes the color-component vectors with given number of values.
|
||||
///
|
||||
/// @param ledCount The number of colors.
|
||||
void intitializeComponentVectors(const size_t ledCount);
|
||||
|
||||
/// The number of led component-values that must be held per color; i.e. size of the color vectors reds / greens / blues
|
||||
size_t _ledCount = 0;
|
||||
|
||||
/// The average component colors red, green, blue of the leds
|
||||
std::vector<floatT> meanValues;
|
||||
|
||||
/// The residual component errors of the leds
|
||||
std::vector<floatT> residualErrors;
|
||||
|
||||
/// The accumulated led color values in 64-bit fixed point domain
|
||||
std::vector<uint64_t> tempValues;
|
||||
|
||||
/// Writes the target frame RGB data to the LED device without any interpolation.
|
||||
void writeDirect();
|
||||
|
||||
/// Writes the assembled RGB data to the LED device.
|
||||
void writeFrame();
|
||||
|
||||
/// Assembles a frame of LED colors in order to write RGB data to the LED device.
|
||||
/// Temporal dithering is applied to diffuse the downsampling error for RGB color components.
|
||||
void assembleAndDitherFrame();
|
||||
|
||||
/// Assembles a frame of LED colors in order to write RGB data to the LED device.
|
||||
/// No dithering is applied, RGB color components are just rounded to nearest integer.
|
||||
void assembleFrame();
|
||||
|
||||
/// Prepares a frame of LED colors by interpolating using the current smoothing window
|
||||
void interpolateFrame();
|
||||
|
||||
/// Performes a decay-based smoothing effect. The frames are interpolated based on their age and a given decay-power.
|
||||
///
|
||||
/// The ingress frames that were received during the current smoothing window are reduced using a weighted moving average
|
||||
/// by applying the weighting-function to the color components of each frame.
|
||||
///
|
||||
/// When downsampling the average color values to the 8-bit RGB resolution of the LED device, rounding errors are minimized
|
||||
/// by temporal dithering algorithm (error diffusion of residual errors).
|
||||
void performDecay(const int64_t now);
|
||||
|
||||
/// Performs a linear smoothing effect
|
||||
void performLinear(const int64_t now);
|
||||
|
||||
/// Aggregates the RGB components of the LED colors using the given weight and updates weighted accordingly
|
||||
///
|
||||
/// @param colors The LED colors to aggregate.
|
||||
/// @param weighted The target vector, that accumulates the terms.
|
||||
/// @param weight The weight to use.
|
||||
static inline void aggregateComponents(const std::vector<ColorRgb>& colors, std::vector<uint64_t>& weighted, const floatT weight);
|
||||
|
||||
/// Gets the current time in microseconds from high precision system clock.
|
||||
inline int64_t micros() const;
|
||||
|
||||
/// The time, when the rendering statistics were logged previously
|
||||
int64_t _renderedStatTime;
|
||||
|
||||
/// The total number of frames that were rendered to the LED device
|
||||
int64_t _renderedCounter;
|
||||
|
||||
/// The count of frames that have been rendered to the LED device when statistics were shown previously
|
||||
int64_t _renderedStatCounter;
|
||||
|
||||
/// The total number of frames that were interpolated using the smoothing algorithm
|
||||
int64_t _interpolationCounter;
|
||||
|
||||
/// The count of frames that have been interpolated when statistics were shown previously
|
||||
int64_t _interpolationStatCounter;
|
||||
|
||||
/// Frame weighting function for finding the frame's integral value
|
||||
///
|
||||
/// @param frameStart The start of frame time.
|
||||
/// @param frameEnd The end of frame time.
|
||||
/// @param windowStart The window start time.
|
||||
/// @returns The frame weight.
|
||||
std::function<floatT(int64_t, int64_t, int64_t)> _weightFrame;
|
||||
};
|
||||
|
@ -14,11 +14,10 @@
|
||||
{
|
||||
"type" : "string",
|
||||
"title" : "edt_conf_smooth_type_title",
|
||||
"enum" : ["linear"],
|
||||
"enum" : ["linear", "decay"],
|
||||
"default" : "linear",
|
||||
"options" : {
|
||||
"enum_titles" : ["edt_conf_enum_linear"],
|
||||
"hidden":true
|
||||
"enum_titles" : ["edt_conf_enum_linear", "edt_conf_enum_decay"]
|
||||
},
|
||||
"propertyOrder" : 2
|
||||
},
|
||||
@ -27,7 +26,7 @@
|
||||
"type" : "integer",
|
||||
"title" : "edt_conf_smooth_time_ms_title",
|
||||
"minimum" : 25,
|
||||
"maximum": 600,
|
||||
"maximum": 5000,
|
||||
"default" : 200,
|
||||
"append" : "edt_append_ms",
|
||||
"propertyOrder" : 3
|
||||
@ -37,11 +36,47 @@
|
||||
"type" : "number",
|
||||
"title" : "edt_conf_smooth_updateFrequency_title",
|
||||
"minimum" : 1.0,
|
||||
"maximum" : 100.0,
|
||||
"maximum" : 2000.0,
|
||||
"default" : 25.0,
|
||||
"append" : "edt_append_hz",
|
||||
"propertyOrder" : 4
|
||||
},
|
||||
"interpolationRate" :
|
||||
{
|
||||
"type" : "number",
|
||||
"title" : "edt_conf_smooth_interpolationRate_title",
|
||||
"minimum" : 1.0,
|
||||
"maximum": 1000.0,
|
||||
"default" : 0,
|
||||
"append" : "edt_append_hz",
|
||||
"propertyOrder" : 5
|
||||
},
|
||||
"outputRate" :
|
||||
{
|
||||
"type" : "number",
|
||||
"title" : "edt_conf_smooth_outputRate_title",
|
||||
"minimum" : 1.0,
|
||||
"maximum": 1000.0,
|
||||
"default" : 0,
|
||||
"append" : "edt_append_hz",
|
||||
"propertyOrder" : 6
|
||||
},
|
||||
"decay" :
|
||||
{
|
||||
"type" : "number",
|
||||
"title" : "edt_conf_smooth_decay_title",
|
||||
"default" : 1.0,
|
||||
"minimum" : 1.0,
|
||||
"maximum": 20.0,
|
||||
"propertyOrder" : 7
|
||||
},
|
||||
"dithering" :
|
||||
{
|
||||
"type" : "boolean",
|
||||
"title" : "edt_conf_smooth_dithering_title",
|
||||
"default" : true,
|
||||
"propertyOrder" : 8
|
||||
},
|
||||
"updateDelay" :
|
||||
{
|
||||
"type" : "integer",
|
||||
@ -50,14 +85,14 @@
|
||||
"maximum": 2048,
|
||||
"default" : 0,
|
||||
"append" : "edt_append_ms",
|
||||
"propertyOrder" : 5
|
||||
"propertyOrder" : 9
|
||||
},
|
||||
"continuousOutput" :
|
||||
{
|
||||
"type" : "boolean",
|
||||
"title" : "edt_conf_smooth_continuousOutput_title",
|
||||
"default" : true,
|
||||
"propertyOrder" : 6
|
||||
"propertyOrder" : 10
|
||||
}
|
||||
},
|
||||
"additionalProperties" : false
|
||||
|
Loading…
Reference in New Issue
Block a user