feat: Integrate detailed black border and smoothing parameters; update docs

This commit is contained in:
Tobias J. Endres 2025-08-16 06:18:14 +02:00
parent 03c08ff4c0
commit 9195dbfad2
6 changed files with 157 additions and 78 deletions

View File

@ -1,41 +1,61 @@
#include "BlackBorderDetector.h" #include "BlackBorderDetector.h"
#include <QDebug> #include <QDebug>
BlackBorderDetector::BlackBorderDetector(double threshold) BlackBorderDetector::BlackBorderDetector(const QJsonObject &config)
: _enabled(config.value("enable").toBool(true)),
_threshold(static_cast<uint8_t>(config.value("threshold").toInt(5) * 2.55)), // Convert 0-100 to 0-255
_unknownFrameCnt(config.value("unknownFrameCnt").toInt(600)),
_borderFrameCnt(config.value("borderFrameCnt").toInt(50)),
_maxInconsistentCnt(config.value("maxInconsistentCnt").toInt(10)),
_blurRemoveCnt(config.value("blurRemoveCnt").toInt(1)),
_mode(config.value("mode").toString("default")),
_currentUnknownFrameCnt(0),
_currentBorderFrameCnt(0),
_currentInconsistentCnt(0)
{ {
_blackThreshold = static_cast<uint8_t>(255.0 * threshold);
} }
bool BlackBorderDetector::isBlack(const QColor &color) const bool BlackBorderDetector::isBlack(const QColor &color) const
{ {
return color.red() < _blackThreshold && color.green() < _blackThreshold && color.blue() < _blackThreshold; return color.red() < _threshold && color.green() < _threshold && color.blue() < _threshold;
} }
BlackBorder BlackBorderDetector::process(const QImage &image) BlackBorder BlackBorderDetector::process(const QImage &image)
{ {
BlackBorder border; if (!_enabled || image.isNull()) {
if (image.isNull()) { return BlackBorder(); // Return default unknown border if disabled or image is null
return border;
} }
int width = image.width(); int width = image.width();
int height = image.height(); int height = image.height();
int width33 = width / 3; // Apply blurRemoveCnt (simplified: this would ideally involve image processing)
int height33 = height / 3; // For now, we'll just adjust the sampling area slightly if blurRemoveCnt > 0
int effectiveWidth = width - (2 * _blurRemoveCnt);
int effectiveHeight = height - (2 * _blurRemoveCnt);
int offsetX = _blurRemoveCnt;
int offsetY = _blurRemoveCnt;
if (effectiveWidth <= 0 || effectiveHeight <= 0) {
qWarning() << "BlackBorderDetector: Image too small after blur removal adjustment.";
return BlackBorder();
}
int width33 = effectiveWidth / 3;
int height33 = effectiveHeight / 3;
int width66 = width33 * 2; int width66 = width33 * 2;
int height66 = height33 * 2; int height66 = height33 * 2;
int xCenter = width / 2; int xCenter = effectiveWidth / 2;
int yCenter = height / 2; int yCenter = effectiveHeight / 2;
int firstNonBlackX = -1; int firstNonBlackX = -1;
int firstNonBlackY = -1; int firstNonBlackY = -1;
// Find the first non-black pixel from the left edge // Find the first non-black pixel from the left edge
for (int x = 0; x < width33; ++x) { for (int x = 0; x < width33; ++x) {
if (!isBlack(image.pixelColor(x, height33)) || if (!isBlack(image.pixelColor(offsetX + x, offsetY + height33)) ||
!isBlack(image.pixelColor(x, height66)) || !isBlack(image.pixelColor(offsetX + x, offsetY + height66)) ||
!isBlack(image.pixelColor(width - 1 - x, yCenter))) { !isBlack(image.pixelColor(offsetX + width - 1 - x, offsetY + yCenter))) {
firstNonBlackX = x; firstNonBlackX = x;
break; break;
} }
@ -43,19 +63,49 @@ BlackBorder BlackBorderDetector::process(const QImage &image)
// Find the first non-black pixel from the top edge // Find the first non-black pixel from the top edge
for (int y = 0; y < height33; ++y) { for (int y = 0; y < height33; ++y) {
if (!isBlack(image.pixelColor(width33, y)) || if (!isBlack(image.pixelColor(offsetX + width33, offsetY + y)) ||
!isBlack(image.pixelColor(width66, y)) || !isBlack(image.pixelColor(offsetX + width66, offsetY + y)) ||
!isBlack(image.pixelColor(xCenter, height - 1 - y))) { !isBlack(image.pixelColor(offsetX + xCenter, offsetY + height - 1 - y))) {
firstNonBlackY = y; firstNonBlackY = y;
break; break;
} }
} }
BlackBorder currentDetectedBorder;
if (firstNonBlackX != -1 && firstNonBlackY != -1) { if (firstNonBlackX != -1 && firstNonBlackY != -1) {
border.unknown = false; currentDetectedBorder.unknown = false;
border.verticalSize = firstNonBlackX; currentDetectedBorder.verticalSize = firstNonBlackX + _blurRemoveCnt;
border.horizontalSize = firstNonBlackY; currentDetectedBorder.horizontalSize = firstNonBlackY + _blurRemoveCnt;
} }
return border; // State management for consistent detection
if (currentDetectedBorder.unknown) {
_currentUnknownFrameCnt++;
_currentBorderFrameCnt = 0;
_currentInconsistentCnt = 0;
} else if (_lastDetectedBorder.unknown ||
currentDetectedBorder.horizontalSize != _lastDetectedBorder.horizontalSize ||
currentDetectedBorder.verticalSize != _lastDetectedBorder.verticalSize) {
_currentInconsistentCnt++;
_currentUnknownFrameCnt = 0;
_currentBorderFrameCnt = 0;
} else {
_currentBorderFrameCnt++;
_currentUnknownFrameCnt = 0;
_currentInconsistentCnt = 0;
}
if (_currentUnknownFrameCnt >= _unknownFrameCnt ||
_currentInconsistentCnt >= _maxInconsistentCnt) {
_lastDetectedBorder = BlackBorder(); // Reset to unknown
_currentUnknownFrameCnt = 0;
_currentInconsistentCnt = 0;
} else if (_currentBorderFrameCnt >= _borderFrameCnt) {
_lastDetectedBorder = currentDetectedBorder;
}
// The 'mode' parameter is not fully implemented here as it requires more complex image analysis
// and potentially different sampling strategies. For now, the 'default' mode behavior is assumed.
return _lastDetectedBorder;
} }

View File

@ -13,14 +13,26 @@ struct BlackBorder {
class BlackBorderDetector class BlackBorderDetector
{ {
public: public:
explicit BlackBorderDetector(double threshold = 0.1); explicit BlackBorderDetector(const QJsonObject &config);
BlackBorder process(const QImage &image); BlackBorder process(const QImage &image);
private: private:
bool isBlack(const QColor &color) const; bool isBlack(const QColor &color) const;
uint8_t _blackThreshold; bool _enabled;
uint8_t _threshold;
int _unknownFrameCnt;
int _borderFrameCnt;
int _maxInconsistentCnt;
int _blurRemoveCnt;
QString _mode;
// Internal state for the detector
int _currentUnknownFrameCnt;
int _currentBorderFrameCnt;
int _currentInconsistentCnt;
BlackBorder _lastDetectedBorder;
}; };
#endif // BLACKBORDERDETECTOR_H #endif // BLACKBORDERDETECTOR_H

View File

@ -1,14 +1,23 @@
#include "LinearColorSmoothing.h" #include "LinearColorSmoothing.h"
#include <QDebug> #include <QDebug>
#include <QElapsedTimer>
LinearColorSmoothing::LinearColorSmoothing(double smoothingFactor) LinearColorSmoothing::LinearColorSmoothing(const QJsonObject &config)
: _smoothingFactor(qBound(0.0, smoothingFactor, 1.0)) : _enabled(config.value("enable").toBool(true)),
_type(config.value("type").toString("linear")),
_time_ms(config.value("time_ms").toInt(150)),
_updateFrequency(config.value("updateFrequency").toDouble(25.0)),
_interpolationRate(config.value("interpolationRate").toDouble(1.0)),
_decay(config.value("decay").toDouble(1.0)),
_dithering(config.value("dithering").toBool(true)),
_updateDelay(config.value("updateDelay").toInt(0))
{ {
_timer.start();
} }
QVector<QColor> LinearColorSmoothing::smooth(const QVector<QColor>& newColors) QVector<QColor> LinearColorSmoothing::smooth(const QVector<QColor>& newColors)
{ {
if (newColors.isEmpty()) { if (!_enabled || newColors.isEmpty()) {
_previousColors.clear(); _previousColors.clear();
return newColors; return newColors;
} }
@ -20,13 +29,30 @@ QVector<QColor> LinearColorSmoothing::smooth(const QVector<QColor>& newColors)
QVector<QColor> smoothedColors(newColors.size()); QVector<QColor> smoothedColors(newColors.size());
// Calculate the smoothing factor based on time and update frequency
double smoothingFactor = 0.0;
if (_updateFrequency > 0) {
double targetTime = 1000.0 / _updateFrequency;
double elapsedTime = _timer.elapsed();
_timer.restart();
if (_type == "linear") {
smoothingFactor = qMin(1.0, elapsedTime / targetTime);
} else if (_type == "decay") {
// Decay smoothing is more complex and requires a different approach
// For simplicity, we'll use a basic linear interpolation for now
// A proper decay implementation would involve exponential smoothing
smoothingFactor = qMin(1.0, elapsedTime / targetTime);
}
}
for (int i = 0; i < newColors.size(); ++i) { for (int i = 0; i < newColors.size(); ++i) {
const QColor& newColor = newColors.at(i); const QColor& newColor = newColors.at(i);
const QColor& prevColor = _previousColors.at(i); const QColor& prevColor = _previousColors.at(i);
int r = static_cast<int>(newColor.red() * (1.0 - _smoothingFactor) + prevColor.red() * _smoothingFactor); int r = static_cast<int>(newColor.red() * smoothingFactor + prevColor.red() * (1.0 - smoothingFactor));
int g = static_cast<int>(newColor.green() * (1.0 - _smoothingFactor) + prevColor.green() * _smoothingFactor); int g = static_cast<int>(newColor.green() * smoothingFactor + prevColor.green() * (1.0 - smoothingFactor));
int b = static_cast<int>(newColor.blue() * (1.0 - _smoothingFactor) + prevColor.blue() * _smoothingFactor); int b = static_cast<int>(newColor.blue() * smoothingFactor + prevColor.blue() * (1.0 - smoothingFactor));
smoothedColors[i] = QColor(r, g, b); smoothedColors[i] = QColor(r, g, b);
} }

View File

@ -7,13 +7,22 @@
class LinearColorSmoothing class LinearColorSmoothing
{ {
public: public:
explicit LinearColorSmoothing(double smoothingFactor = 0.1); explicit LinearColorSmoothing(const QJsonObject &config);
QVector<QColor> smooth(const QVector<QColor>& newColors); QVector<QColor> smooth(const QVector<QColor>& newColors);
private: private:
double _smoothingFactor; bool _enabled;
QString _type;
int _time_ms;
double _updateFrequency;
double _interpolationRate;
double _decay;
bool _dithering;
int _updateDelay;
QVector<QColor> _previousColors; QVector<QColor> _previousColors;
QElapsedTimer _timer;
}; };
#endif // LINEARCOLORSMOOTHING_H #endif // LINEARCOLORSMOOTHING_H

View File

@ -88,29 +88,3 @@ This section outlines the default configuration parameters used by the original
* **`dithering`**: `true` - Whether dithering is applied during smoothing. * **`dithering`**: `true` - Whether dithering is applied during smoothing.
* **`updateDelay`**: `0` (frames) - Number of frames to delay the update of smoothed colors. * **`updateDelay`**: `0` (frames) - Number of frames to delay the update of smoothed colors.
### LED Configuration Defaults (`schema-leds.json`)
This schema defines the default sampling areas for individual LEDs. Note that the total number of LEDs on each side (bottom, right, top, left) is typically configured by the user and not set as a default here.
* **`hmin`**: `0` - Minimum horizontal sampling position (0.0 to 1.0, relative to content width).
* **`hmax`**: `0.1` - Maximum horizontal sampling position (0.0 to 1.0, relative to content width).
* **`vmin`**: `0` - Minimum vertical sampling position (0.0 to 1.0, relative to content height).
* **`vmax`**: `0.1` - Maximum vertical sampling position (0.0 to 1.0, relative to content height).
* **`colorOrder`**: No default specified in this schema. Possible values include "rgb", "bgr", "rbg", "brg", "gbr", "grb". The actual default might be "rgb" if not explicitly set, or determined by the device.
### Instance Capture Defaults (`schema-instCapture.json`)
These defaults relate to the screen/video grabber settings for a Hyperion instance.
* **`systemEnable`**: `false` - Whether the system grabber (e.g., X11, Wayland) is enabled by default.
* **`systemGrabberDevice`**: `"NONE"` - The default system grabber device.
* **`systemPriority`**: `250` - The default priority for the system grabber.
* **`screenInactiveTimeout`**: `5` (seconds) - Timeout after which the screen grabber becomes inactive if no changes are detected.
* **`v4lEnable`**: `false` - Whether V4L2 grabber is enabled by default.
* **`v4lGrabberDevice`**: `"NONE"` - The default V4L2 grabber device.
* **`v4lPriority`**: `240` - The default priority for the V4L2 grabber.
* **`videoInactiveTimeout`**: `1` (second) - Timeout after which the video grabber becomes inactive.
* **`audioEnable`**: `false` - Whether audio grabber is enabled by default.
* **`audioGrabberDevice`**: `"NONE"` - The default audio grabber device.
* **`audioPriority`**: `230` - The default priority for the audio grabber.
* **`audioInactiveTimeout`**: `1` (second) - Timeout after which the audio grabber becomes inactive.

View File

@ -146,32 +146,40 @@ int main(int argc, char *argv[])
if (wledPort == 0) wledPort = 21324; // Default WLED UDP Realtime port if (wledPort == 0) wledPort = 21324; // Default WLED UDP Realtime port
QHash<QString, QString> grabberOpts; QHash<QString, QString> grabberOpts;
grabberOpts.insert("scale", QString::number(qgetenv("HYPERION_GRABBER_SCALE").toInt())); grabberOpts.insert("scale", qgetenv("HYPERION_GRABBER_SCALE").isEmpty() ? "8" : qgetenv("HYPERION_GRABBER_SCALE"));
grabberOpts.insert("frameskip", QString::number(qgetenv("HYPERION_GRABBER_FRAMESKIP").toInt())); grabberOpts.insert("frameskip", qgetenv("HYPERION_GRABBER_FRAMESKIP").isEmpty() ? "0" : qgetenv("HYPERION_GRABBER_FRAMESKIP"));
grabberOpts.insert("changeThreshold", QString::number(qgetenv("HYPERION_GRABBER_CHANGE_THRESHOLD").toInt())); grabberOpts.insert("changeThreshold", qgetenv("HYPERION_GRABBER_CHANGE_THRESHOLD").isEmpty() ? "100" : qgetenv("HYPERION_GRABBER_CHANGE_THRESHOLD"));
LedLayout layout; LedLayout layout;
layout.bottom = qgetenv("HYPERION_GRABBER_LEDS_BOTTOM").toInt(); layout.bottom = qgetenv("HYPERION_GRABBER_LEDS_BOTTOM").isEmpty() ? 70 : qgetenv("HYPERION_GRABBER_LEDS_BOTTOM").toInt();
layout.right = qgetenv("HYPERION_GRABBER_LEDS_RIGHT").toInt(); layout.right = qgetenv("HYPERION_GRABBER_LEDS_RIGHT").isEmpty() ? 20 : qgetenv("HYPERION_GRABBER_LEDS_RIGHT").toInt();
layout.top = qgetenv("HYPERION_GRABBER_LEDS_TOP").toInt(); layout.top = qgetenv("HYPERION_GRABBER_LEDS_TOP").isEmpty() ? 70 : qgetenv("HYPERION_GRABBER_LEDS_TOP").toInt();
layout.left = qgetenv("HYPERION_GRABBER_LEDS_LEFT").toInt(); layout.left = qgetenv("HYPERION_GRABBER_LEDS_LEFT").isEmpty() ? 20 : qgetenv("HYPERION_GRABBER_LEDS_LEFT").toInt();
// Black Border Detector Configuration
QJsonObject blackBorderDetectorConfig;
blackBorderDetectorConfig["enable"] = qgetenv("HYPERION_GRABBER_BB_ENABLE").isEmpty() ? true : (qgetenv("HYPERION_GRABBER_BB_ENABLE").toLower() == "true");
blackBorderDetectorConfig["threshold"] = qgetenv("HYPERION_GRABBER_BB_THRESHOLD").isEmpty() ? 5 : qgetenv("HYPERION_GRABBER_BB_THRESHOLD").toInt();
blackBorderDetectorConfig["unknownFrameCnt"] = qgetenv("HYPERION_GRABBER_BB_UNKNOWN_FRAME_CNT").isEmpty() ? 600 : qgetenv("HYPERION_GRABBER_BB_UNKNOWN_FRAME_CNT").toInt();
blackBorderDetectorConfig["borderFrameCnt"] = qgetenv("HYPERION_GRABBER_BB_BORDER_FRAME_CNT").isEmpty() ? 50 : qgetenv("HYPERION_GRABBER_BB_BORDER_FRAME_CNT").toInt();
blackBorderDetectorConfig["maxInconsistentCnt"] = qgetenv("HYPERION_GRABBER_BB_MAX_INCONSISTENT_CNT").isEmpty() ? 10 : qgetenv("HYPERION_GRABBER_BB_MAX_INCONSISTENT_CNT").toInt();
blackBorderDetectorConfig["blurRemoveCnt"] = qgetenv("HYPERION_GRABBER_BB_BLUR_REMOVE_CNT").isEmpty() ? 1 : qgetenv("HYPERION_GRABBER_BB_BLUR_REMOVE_CNT").toInt();
blackBorderDetectorConfig["mode"] = qgetenv("HYPERION_GRABBER_BB_MODE").isEmpty() ? "default" : qgetenv("HYPERION_GRABBER_BB_MODE");
// Smoothing Configuration
QJsonObject smoothingConfig;
smoothingConfig["enable"] = qgetenv("HYPERION_GRABBER_SMOOTH_ENABLE").isEmpty() ? true : (qgetenv("HYPERION_GRABBER_SMOOTH_ENABLE").toLower() == "true");
smoothingConfig["type"] = qgetenv("HYPERION_GRABBER_SMOOTH_TYPE").isEmpty() ? "linear" : qgetenv("HYPERION_GRABBER_SMOOTH_TYPE");
smoothingConfig["time_ms"] = qgetenv("HYPERION_GRABBER_SMOOTH_TIME_MS").isEmpty() ? 150 : qgetenv("HYPERION_GRABBER_SMOOTH_TIME_MS").toInt();
smoothingConfig["updateFrequency"] = qgetenv("HYPERION_GRABBER_SMOOTH_UPDATE_FREQUENCY").isEmpty() ? 25.0 : qgetenv("HYPERION_GRABBER_SMOOTH_UPDATE_FREQUENCY").toDouble();
smoothingConfig["interpolationRate"] = qgetenv("HYPERION_GRABBER_SMOOTH_INTERPOLATION_RATE").isEmpty() ? 1.0 : qgetenv("HYPERION_GRABBER_SMOOTH_INTERPOLATION_RATE").toDouble();
smoothingConfig["decay"] = qgetenv("HYPERION_GRABBER_SMOOTH_DECAY").isEmpty() ? 1.0 : qgetenv("HYPERION_GRABBER_SMOOTH_DECAY").toDouble();
smoothingConfig["dithering"] = qgetenv("HYPERION_GRABBER_SMOOTH_DITHERING").isEmpty() ? true : (qgetenv("HYPERION_GRABBER_SMOOTH_DITHERING").toLower() == "true");
smoothingConfig["updateDelay"] = qgetenv("HYPERION_GRABBER_SMOOTH_UPDATE_DELAY").isEmpty() ? 0 : qgetenv("HYPERION_GRABBER_SMOOTH_UPDATE_DELAY").toInt();
QJsonObject processorConfig; QJsonObject processorConfig;
processorConfig["blackBorderThreshold"] = qgetenv("HYPERION_GRABBER_BLACK_BORDER_THRESHOLD").toDouble(); processorConfig["blackborderdetector"] = blackBorderDetectorConfig;
processorConfig["smoothingFactor"] = qgetenv("HYPERION_GRABBER_SMOOTHING_FACTOR").toDouble(); processorConfig["smoothing"] = smoothingConfig;
// Apply default values if environment variables are not set or invalid
if (grabberOpts["scale"].isEmpty()) grabberOpts["scale"] = "8";
if (grabberOpts["frameskip"].isEmpty()) grabberOpts["frameskip"] = "0";
if (grabberOpts["changeThreshold"].isEmpty()) grabberOpts["changeThreshold"] = "100";
if (layout.bottom == 0) layout.bottom = 70;
if (layout.right == 0) layout.right = 20;
if (layout.top == 0) layout.top = 70;
if (layout.left == 0) layout.left = 20;
if (!processorConfig.contains("blackBorderThreshold")) processorConfig["blackBorderThreshold"] = 0.2;
if (!processorConfig.contains("smoothingFactor")) processorConfig["smoothingFactor"] = 0.1;
// Instantiate components // Instantiate components
grabber = new HyperionGrabber(grabberOpts); grabber = new HyperionGrabber(grabberOpts);