2021-06-12 18:05:28 +00:00
|
|
|
// 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
|
|
|
|
{
|
2022-10-28 19:39:57 +00:00
|
|
|
void convertBGRAToCImg(const std::vector<unsigned char>& imageBGRA,
|
|
|
|
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) {
|
|
|
|
image(c, r, 0, 0) = imageBGRA[counter + 0];
|
|
|
|
image(c, r, 0, 1) = imageBGRA[counter + 1];
|
|
|
|
image(c, r, 0, 2) = imageBGRA[counter + 2];
|
|
|
|
image(c, r, 0, 3) = imageBGRA[counter + 3];
|
|
|
|
counter += 4;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
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));
|
|
|
|
imageBGRA.emplace_back((unsigned char)image(c, r, 0, 3));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-09 21:57:15 +00:00
|
|
|
void convertRGBAToCImg(const std::vector<unsigned char>& imageRGBA,
|
2021-07-07 18:31:46 +00:00
|
|
|
cimg_library::CImg<unsigned char>& image)
|
2021-06-12 18:05:28 +00:00
|
|
|
{
|
2022-10-28 19:39:57 +00:00
|
|
|
// CImg does not interleave pixels as in RGBARGBARGBA so a conversion is required.
|
2021-06-12 18:05:28 +00:00
|
|
|
int counter = 0;
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int r = 0; r < image.height(); ++r) {
|
|
|
|
for (int c = 0; c < image.width(); ++c) {
|
2021-06-12 18:05:28 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-09 21:57:15 +00:00
|
|
|
void convertCImgToRGBA(const cimg_library::CImg<unsigned char>& image,
|
2021-07-07 18:31:46 +00:00
|
|
|
std::vector<unsigned char>& imageRGBA)
|
2021-06-12 18:05:28 +00:00
|
|
|
{
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int r = image.height() - 1; r >= 0; --r) {
|
|
|
|
for (int c = 0; c < image.width(); ++c) {
|
2021-11-09 21:57:15 +00:00
|
|
|
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));
|
|
|
|
imageRGBA.emplace_back((unsigned char)image(c, r, 0, 3));
|
2021-06-12 18:05:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void getTransparentPaddingCoords(cimg_library::CImg<unsigned char>& image,
|
2021-07-07 18:31:46 +00:00
|
|
|
int (&imageCoords)[4])
|
2021-06-12 18:05:28 +00:00
|
|
|
{
|
|
|
|
// 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.
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = image.height() - 1; i > 0; --i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
|
|
|
|
pixelValueSum = imageRow.get_shared_channel(3).sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++rowCounterTop;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = 0; i < image.height(); ++i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
|
|
|
|
pixelValueSum = imageRow.get_shared_channel(3).sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++rowCounterBottom;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = 0; i < image.width(); ++i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
|
|
|
|
pixelValueSum = imageColumn.get_shared_channel(3).sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++columnCounterLeft;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = image.width() - 1; i > 0; --i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
|
|
|
|
pixelValueSum = imageColumn.get_shared_channel(3).sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++columnCounterRight;
|
2021-06-12 18:05:28 +00:00
|
|
|
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.
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = image.height() - 1; i > 0; --i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
|
|
|
|
pixelValueSum = imageRow.get_shared_channel(3).sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++rowCounterTop;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = 0; i < image.height(); ++i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
|
|
|
|
pixelValueSum = imageRow.get_shared_channel(3).sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++rowCounterBottom;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = 0; i < image.width(); ++i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
|
|
|
|
pixelValueSum = imageColumn.get_shared_channel(3).sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++columnCounterLeft;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = image.width() - 1; i > 0; --i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
|
|
|
|
pixelValueSum = imageColumn.get_shared_channel(3).sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++columnCounterRight;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (rowCounterTop > 0)
|
2021-07-07 18:31:46 +00:00
|
|
|
image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 - rowCounterTop, 0, 0);
|
2021-06-12 18:05:28 +00:00
|
|
|
|
|
|
|
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)
|
2021-07-07 18:31:46 +00:00
|
|
|
image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1, image.height() - 1,
|
|
|
|
0, 0);
|
2021-06-12 18:05:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = image.height() - 1; i > 0; --i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
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)
|
2021-11-17 16:48:49 +00:00
|
|
|
++rowCounterUpper;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = 0; i < image.height(); ++i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageRow = image.get_rows(i, i);
|
|
|
|
imageRow.channels(0, 2);
|
|
|
|
pixelValueSum = imageRow.sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++rowCounterLower;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (rowCounterUpper > 0)
|
2021-07-07 18:31:46 +00:00
|
|
|
image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 - rowCounterUpper, 0,
|
|
|
|
0);
|
2021-06-12 18:05:28 +00:00
|
|
|
|
|
|
|
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.
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = 0; i < image.width(); ++i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
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)
|
2021-11-17 16:48:49 +00:00
|
|
|
++columnCounterLeft;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-11-17 16:48:49 +00:00
|
|
|
for (int i = image.width() - 1; i > 0; --i) {
|
2021-06-12 18:05:28 +00:00
|
|
|
cimg_library::CImg<unsigned char> imageColumn = image.get_columns(i, i);
|
|
|
|
imageColumn.channels(0, 2);
|
|
|
|
pixelValueSum = imageColumn.sum();
|
|
|
|
if (pixelValueSum == 0.0l)
|
2021-11-17 16:48:49 +00:00
|
|
|
++columnCounterRight;
|
2021-06-12 18:05:28 +00:00
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (columnCounterLeft > 0)
|
|
|
|
image.crop(columnCounterLeft, 0, 0, 3, image.width() - 1, image.height() - 1, 0, 0);
|
|
|
|
|
|
|
|
if (columnCounterRight > 0)
|
2021-07-07 18:31:46 +00:00
|
|
|
image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1, image.height() - 1,
|
|
|
|
0, 0);
|
2021-06-12 18:05:28 +00:00
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
void addDropShadow(cimg_library::CImg<unsigned char>& image,
|
|
|
|
unsigned int shadowDistance,
|
|
|
|
float transparency,
|
|
|
|
unsigned int iterations)
|
2021-06-12 18:05:28 +00:00
|
|
|
{
|
|
|
|
// Check that the image actually has an alpha channel.
|
|
|
|
if (image.spectrum() != 4)
|
|
|
|
return;
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
// 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);
|
2021-06-12 18:05:28 +00:00
|
|
|
|
|
|
|
// 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),
|
2021-07-07 18:31:46 +00:00
|
|
|
static_cast<const float>(shadowDistance), 1, true, iterations);
|
2021-06-12 18:05:28 +00:00
|
|
|
|
|
|
|
// 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),
|
2021-07-07 18:31:46 +00:00
|
|
|
maskImage.get_shared_channel(3), 1, 255);
|
2021-06-12 18:05:28 +00:00
|
|
|
// Draw the source image on top of the shadow image.
|
|
|
|
shadowImage.draw_image(0, 0, image.get_shared_channels(0, 2),
|
2021-07-07 18:31:46 +00:00
|
|
|
image.get_shared_channel(3), 1, 255);
|
2021-06-12 18:05:28 +00:00
|
|
|
// Remove the any unused space that we added to leave room for the shadow.
|
|
|
|
removeTransparentPadding(shadowImage);
|
|
|
|
|
|
|
|
image = shadowImage;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
} // namespace CImg
|
2021-06-12 18:05:28 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
} // namespace Utils
|