Switch to QImage scaling when downscaling as QPainter is awful#1214
Switch to QImage scaling when downscaling as QPainter is awful#1214j-b-m wants to merge 2 commits intomltframework:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adjusts Qt blending downscaling to avoid poor-quality QPainter scaling (Qt6 regression), by pre-scaling via QImage when reducing image size.
Changes:
- Detects rescale quality preference via
consumer.rescaleand toggles HQ painting accordingly. - Pre-scales the source
QImagewhen downscaling (instead of applying a QPainter scale transform). - Makes QPainter render hints conditional on the chosen interpolation mode and draws either the scaled or original image.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -219,7 +240,17 @@ static int filter_get_image(mlt_frame frame, | |||
| } | |||
| // Center image in rect | |||
| transform.translate((rect.w - (b_width * scale)) / 2.0, (rect.h - (b_height * scale)) / 2.0); | |||
| transform.scale(scale, scale); | |||
| if (scale != 1.) { | |||
| if (downscale) { | |||
| scaled = true; | |||
| scaledSource = sourceImage.scaled(sourceImage.size() * scale, | |||
There was a problem hiding this comment.
downscale is computed once as rect.w < b_width || rect.h < b_height, but in the non-distort branch the actual scale factor is influenced by b_ar (scale = rect.h / b_height * b_ar). That can produce scale < 1 even when rect.w >= b_width and rect.h >= b_height, causing the code to fall back to QPainter downscaling (the case this PR is trying to avoid). Consider computing the downscale decision from the computed scale (or the final target QSize) inside each branch instead of using rect vs b_* upfront.
| scaledSource = sourceImage.scaled(rect.w, | ||
| rect.h, | ||
| Qt::IgnoreAspectRatio, | ||
| hqPainting ? Qt::SmoothTransformation | ||
| : Qt::FastTransformation); |
There was a problem hiding this comment.
QImage::scaled() takes integer dimensions; here rect.w/rect.h are doubles and will be implicitly truncated when passed as width/height. If rect is fractional or very small (< 1px), this can introduce 1px jitter or even produce a null (0x0) scaled image. Consider explicitly rounding (e.g., qRound) and clamping to a minimum of 1 for the target size before calling scaled().
| transform.translate((rect.w - (b_width * scale)) / 2.0, (rect.h - (b_height * scale)) / 2.0); | ||
| transform.scale(scale, scale); | ||
| if (scale != 1.) { | ||
| if (downscale) { | ||
| scaled = true; | ||
| scaledSource = sourceImage.scaled(sourceImage.size() * scale, | ||
| Qt::IgnoreAspectRatio, | ||
| hqPainting ? Qt::SmoothTransformation | ||
| : Qt::FastTransformation); |
There was a problem hiding this comment.
In the non-distort downscale path, scaledSource is created with sourceImage.size() * scale (integer-rounded internally), but the centering translation uses (rect.w - (b_width * scale)) / 2.0 and (rect.h - (b_height * scale)) / 2.0 based on the unrounded float math. This can lead to off-by-one centering/cropping. Consider basing the translation on scaledSource.width()/height() (or compute a single rounded target size used for both scaling and translation).
bmatherly
left a comment
There was a problem hiding this comment.
The artifacts kind of look like the operation is not working on pixels that have some transparency (around the border of the logos). I did not recreate the problem myself. Does it only happen on images with transparency? Or does it still occur if the image background is fully opaque? I'm just wondering if there is some parameter we need to pass to the painter to make sure it handles transparency (alpha) correctly.
|
|
||
| char *interps = mlt_properties_get(frame_properties, "consumer.rescale"); | ||
| if (interps) { | ||
| interps = strdup(interps); |
There was a problem hiding this comment.
Can you explain why you copy the string? I think you can use it directly.
There was a problem hiding this comment.
Using strdup on this string property is a common thing in our source code that dates way back. The property getter returns a pointer, and after the call the string it points to can be changed including NULLed, leading to comparison over-reads or crashes. I think this is more of an issue for a string property on a service getting changed by an application, but these are checking a frame property set by mlt_consumer that is not going to be mutated by another thread.
This technique goes back to a bug fix in 9a19d7b. The bug report is not available, and there are other fixes mixed in with that commit. So, hard to say what was the actual fix. Maybe this was the ultimate fix, and something later actually did.
There was a problem hiding this comment.
This technique goes back to a bug fix in 9a19d7b
Thanks for the reference. I suppose the author (or the AI they used) just followed other examples for interp.
The property getter returns a pointer, and after the call the string it points to can be changed including NULLed, leading to comparison over-reads or crashes.
While that is a true statement, this strdup() trick doesn't fix that because the pointer could still be changed after the get() call and before the strdup call. So this trick only reduces the probability. If data being changed is a concern, then the service should be locked.
I would prefer that we not propagate this unnecessary strdup and start setting a good example for future authors. But I can appreciate that it is following an established convention.
There was a problem hiding this comment.
Yes, I just copied code used in the qtblend transition that was initially based on the affine transition where this is still used. But sure, I will clean that up
Looking at the images attached here, even the lines inside the opaque areas have very bad aliasing suggesting no interpolation. |
|
JB, are the images you provided scaled-up for emphasis? In the docs, both QPainter and QImage say the hints provided both do bilinear interpolation: However, Claude suggested this difference: The Qt docs description is misleading/simplified. Despite the "bilinear filtering" wording, Qt's internal implementation of The meaningful difference is:
For a 4× downscale, bilinear reads 4 out of ~16 contributing source pixels; the area-average reads all ~16. The Qt docs use "bilinear" as a loose synonym for "smooth/filtered" vs "nearest-neighbor" — not as a precise description of the algorithm. So the fix is correct: pre-scaling via |
Oh yeah. I see what you mean. |

Seems like a Qt6 regression, but QPainter scaling gives terrible results when downscaling. So in such cases, scale using QImage before painting. Kdenlive issue 2123
Comparison of results (updated QImage scaling on top)
