Skip to content

Commit c188cb7

Browse files
tpmoddingclaude
andcommitted
feat: add aspect ratio black-border correction for screen grabbers (hyperion-project#1301)
Adds a configurable "aspectRatio" option to all screen grabbers that pads captured frames with black pillarboxes before LED mapping, so 4:3 content on 16:9 screens (or 16:9 on 21:9) is not incorrectly stretched across the full LED ring. Implemented as a zero-cost passthrough when disabled (the default). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 71c9d3a commit c188cb7

4 files changed

Lines changed: 67 additions & 0 deletions

File tree

assets/webconfig/i18n/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,9 @@
438438
"edt_conf_flatbufServer_heading_title": "Flatbuffer Server",
439439
"edt_conf_flatbufServer_timeout_expl": "If no data is received for the given period, the component will be (soft) disabled.",
440440
"edt_conf_flatbufServer_timeout_title": "Timeout",
441+
"edt_conf_fg_aspectRatio_title": "Aspect Ratio Correction",
442+
"edt_conf_fg_aspectRatio_4_3_to_16_9": "4:3 → 16:9 (add pillarboxes)",
443+
"edt_conf_fg_aspectRatio_16_9_to_21_9": "16:9 → 21:9 (add pillarboxes)",
441444
"edt_conf_fg_display_expl": "Select which desktop should be captured (multi monitor setup)",
442445
"edt_conf_fg_display_title": "Display",
443446
"edt_conf_fg_frequency_Hz_expl": "How fast new pictures are captured, i.e. it is the sampling rate. Note: The video might be played at a higher or lower frame rate.",

include/hyperion/GrabberWrapper.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class GrabberWrapper : public QObject
122122
int ret = grabber.grabFrame(_image);
123123
if (ret >= 0)
124124
{
125+
applyAspectRatio();
125126
emit systemImage(_grabberName, _image);
126127
return true;
127128
}
@@ -205,6 +206,8 @@ private slots:
205206
void handleSourceRequestVideo(hyperion::Components component, int hyperionInd, bool listen);
206207
void handleSourceRequestAudio(hyperion::Components component, int hyperionInd, bool listen);
207208

209+
void applyAspectRatio();
210+
208211
Grabber *_ggrabber;
209212
QString _grabberName;
210213

@@ -216,4 +219,10 @@ private slots:
216219

217220
/// The image used for grabbing frames
218221
Image<ColorRgb> _image;
222+
223+
/// Reusable buffer for aspect ratio padding
224+
Image<ColorRgb> _paddedImage;
225+
226+
/// Target aspect ratio for black-border padding (0 = disabled)
227+
float _targetAspectRatio = 0.0f;
219228
};

libsrc/hyperion/GrabberWrapper.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
#include <hyperion/Grabber.h>
44
#include <HyperionConfig.h>
55

6+
// std
7+
#include <cstring>
8+
69
// utils includes
710
#include <utils/GlobalSignals.h>
811
#include <events/EventHandler.h>
@@ -263,6 +266,34 @@ void GrabberWrapper::setCropping(int cropLeft, int cropRight, int cropTop, int c
263266
_ggrabber->setCropping(cropLeft, cropRight, cropTop, cropBottom);
264267
}
265268

269+
void GrabberWrapper::applyAspectRatio()
270+
{
271+
if (_targetAspectRatio <= 0.0f)
272+
return;
273+
274+
const int srcW = _image.width();
275+
const int srcH = _image.height();
276+
// Round to nearest even number to keep stride well-aligned
277+
const int dstW = (static_cast<int>(srcH * _targetAspectRatio) + 1) & ~1;
278+
279+
if (dstW <= srcW)
280+
return;
281+
282+
if (_paddedImage.width() != dstW || _paddedImage.height() != srcH)
283+
_paddedImage.resize(dstW, srcH);
284+
285+
_paddedImage.clear(ColorRgb::BLACK);
286+
287+
const int offsetX = (dstW - srcW) / 2;
288+
const ColorRgb* src = _image.memptr();
289+
ColorRgb* dst = _paddedImage.memptr();
290+
291+
for (int y = 0; y < srcH; ++y)
292+
memcpy(dst + y * dstW + offsetX, src + y * srcW, static_cast<size_t>(srcW) * sizeof(ColorRgb));
293+
294+
_image.swap(_paddedImage);
295+
}
296+
266297
void GrabberWrapper::updateTimer(int interval)
267298
{
268299
if(_updateInterval_ms != interval)
@@ -316,6 +347,16 @@ void GrabberWrapper::handleSettingsUpdate(settings::type type, const QJsonDocume
316347
obj["cropBottom"].toInt(0));
317348

318349
_ggrabber->setFramerate(obj["fps"].toInt(DEFAULT_RATE_HZ));
350+
351+
// aspect ratio padding
352+
const QString ar = obj["aspectRatio"].toString("disabled");
353+
if (ar == "4:3-to-16:9")
354+
_targetAspectRatio = 16.0f / 9.0f;
355+
else if (ar == "16:9-to-21:9")
356+
_targetAspectRatio = 21.0f / 9.0f;
357+
else
358+
_targetAspectRatio = 0.0f;
359+
319360
// eval new update time
320361
updateTimer(_ggrabber->getUpdateInterval());
321362

libsrc/hyperion/schema/schema-framegrabber.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@
137137
"default": 0,
138138
"append": "edt_append_pixel",
139139
"propertyOrder": 17
140+
},
141+
"aspectRatio": {
142+
"type": "string",
143+
"title": "edt_conf_fg_aspectRatio_title",
144+
"enum": [ "disabled", "4:3-to-16:9", "16:9-to-21:9" ],
145+
"options": {
146+
"enum_titles": [
147+
"edt_conf_enum_disabled",
148+
"edt_conf_fg_aspectRatio_4_3_to_16_9",
149+
"edt_conf_fg_aspectRatio_16_9_to_21_9"
150+
]
151+
},
152+
"default": "disabled",
153+
"propertyOrder": 18
140154
}
141155
},
142156
"additionalProperties" : false

0 commit comments

Comments
 (0)