Added options to the miximage generator for how to fit screenshots that do not match the aspect ratio of the miximage frame

This commit is contained in:
Leon Styhre 2023-07-01 11:15:43 +02:00
parent bf106711cb
commit 923240aac0
4 changed files with 234 additions and 64 deletions

View file

@ -409,16 +409,100 @@ bool MiximageGenerator::generateImage()
if (Settings::getInstance()->getBool("MiximageRemovePillarboxes"))
Utils::CImg::cropPillarboxes(screenshotImage);
if (Settings::getInstance()->getString("MiximageScreenshotScaling") == "smooth") {
// Lanczos scaling is normally not recommended for low resolution graphics as
// it makes the pixels appear smooth when scaling, but for more modern game
// platforms it may be a good idea to use it.
screenshotImage.resize(screenshotWidth, screenshotHeight, 1, 4, 6);
const float fileAspect {static_cast<float>(fileWidth) / static_cast<float>(fileHeight)};
// Options for how to handle screenshots that are not closely matching the aspect ratio of
// the miximage. Can be set to contain, crop or stretch.
const bool containImage {
(fileAspect >= 1.0f &&
Settings::getInstance()->getString("MiximageScreenshotHorizontalFit") == "contain") ||
(fileAspect < 1.0f &&
Settings::getInstance()->getString("MiximageScreenshotVerticalFit") == "contain")};
const bool cropImage {
(fileAspect >= 1.0f &&
Settings::getInstance()->getString("MiximageScreenshotHorizontalFit") == "crop") ||
(fileAspect < 1.0f &&
Settings::getInstance()->getString("MiximageScreenshotVerticalFit") == "crop")};
// Define a threshold higher and lower than the 1.325 aspect ratio of the miximage to avoid
// fitting and cropping images which only deviate slightly.
const float miximageAspectRatio {1.325f};
const float maxAspectValue {
Settings::getInstance()->getString("MiximageScreenshotAspectThreshold") == "high" ? 1.6f :
1.375f};
const float minAspectValue {
Settings::getInstance()->getString("MiximageScreenshotAspectThreshold") == "high" ? 1.05f :
1.275f};
// Set the frame color based on an average of the screenshot contents.
unsigned char frameColor[] = {0, 0, 0, 0};
// Lanczos scaling (6) is normally not recommended for low resolution graphics as it makes the
// the pixels appear smooth when scaling, but for more modern game platforms it may be a good
// idea to use it. Box interpolation (1) gives completely sharp pixels, which is best suited
// for low resolution retro games.
const int scalingMethod {
Settings::getInstance()->getString("MiximageScreenshotScaling") == "smooth" ? 6 : 1};
if (cropImage && (fileAspect > maxAspectValue || fileAspect < minAspectValue)) {
if (fileWidth >= fileHeight && fileAspect > miximageAspectRatio) {
screenshotImage.resize(
static_cast<int>(std::round(static_cast<float>(screenshotHeight) * fileAspect)),
screenshotHeight, 1, 4, scalingMethod);
const int offsetX {(screenshotImage.width() - static_cast<int>(screenshotWidth)) / 2};
// Add -2 to the offset to avoid single-pixel black lines caused by inexact rounding.
screenshotImage.crop(offsetX, 0, 0, 3, screenshotWidth + offsetX, screenshotHeight - 2,
0, 0);
}
else {
screenshotImage.resize(
screenshotWidth,
static_cast<int>(std::round(static_cast<float>(screenshotWidth) / fileAspect)), 1,
4, scalingMethod);
const int offsetY {(screenshotImage.height() - static_cast<int>(screenshotHeight)) / 2};
screenshotImage.crop(0, offsetY, 0, 3, screenshotWidth - 2, screenshotHeight + offsetY,
0, 0);
}
}
else {
// Box interpolation gives completely sharp pixels, which is best suited for
// low resolution retro games.
screenshotImage.resize(screenshotWidth, screenshotHeight, 1, 4, 1);
screenshotImage.resize(screenshotWidth, screenshotHeight, 1, 4, scalingMethod);
}
if (containImage && (fileAspect > maxAspectValue || fileAspect < minAspectValue)) {
CImg<unsigned char> tempImage(fileWidth, fileHeight, 1, 4, 0);
sampleFrameColor(screenshotImage, frameColor);
tempImage = screenshotImage;
if (Settings::getInstance()->getString("MiximageScreenshotBlankAreasColor") == "frame") {
screenshotImage.get_shared_channel(0).fill(frameColor[0]);
screenshotImage.get_shared_channel(1).fill(frameColor[1]);
screenshotImage.get_shared_channel(2).fill(frameColor[2]);
}
else {
screenshotImage.fill(0);
}
if (fileWidth >= fileHeight && fileAspect > miximageAspectRatio) {
const float sizeRatio {static_cast<float>(screenshotWidth) /
static_cast<float>(fileWidth)};
const int resizeHeight {static_cast<int>(static_cast<float>(fileHeight) * sizeRatio)};
tempImage.resize(
screenshotWidth,
static_cast<int>(std::round(static_cast<float>(fileHeight) * sizeRatio)), 1, 4,
scalingMethod);
screenshotImage.draw_image(0, (screenshotHeight - resizeHeight) / 2, tempImage);
}
else {
const float sizeRatio {static_cast<float>(screenshotHeight) /
static_cast<float>(fileHeight)};
const int resizeWidth {static_cast<int>(static_cast<float>(fileWidth) * sizeRatio)};
tempImage.resize(
static_cast<int>(std::round(static_cast<float>(fileWidth) * sizeRatio)),
screenshotHeight, 1, 4, scalingMethod);
screenshotImage.draw_image((screenshotWidth - resizeWidth) / 2, 0, tempImage);
}
}
else {
sampleFrameColor(screenshotImage, frameColor);
}
// Remove any transparency information from the screenshot. There really should be no
@ -454,8 +538,8 @@ bool MiximageGenerator::generateImage()
CImg<unsigned char> frameImage(mWidth, mHeight, 1, 4, 0);
xPosScreenshot = canvasImage.width() / 2 - screenshotImage.width() / 2 + screenshotOffset;
yPosScreenshot = canvasImage.height() / 2 - screenshotImage.height() / 2;
xPosScreenshot = canvasImage.width() / 2 - screenshotWidth / 2 + screenshotOffset;
yPosScreenshot = canvasImage.height() / 2 - screenshotHeight / 2;
if (mMarquee) {
if (FreeImage_GetBPP(marqueeFile) != 32) {
@ -617,10 +701,6 @@ bool MiximageGenerator::generateImage()
frameImageAlpha.draw_image(xPosPhysicalMedia, yPosPhysicalMedia, physicalMediaImageAlpha);
frameImageAlpha.draw_image(xPosMarquee, yPosMarquee, marqueeImageAlpha);
// Set a frame color based on an average of the screenshot contents.
unsigned char frameColor[] = {0, 0, 0, 0};
sampleFrameColor(screenshotImage, frameColor);
// Upper / lower frame.
frameImage.draw_rectangle(xPosScreenshot + 2, yPosScreenshot - screenshotFrameWidth,
xPosScreenshot + screenshotWidth - 2,

View file

@ -468,6 +468,92 @@ void GuiScraperMenu::openMiximageOptions()
}
});
// Horizontally oriented screenshots fit.
auto miximageHorizontalFit = std::make_shared<OptionListComponent<std::string>>(
getHelpStyle(), "HORIZONTAL SCREENSHOT FIT", false);
const std::string selectedHorizontalFit {
Settings::getInstance()->getString("MiximageScreenshotHorizontalFit")};
miximageHorizontalFit->add("contain", "contain", selectedHorizontalFit == "contain");
miximageHorizontalFit->add("crop", "crop", selectedHorizontalFit == "crop");
miximageHorizontalFit->add("stretch", "stretch", selectedHorizontalFit == "stretch");
// If there are no objects returned, then there must be a manually modified entry in the
// configuration file. Simply set the horizontal screenshot fit to "crop" in this case.
if (miximageHorizontalFit->getSelectedObjects().size() == 0)
miximageHorizontalFit->selectEntry(1);
s->addWithLabel("HORIZONTAL SCREENSHOT FIT", miximageHorizontalFit);
s->addSaveFunc([miximageHorizontalFit, s] {
if (miximageHorizontalFit->getSelected() !=
Settings::getInstance()->getString("MiximageScreenshotHorizontalFit")) {
Settings::getInstance()->setString("MiximageScreenshotHorizontalFit",
miximageHorizontalFit->getSelected());
s->setNeedsSaving();
}
});
// Vertically oriented screenshots fit.
auto miximageVerticalFit = std::make_shared<OptionListComponent<std::string>>(
getHelpStyle(), "VERTICAL SCREENSHOT FIT", false);
const std::string selectedVerticalFit {
Settings::getInstance()->getString("MiximageScreenshotVerticalFit")};
miximageVerticalFit->add("contain", "contain", selectedVerticalFit == "contain");
miximageVerticalFit->add("crop", "crop", selectedVerticalFit == "crop");
miximageVerticalFit->add("stretch", "stretch", selectedVerticalFit == "stretch");
// If there are no objects returned, then there must be a manually modified entry in the
// configuration file. Simply set the vertical screenshot fit to "contain" in this case.
if (miximageVerticalFit->getSelectedObjects().size() == 0)
miximageVerticalFit->selectEntry(0);
s->addWithLabel("VERTICAL SCREENSHOT FIT", miximageVerticalFit);
s->addSaveFunc([miximageVerticalFit, s] {
if (miximageVerticalFit->getSelected() !=
Settings::getInstance()->getString("MiximageScreenshotVerticalFit")) {
Settings::getInstance()->setString("MiximageScreenshotVerticalFit",
miximageVerticalFit->getSelected());
s->setNeedsSaving();
}
});
// Screenshots aspect ratio threshold.
auto miximageAspectThreshold = std::make_shared<OptionListComponent<std::string>>(
getHelpStyle(), "ASPECT RATIO THRESHOLD", false);
const std::string selectedAspectThreshold {
Settings::getInstance()->getString("MiximageScreenshotAspectThreshold")};
miximageAspectThreshold->add("high", "high", selectedAspectThreshold == "high");
miximageAspectThreshold->add("low", "low", selectedAspectThreshold == "low");
// If there are no objects returned, then there must be a manually modified entry in the
// configuration file. Simply set the screenshot aspect threshold to "high" in this case.
if (miximageAspectThreshold->getSelectedObjects().size() == 0)
miximageAspectThreshold->selectEntry(0);
s->addWithLabel("SCREENSHOT ASPECT RATIO THRESHOLD", miximageAspectThreshold);
s->addSaveFunc([miximageAspectThreshold, s] {
if (miximageAspectThreshold->getSelected() !=
Settings::getInstance()->getString("MiximageScreenshotAspectThreshold")) {
Settings::getInstance()->setString("MiximageScreenshotAspectThreshold",
miximageAspectThreshold->getSelected());
s->setNeedsSaving();
}
});
// Blank areas fill color.
auto miximageBlankAreasColor = std::make_shared<OptionListComponent<std::string>>(
getHelpStyle(), "BLANK AREAS FILL COLOR", false);
const std::string selectedBlankAreasColor {
Settings::getInstance()->getString("MiximageScreenshotBlankAreasColor")};
miximageBlankAreasColor->add("black", "black", selectedBlankAreasColor == "black");
miximageBlankAreasColor->add("frame", "frame", selectedBlankAreasColor == "frame");
// If there are no objects returned, then there must be a manually modified entry in the
// configuration file. Simply set the blank area fill color to "black" in this case.
if (miximageBlankAreasColor->getSelectedObjects().size() == 0)
miximageBlankAreasColor->selectEntry(0);
s->addWithLabel("BLANK AREAS FILL COLOR", miximageBlankAreasColor);
s->addSaveFunc([miximageBlankAreasColor, s] {
if (miximageBlankAreasColor->getSelected() !=
Settings::getInstance()->getString("MiximageScreenshotBlankAreasColor")) {
Settings::getInstance()->setString("MiximageScreenshotBlankAreasColor",
miximageBlankAreasColor->getSelected());
s->setNeedsSaving();
}
});
// Screenshot scaling method.
auto miximageScaling = std::make_shared<OptionListComponent<std::string>>(
getHelpStyle(), "SCREENSHOT SCALING", false);

View file

@ -121,6 +121,10 @@ void Settings::setDefaults()
mBoolMap["ScrapeManuals"] = {true, true};
mStringMap["MiximageResolution"] = {"1280x960", "1280x960"};
mStringMap["MiximageScreenshotHorizontalFit"] = {"crop", "crop"};
mStringMap["MiximageScreenshotVerticalFit"] = {"contain", "contain"};
mStringMap["MiximageScreenshotAspectThreshold"] = {"high", "high"};
mStringMap["MiximageScreenshotBlankAreasColor"] = {"black", "black"};
mStringMap["MiximageScreenshotScaling"] = {"sharp", "sharp"};
mStringMap["MiximageBoxSize"] = {"medium", "medium"};
mStringMap["MiximagePhysicalMediaSize"] = {"medium", "medium"};

View file

@ -16,9 +16,9 @@ namespace Utils
cimg_library::CImg<unsigned char>& image)
{
// CImg does not interleave pixels as in BGRABGRABGRA so a conversion is required.
int counter = 0;
for (int r = 0; r < image.height(); ++r) {
for (int c = 0; c < image.width(); ++c) {
int counter {0};
for (int r {0}; r < image.height(); ++r) {
for (int c {0}; c < image.width(); ++c) {
image(c, r, 0, 0) = imageBGRA[counter + 0];
image(c, r, 0, 1) = imageBGRA[counter + 1];
image(c, r, 0, 2) = imageBGRA[counter + 2];
@ -31,8 +31,8 @@ namespace Utils
void convertCImgToBGRA(const cimg_library::CImg<unsigned char>& image,
std::vector<unsigned char>& imageBGRA)
{
for (int r = image.height() - 1; r >= 0; --r) {
for (int c = 0; c < image.width(); ++c) {
for (int r {image.height() - 1}; r >= 0; --r) {
for (int c {0}; c < image.width(); ++c) {
imageBGRA.emplace_back((unsigned char)image(c, r, 0, 0));
imageBGRA.emplace_back((unsigned char)image(c, r, 0, 1));
imageBGRA.emplace_back((unsigned char)image(c, r, 0, 2));
@ -45,9 +45,9 @@ namespace Utils
cimg_library::CImg<unsigned char>& image)
{
// CImg does not interleave pixels as in RGBARGBARGBA so a conversion is required.
int counter = 0;
for (int r = 0; r < image.height(); ++r) {
for (int c = 0; c < image.width(); ++c) {
int counter {0};
for (int r {0}; r < image.height(); ++r) {
for (int c {0}; c < image.width(); ++c) {
image(c, r, 0, 0) = imageRGBA[counter + 2];
image(c, r, 0, 1) = imageRGBA[counter + 1];
image(c, r, 0, 2) = imageRGBA[counter + 0];
@ -60,8 +60,8 @@ namespace Utils
void convertCImgToRGBA(const cimg_library::CImg<unsigned char>& image,
std::vector<unsigned char>& imageRGBA)
{
for (int r = image.height() - 1; r >= 0; --r) {
for (int c = 0; c < image.width(); ++c) {
for (int r {image.height() - 1}; r >= 0; --r) {
for (int c {0}; c < image.width(); ++c) {
imageRGBA.emplace_back((unsigned char)image(c, r, 0, 2));
imageRGBA.emplace_back((unsigned char)image(c, r, 0, 1));
imageRGBA.emplace_back((unsigned char)image(c, r, 0, 0));
@ -77,44 +77,44 @@ namespace Utils
if (image.spectrum() != 4)
return;
double pixelValueSum = 0.0l;
int rowCounterTop = 0;
int rowCounterBottom = 0;
unsigned int columnCounterLeft = 0;
unsigned int columnCounterRight = 0;
double pixelValueSum {0.0};
int rowCounterTop {0};
int rowCounterBottom {0};
unsigned int columnCounterLeft {0};
unsigned int columnCounterRight {0};
// Count the number of rows and columns that are completely transparent.
for (int i = image.height() - 1; i > 0; --i) {
for (int i {image.height() - 1}; i > 0; --i) {
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
pixelValueSum = imageRow.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++rowCounterTop;
else
break;
}
for (int i = 0; i < image.height(); ++i) {
for (int i {0}; i < image.height(); ++i) {
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
pixelValueSum = imageRow.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++rowCounterBottom;
else
break;
}
for (int i = 0; i < image.width(); ++i) {
for (int i {0}; i < image.width(); ++i) {
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
pixelValueSum = imageColumn.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++columnCounterLeft;
else
break;
}
for (int i = image.width() - 1; i > 0; --i) {
for (int i {image.width() - 1}; i > 0; --i) {
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
pixelValueSum = imageColumn.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++columnCounterRight;
else
break;
@ -132,44 +132,44 @@ namespace Utils
if (image.spectrum() != 4)
return;
double pixelValueSum = 0.0l;
int rowCounterTop = 0;
int rowCounterBottom = 0;
unsigned int columnCounterLeft = 0;
unsigned int columnCounterRight = 0;
double pixelValueSum {0.0};
int rowCounterTop {0};
int rowCounterBottom {0};
unsigned int columnCounterLeft {0};
unsigned int columnCounterRight {0};
// Count the number of rows and columns that are completely transparent.
for (int i = image.height() - 1; i > 0; --i) {
for (int i {image.height() - 1}; i > 0; --i) {
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
pixelValueSum = imageRow.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++rowCounterTop;
else
break;
}
for (int i = 0; i < image.height(); ++i) {
for (int i {0}; i < image.height(); ++i) {
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
pixelValueSum = imageRow.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++rowCounterBottom;
else
break;
}
for (int i = 0; i < image.width(); ++i) {
for (int i {0}; i < image.width(); ++i) {
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
pixelValueSum = imageColumn.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++columnCounterLeft;
else
break;
}
for (int i = image.width() - 1; i > 0; --i) {
for (int i {image.width() - 1}; i > 0; --i) {
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
pixelValueSum = imageColumn.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++columnCounterRight;
else
break;
@ -191,27 +191,27 @@ namespace Utils
void cropLetterboxes(cimg_library::CImg<unsigned char>& image)
{
double pixelValueSum = 0.0l;
int rowCounterUpper = 0;
int rowCounterLower = 0;
double pixelValueSum {0.0};
int rowCounterUpper {0};
int rowCounterLower {0};
// Count the number of rows that are pure black.
for (int i = image.height() - 1; i > 0; --i) {
for (int i {image.height() - 1}; i > 0; --i) {
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
// Ignore the alpha channel.
imageRow.channels(0, 2);
pixelValueSum = imageRow.sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++rowCounterUpper;
else
break;
}
for (int i = 0; i < image.height(); ++i) {
for (int i {0}; i < image.height(); ++i) {
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
imageRow.channels(0, 2);
pixelValueSum = imageRow.sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++rowCounterLower;
else
break;
@ -227,27 +227,27 @@ namespace Utils
void cropPillarboxes(cimg_library::CImg<unsigned char>& image)
{
double pixelValueSum = 0.0l;
unsigned int columnCounterLeft = 0;
unsigned int columnCounterRight = 0;
double pixelValueSum {0.0};
unsigned int columnCounterLeft {0};
unsigned int columnCounterRight {0};
// Count the number of columns that are pure black.
for (int i = 0; i < image.width(); ++i) {
for (int i {0}; i < image.width(); ++i) {
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
// Ignore the alpha channel.
imageColumn.channels(0, 2);
pixelValueSum = imageColumn.sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++columnCounterLeft;
else
break;
}
for (int i = image.width() - 1; i > 0; --i) {
for (int i {image.width() - 1}; i > 0; --i) {
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
imageColumn.channels(0, 2);
pixelValueSum = imageColumn.sum();
if (pixelValueSum == 0.0l)
if (pixelValueSum == 0.0)
++columnCounterRight;
else
break;