Moved most CImg functions from MiximageGenerator to a new utility module.

This commit is contained in:
Leon Styhre 2021-06-12 20:05:28 +02:00
parent a9a6b606db
commit 05990d0457
5 changed files with 327 additions and 215 deletions

View file

@ -322,14 +322,14 @@ bool MiximageGenerator::generateImage()
CImg<unsigned char> screenshotImage(fileWidth, fileHeight, 1, 4, 0);
// Convert image to CImg internal format.
convertToCImgFormat(screenshotImage, screenshotVector);
// Convert the RGBA image to CImg internal format.
Utils::CImg::convertRGBAToCImg(screenshotVector, screenshotImage);
screenshotVector.clear();
if (Settings::getInstance()->getBool("MiximageRemoveLetterboxes"))
cropLetterboxes(screenshotImage);
Utils::CImg::cropLetterboxes(screenshotImage);
if (Settings::getInstance()->getBool("MiximageRemovePillarboxes"))
cropPillarboxes(screenshotImage);
Utils::CImg::cropPillarboxes(screenshotImage);
if (Settings::getInstance()->getString("MiximageScreenshotScaling") == "smooth") {
// Lanczos scaling is normally not recommended for low resolution graphics as
@ -391,8 +391,8 @@ bool MiximageGenerator::generateImage()
marqueeImage = CImg<unsigned char>(FreeImage_GetWidth(marqueeFile),
FreeImage_GetHeight(marqueeFile), 1, 4, 0);
convertToCImgFormat(marqueeImage, marqueeVector);
removeTransparentPadding(marqueeImage);
Utils::CImg::convertRGBAToCImg(marqueeVector, marqueeImage);
Utils::CImg::removeTransparentPadding(marqueeImage);
unsigned int marqueeWidth = static_cast<unsigned int>(marqueeImage.width());
unsigned int marqueeHeight = static_cast<unsigned int>(marqueeImage.height());
@ -402,7 +402,8 @@ bool MiximageGenerator::generateImage()
// We use Lanczos3 which is the highest quality resampling method available.
marqueeImage.resize(marqueeWidth, marqueeHeight, 1, 4, 6);
addDropShadow(marqueeImage, marqueeShadowSize);
// Add a drop shadow using 4 iterations of box blur.
Utils::CImg::addDropShadow(marqueeImage, marqueeShadowSize, 0.6, 4);
xPosMarquee = canvasImage.width() - marqueeImage.width();
yPosMarquee = 0;
@ -432,8 +433,8 @@ bool MiximageGenerator::generateImage()
boxImage = CImg<unsigned char>(FreeImage_GetWidth(boxFile),
FreeImage_GetHeight(boxFile), 1, 4);
convertToCImgFormat(boxImage, boxVector);
removeTransparentPadding(boxImage);
Utils::CImg::convertRGBAToCImg(boxVector, boxImage);
Utils::CImg::removeTransparentPadding(boxImage);
float scaleFactor = static_cast<float>(boxTargetHeight) /
static_cast<float>(boxImage.height());
@ -457,7 +458,7 @@ bool MiximageGenerator::generateImage()
boxImage.resize(width, boxTargetHeight, 1, 4, 6);
}
addDropShadow(boxImage, boxShadowSize);
Utils::CImg::addDropShadow(boxImage, boxShadowSize, 0.6, 4);
xPosBox = 0;
yPosBox = canvasImage.height() - boxImage.height();
@ -522,8 +523,8 @@ bool MiximageGenerator::generateImage()
std::vector<unsigned char> canvasVector;
// Convert image from CImg internal format.
convertFromCImgFormat(canvasImage, canvasVector);
// Convert the image from CImg internal format.
Utils::CImg::convertCImgToRGBA(canvasImage, canvasVector);
FIBITMAP* mixImage = nullptr;
mixImage = FreeImage_ConvertFromRawBits(&canvasVector.at(0), canvasImage.width(),
@ -553,169 +554,6 @@ bool MiximageGenerator::generateImage()
return true;
}
void MiximageGenerator::cropLetterboxes(CImg<unsigned char>& image)
{
double pixelValueSum = 0.0l;
int rowCounterUpper = 0;
int rowCounterLower = 0;
// Count the number of rows that are pure black.
for (int i = image.height() - 1; i > 0; i--) {
CImg<unsigned char> imageRow = image.get_rows(i, i);
// Ignore the alpha channel.
imageRow.channels(0, 2);
pixelValueSum = imageRow.sum();
if (pixelValueSum == 0.0l)
rowCounterUpper++;
else
break;
}
for (int i = 0; i < image.height(); i++) {
CImg<unsigned char> imageRow = image.get_rows(i, i);
imageRow.channels(0, 2);
pixelValueSum = imageRow.sum();
if (pixelValueSum == 0.0l)
rowCounterLower++;
else
break;
}
if (rowCounterUpper > 0)
image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 - rowCounterUpper, 0, 0);
if (rowCounterLower > 0)
image.crop(0, rowCounterLower, 0, 3, image.width() - 1, image.height() - 1, 0, 0);
}
void MiximageGenerator::cropPillarboxes(CImg<unsigned char>& image)
{
double pixelValueSum = 0.0l;
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++) {
CImg<unsigned char> imageColumn = image.get_columns(i, i);
// Ignore the alpha channel.
imageColumn.channels(0, 2);
pixelValueSum = imageColumn.sum();
if (pixelValueSum == 0.0l)
columnCounterLeft++;
else
break;
}
for (int i = image.width() - 1; i > 0; i--) {
CImg<unsigned char> imageColumn = image.get_columns(i, i);
imageColumn.channels(0, 2);
pixelValueSum = imageColumn.sum();
if (pixelValueSum == 0.0l)
columnCounterRight++;
else
break;
}
if (columnCounterLeft > 0)
image.crop(columnCounterLeft, 0, 0, 3, image.width() - 1, image.height() - 1, 0, 0);
if (columnCounterRight > 0)
image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1, image.height() - 1, 0, 0);
}
void MiximageGenerator::removeTransparentPadding(CImg<unsigned char>& image)
{
if (image.spectrum() != 4)
return;
double pixelValueSum = 0.0l;
int rowCounterUpper = 0;
int rowCounterLower = 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--) {
CImg<unsigned char> imageRow = image.get_rows(i, i);
pixelValueSum = imageRow.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
rowCounterUpper++;
else
break;
}
for (int i = 0; i < image.height(); i++) {
CImg<unsigned char> imageRow = image.get_rows(i, i);
pixelValueSum = imageRow.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
rowCounterLower++;
else
break;
}
for (int i = 0; i < image.width(); i++) {
CImg<unsigned char> imageColumn = image.get_columns(i, i);
pixelValueSum = imageColumn.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
columnCounterLeft++;
else
break;
}
for (int i = image.width() - 1; i > 0; i--) {
CImg<unsigned char> imageColumn = image.get_columns(i, i);
pixelValueSum = imageColumn.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
columnCounterRight++;
else
break;
}
if (rowCounterUpper > 0)
image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 - rowCounterUpper, 0, 0);
if (rowCounterLower > 0)
image.crop(0, rowCounterLower, 0, 3, image.width() - 1, image.height() - 1, 0, 0);
if (columnCounterLeft > 0)
image.crop(columnCounterLeft, 0, 0, 3, image.width() - 1, image.height() - 1, 0, 0);
if (columnCounterRight > 0)
image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1, image.height() - 1, 0, 0);
}
void MiximageGenerator::addDropShadow(CImg<unsigned char>& image, unsigned int shadowDistance)
{
// Make the shadow image larger than the source image to leave space for the drop shadow.
CImg<unsigned char> shadowImage(image.width() + shadowDistance * 3,
image.height() + shadowDistance * 3, 1, 4, 0);
// Create a mask image.
CImg<unsigned char> maskImage(image.width(), image.height(), 1, 4, 0);
maskImage.draw_image(0, 0, image);
// Fill the RGB channels with white so we end up with a simple mask.
maskImage.get_shared_channels(0, 2).fill(255);
// Make a black outline of the source image as a basis for the shadow.
shadowImage.draw_image(shadowDistance, shadowDistance, image);
shadowImage.get_shared_channels(0, 2).fill(0);
// Lower the transparency and apply the blur.
shadowImage.get_shared_channel(3) /= 0.6f;
shadowImage.blur_box(static_cast<const float>(shadowDistance),
static_cast<const float>(shadowDistance), 1, true, 4);
// Add the mask to the alpha channel of the shadow image.
shadowImage.get_shared_channel(3).draw_image(0, 0, maskImage.get_shared_channels(0, 0),
maskImage.get_shared_channel(3), 1, 255);
// Draw the source image on top of the shadow image.
shadowImage.draw_image(0, 0, image.get_shared_channels(0, 2),
image.get_shared_channel(3), 1, 255);
// Remove the any unused space that we added to leave room for the shadow.
removeTransparentPadding(shadowImage);
image = shadowImage;
}
void MiximageGenerator::calculateMarqueeSize(const unsigned int& targetWidth,
const unsigned int& targetHeight, unsigned int& width, unsigned int& height)
{
@ -810,35 +648,6 @@ void MiximageGenerator::sampleFrameColor(CImg<unsigned char>& screenshotImage,
frameColor[3] = 255;
}
void MiximageGenerator::convertToCImgFormat(CImg<unsigned char>& image,
std::vector<unsigned char> imageVector)
{
// CImg does not interleave the 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++) {
image(c, r, 0, 0) = imageVector[counter + 2];
image(c, r, 0, 1) = imageVector[counter + 1];
image(c, r, 0, 2) = imageVector[counter + 0];
image(c, r, 0, 3) = imageVector[counter + 3];
counter += 4;
}
}
}
void MiximageGenerator::convertFromCImgFormat(CImg<unsigned char> image,
std::vector<unsigned char>& imageVector)
{
for (int r = image.height() - 1; r >= 0; r--) {
for (int c = 0; c < image.width(); c++) {
imageVector.push_back((unsigned char)image(c,r,0,2));
imageVector.push_back((unsigned char)image(c,r,0,1));
imageVector.push_back((unsigned char)image(c,r,0,0));
imageVector.push_back((unsigned char)image(c,r,0,3));
}
}
}
std::string MiximageGenerator::getSavePath()
{
const std::string name = Utils::FileSystem::getStem(mGame->getPath());

View file

@ -10,13 +10,10 @@
#ifndef ES_APP_SCRAPERS_MIXIMAGE_GENERATOR_H
#define ES_APP_SCRAPERS_MIXIMAGE_GENERATOR_H
// Disable the CImg display capabilities.
#define cimg_display 0
#include "utils/CImgUtil.h"
#include "FileData.h"
#include "GuiComponent.h"
#include <CImg.h>
#include <FreeImage.h>
#include <future>
@ -32,17 +29,10 @@ public:
private:
bool generateImage();
void cropLetterboxes(CImg<unsigned char>& image);
void cropPillarboxes(CImg<unsigned char>& image);
void removeTransparentPadding(CImg<unsigned char>& image);
void addDropShadow(CImg<unsigned char>& image, unsigned int shadowDistance);
void calculateMarqueeSize(const unsigned int& targetWidth, const unsigned int& targetHeight,
unsigned int& width, unsigned int& height);
void sampleFrameColor(CImg<unsigned char>& screenshotImage, unsigned char (&frameColor)[4]);
void convertToCImgFormat(CImg<unsigned char>& image, std::vector<unsigned char> imageVector);
void convertFromCImgFormat(CImg<unsigned char> image, std::vector<unsigned char>& imageVector);
std::string getSavePath();
FileData* mGame;

View file

@ -78,6 +78,7 @@ set(CORE_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/resources/TextureResource.h
# Utils
${CMAKE_CURRENT_SOURCE_DIR}/src/utils/CImgUtil.h
${CMAKE_CURRENT_SOURCE_DIR}/src/utils/FileSystemUtil.h
${CMAKE_CURRENT_SOURCE_DIR}/src/utils/StringUtil.h
${CMAKE_CURRENT_SOURCE_DIR}/src/utils/TimeUtil.h
@ -157,6 +158,7 @@ set(CORE_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/resources/TextureDataManager.cpp
# Utils
${CMAKE_CURRENT_SOURCE_DIR}/src/utils/CImgUtil.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/utils/FileSystemUtil.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/utils/StringUtil.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/utils/TimeUtil.cpp

View file

@ -0,0 +1,275 @@
// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// CImgUtil.cpp
//
// Utility functions using the CImg image processing library.
//
#include "utils/CImgUtil.h"
namespace Utils
{
namespace CImg
{
void convertRGBAToCImg(std::vector<unsigned char> imageRGBA,
cimg_library::CImg<unsigned char>& image)
{
// CImg does not interleave the 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++) {
image(c, r, 0, 0) = imageRGBA[counter + 2];
image(c, r, 0, 1) = imageRGBA[counter + 1];
image(c, r, 0, 2) = imageRGBA[counter + 0];
image(c, r, 0, 3) = imageRGBA[counter + 3];
counter += 4;
}
}
}
void convertCImgToRGBA(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++) {
imageRGBA.push_back((unsigned char)image(c,r,0,2));
imageRGBA.push_back((unsigned char)image(c,r,0,1));
imageRGBA.push_back((unsigned char)image(c,r,0,0));
imageRGBA.push_back((unsigned char)image(c,r,0,3));
}
}
}
void getTransparentPaddingCoords(cimg_library::CImg<unsigned char>& image,
int (&imageCoords)[4])
{
// Check that the image actually has an alpha channel.
if (image.spectrum() != 4)
return;
double pixelValueSum = 0.0l;
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--) {
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
pixelValueSum = imageRow.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
rowCounterTop++;
else
break;
}
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)
rowCounterBottom++;
else
break;
}
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)
columnCounterLeft++;
else
break;
}
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)
columnCounterRight++;
else
break;
}
imageCoords[0] = columnCounterLeft;
imageCoords[1] = rowCounterTop;
imageCoords[2] = columnCounterRight;
imageCoords[3] = rowCounterBottom;
}
void removeTransparentPadding(cimg_library::CImg<unsigned char>& image)
{
// Check that the image actually has an alpha channel.
if (image.spectrum() != 4)
return;
double pixelValueSum = 0.0l;
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--) {
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
pixelValueSum = imageRow.get_shared_channel(3).sum();
if (pixelValueSum == 0.0l)
rowCounterTop++;
else
break;
}
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)
rowCounterBottom++;
else
break;
}
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)
columnCounterLeft++;
else
break;
}
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)
columnCounterRight++;
else
break;
}
if (rowCounterTop > 0)
image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 -
rowCounterTop, 0, 0);
if (rowCounterBottom > 0)
image.crop(0, rowCounterBottom, 0, 3, image.width() - 1, image.height() - 1, 0, 0);
if (columnCounterLeft > 0)
image.crop(columnCounterLeft, 0, 0, 3, image.width() - 1, image.height() - 1, 0, 0);
if (columnCounterRight > 0)
image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1,
image.height() - 1, 0, 0);
}
void cropLetterboxes(cimg_library::CImg<unsigned char>& image)
{
double pixelValueSum = 0.0l;
int rowCounterUpper = 0;
int rowCounterLower = 0;
// Count the number of rows that are pure black.
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)
rowCounterUpper++;
else
break;
}
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)
rowCounterLower++;
else
break;
}
if (rowCounterUpper > 0)
image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 -
rowCounterUpper, 0, 0);
if (rowCounterLower > 0)
image.crop(0, rowCounterLower, 0, 3, image.width() - 1, image.height() - 1, 0, 0);
}
void cropPillarboxes(cimg_library::CImg<unsigned char>& image)
{
double pixelValueSum = 0.0l;
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++) {
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)
columnCounterLeft++;
else
break;
}
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)
columnCounterRight++;
else
break;
}
if (columnCounterLeft > 0)
image.crop(columnCounterLeft, 0, 0, 3, image.width() - 1, image.height() - 1, 0, 0);
if (columnCounterRight > 0)
image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1,
image.height() - 1, 0, 0);
}
void addDropShadow(cimg_library::CImg<unsigned char>& image, unsigned int shadowDistance,
float transparency, unsigned int iterations)
{
// Check that the image actually has an alpha channel.
if (image.spectrum() != 4)
return;
// Make the shadow image larger than the source image to leave space for the drop shadow.
cimg_library::CImg<unsigned char> shadowImage(image.width() + shadowDistance * 3,
image.height() + shadowDistance * 3, 1, 4, 0);
// Create a mask image.
cimg_library::CImg<unsigned char> maskImage(image.width(), image.height(), 1, 4, 0);
maskImage.draw_image(0, 0, image);
// Fill the RGB channels with white so we end up with a simple mask.
maskImage.get_shared_channels(0, 2).fill(255);
// Make a black outline of the source image as a basis for the shadow.
shadowImage.draw_image(shadowDistance, shadowDistance, image);
shadowImage.get_shared_channels(0, 2).fill(0);
// Lower the transparency and apply the blur.
shadowImage.get_shared_channel(3) /= transparency;
shadowImage.blur_box(static_cast<const float>(shadowDistance),
static_cast<const float>(shadowDistance), 1, true, iterations);
// Add the mask to the alpha channel of the shadow image.
shadowImage.get_shared_channel(3).draw_image(0, 0, maskImage.get_shared_channels(0, 0),
maskImage.get_shared_channel(3), 1, 255);
// Draw the source image on top of the shadow image.
shadowImage.draw_image(0, 0, image.get_shared_channels(0, 2),
image.get_shared_channel(3), 1, 255);
// Remove the any unused space that we added to leave room for the shadow.
removeTransparentPadding(shadowImage);
image = shadowImage;
}
} // CImg::
} // Utils::

View file

@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// CImgUtil.h
//
// Utility functions using the CImg image processing library.
//
#ifndef ES_CORE_UTILS_CIMG_UTIL_H
#define ES_CORE_UTILS_CIMG_UTIL_H
// Disable the CImg display capabilities.
#define cimg_display 0
#include <CImg.h>
#include <vector>
namespace Utils
{
namespace CImg
{
void convertRGBAToCImg(std::vector<unsigned char> imageRGBA,
cimg_library::CImg<unsigned char>& image);
void convertCImgToRGBA(cimg_library::CImg<unsigned char> image,
std::vector<unsigned char>& imageRGBA);
void getTransparentPaddingCoords(cimg_library::CImg<unsigned char>& image,
int (&imageCoords)[4]);
void removeTransparentPadding(cimg_library::CImg<unsigned char>& image);
void cropLetterboxes(cimg_library::CImg<unsigned char>& image);
void cropPillarboxes(cimg_library::CImg<unsigned char>& image);
void addDropShadow(cimg_library::CImg<unsigned char>& image, unsigned int shadowDistance,
float transparency, unsigned int iterations);
}
}
#endif // ES_CORE_UTILS_CIMG_UTIL_H