2015-03-09 14:32:11 +01:00
|
|
|
/*
|
|
|
|
Copyright (C) 2015 Aaron Suen <warr1024@gmail.com>
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Lesser General Public License as published by
|
|
|
|
the Free Software Foundation; either version 2.1 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Lesser General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Lesser General Public License along
|
|
|
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "imagefilters.h"
|
|
|
|
#include "util/numeric.h"
|
2017-08-18 08:07:59 +02:00
|
|
|
#include <cmath>
|
2021-04-20 19:50:19 +02:00
|
|
|
#include <cassert>
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
// Simple 2D bitmap class with just the functionality needed here
|
|
|
|
class Bitmap {
|
|
|
|
u32 linesize, lines;
|
|
|
|
std::vector<u8> data;
|
|
|
|
|
|
|
|
static inline u32 bytepos(u32 index) { return index >> 3; }
|
|
|
|
static inline u8 bitpos(u32 index) { return index & 7; }
|
|
|
|
|
|
|
|
public:
|
|
|
|
Bitmap(u32 width, u32 height) : linesize(width), lines(height),
|
|
|
|
data(bytepos(width * height) + 1) {}
|
|
|
|
|
|
|
|
inline bool get(u32 x, u32 y) const {
|
|
|
|
u32 index = y * linesize + x;
|
|
|
|
return data[bytepos(index)] & (1 << bitpos(index));
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void set(u32 x, u32 y) {
|
|
|
|
u32 index = y * linesize + x;
|
|
|
|
data[bytepos(index)] |= 1 << bitpos(index);
|
|
|
|
}
|
|
|
|
|
|
|
|
inline bool all() const {
|
|
|
|
for (u32 i = 0; i < data.size() - 1; i++) {
|
|
|
|
if (data[i] != 0xff)
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// last byte not entirely filled
|
|
|
|
for (u8 i = 0; i < bitpos(linesize * lines); i++) {
|
|
|
|
bool value_of_bit = data.back() & (1 << i);
|
|
|
|
if (!value_of_bit)
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void copy(Bitmap &to) const {
|
|
|
|
assert(to.linesize == linesize && to.lines == lines);
|
|
|
|
to.data = data;
|
|
|
|
}
|
|
|
|
};
|
2015-03-09 14:32:11 +01:00
|
|
|
|
|
|
|
/* Fill in RGB values for transparent pixels, to correct for odd colors
|
|
|
|
* appearing at borders when blending. This is because many PNG optimizers
|
|
|
|
* like to discard RGB values of transparent pixels, but when blending then
|
2021-04-20 19:50:19 +02:00
|
|
|
* with non-transparent neighbors, their RGB values will show up nonetheless.
|
2015-03-09 14:32:11 +01:00
|
|
|
*
|
|
|
|
* This function modifies the original image in-place.
|
|
|
|
*
|
|
|
|
* Parameter "threshold" is the alpha level below which pixels are considered
|
2021-04-20 19:50:19 +02:00
|
|
|
* transparent. Should be 127 when the texture is used with ALPHA_CHANNEL_REF,
|
|
|
|
* 0 when alpha blending is used.
|
2015-03-09 14:32:11 +01:00
|
|
|
*/
|
2015-04-01 06:19:10 +02:00
|
|
|
void imageCleanTransparent(video::IImage *src, u32 threshold)
|
|
|
|
{
|
2015-03-09 14:32:11 +01:00
|
|
|
core::dimension2d<u32> dim = src->getDimension();
|
|
|
|
|
2021-04-20 19:50:19 +02:00
|
|
|
Bitmap bitmap(dim.Width, dim.Height);
|
|
|
|
|
|
|
|
// First pass: Mark all opaque pixels
|
2015-03-09 14:32:11 +01:00
|
|
|
// Note: loop y around x for better cache locality.
|
|
|
|
for (u32 ctry = 0; ctry < dim.Height; ctry++)
|
|
|
|
for (u32 ctrx = 0; ctrx < dim.Width; ctrx++) {
|
2021-04-20 19:50:19 +02:00
|
|
|
if (src->getPixel(ctrx, ctry).getAlpha() > threshold)
|
|
|
|
bitmap.set(ctrx, ctry);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Exit early if all pixels opaque
|
|
|
|
if (bitmap.all())
|
|
|
|
return;
|
|
|
|
|
|
|
|
Bitmap newmap = bitmap;
|
|
|
|
|
|
|
|
// Then repeatedly look for transparent pixels, filling them in until
|
|
|
|
// we're finished (capped at 50 iterations).
|
|
|
|
for (u32 iter = 0; iter < 50; iter++) {
|
2015-03-09 14:32:11 +01:00
|
|
|
|
2021-04-20 19:50:19 +02:00
|
|
|
for (u32 ctry = 0; ctry < dim.Height; ctry++)
|
|
|
|
for (u32 ctrx = 0; ctrx < dim.Width; ctrx++) {
|
|
|
|
// Skip pixels we have already processed
|
|
|
|
if (bitmap.get(ctrx, ctry))
|
2015-03-09 14:32:11 +01:00
|
|
|
continue;
|
|
|
|
|
2021-04-20 19:50:19 +02:00
|
|
|
video::SColor c = src->getPixel(ctrx, ctry);
|
|
|
|
|
|
|
|
// Sample size and total weighted r, g, b values
|
2015-03-09 14:32:11 +01:00
|
|
|
u32 ss = 0, sr = 0, sg = 0, sb = 0;
|
|
|
|
|
2021-04-20 19:50:19 +02:00
|
|
|
// Walk each neighbor pixel (clipped to image bounds)
|
2015-03-09 14:32:11 +01:00
|
|
|
for (u32 sy = (ctry < 1) ? 0 : (ctry - 1);
|
|
|
|
sy <= (ctry + 1) && sy < dim.Height; sy++)
|
|
|
|
for (u32 sx = (ctrx < 1) ? 0 : (ctrx - 1);
|
|
|
|
sx <= (ctrx + 1) && sx < dim.Width; sx++) {
|
2021-04-20 19:50:19 +02:00
|
|
|
// Ignore pixels we haven't processed
|
|
|
|
if (!bitmap.get(sx, sy))
|
2015-03-09 14:32:11 +01:00
|
|
|
continue;
|
2021-04-20 19:50:19 +02:00
|
|
|
|
|
|
|
// Add RGB values weighted by alpha IF the pixel is opaque, otherwise
|
|
|
|
// use full weight since we want to propagate colors.
|
|
|
|
video::SColor d = src->getPixel(sx, sy);
|
|
|
|
u32 a = d.getAlpha() <= threshold ? 255 : d.getAlpha();
|
2015-03-09 14:32:11 +01:00
|
|
|
ss += a;
|
|
|
|
sr += a * d.getRed();
|
|
|
|
sg += a * d.getGreen();
|
|
|
|
sb += a * d.getBlue();
|
|
|
|
}
|
|
|
|
|
2021-04-20 19:50:19 +02:00
|
|
|
// Set pixel to average weighted by alpha
|
2015-03-09 14:32:11 +01:00
|
|
|
if (ss > 0) {
|
|
|
|
c.setRed(sr / ss);
|
|
|
|
c.setGreen(sg / ss);
|
|
|
|
c.setBlue(sb / ss);
|
|
|
|
src->setPixel(ctrx, ctry, c);
|
2021-04-20 19:50:19 +02:00
|
|
|
newmap.set(ctrx, ctry);
|
2015-03-09 14:32:11 +01:00
|
|
|
}
|
|
|
|
}
|
2021-04-20 19:50:19 +02:00
|
|
|
|
|
|
|
if (newmap.all())
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Apply changes to bitmap for next run. This is done so we don't introduce
|
|
|
|
// a bias in color propagation in the direction pixels are processed.
|
|
|
|
newmap.copy(bitmap);
|
|
|
|
|
|
|
|
}
|
2015-03-09 14:32:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Scale a region of an image into another image, using nearest-neighbor with
|
|
|
|
* anti-aliasing; treat pixels as crisp rectangles, but blend them at boundaries
|
|
|
|
* to prevent non-integer scaling ratio artifacts. Note that this may cause
|
|
|
|
* some blending at the edges where pixels don't line up perfectly, but this
|
|
|
|
* filter is designed to produce the most accurate results for both upscaling
|
|
|
|
* and downscaling.
|
|
|
|
*/
|
2015-04-01 06:19:10 +02:00
|
|
|
void imageScaleNNAA(video::IImage *src, const core::rect<s32> &srcrect, video::IImage *dest)
|
|
|
|
{
|
2015-03-09 14:32:11 +01:00
|
|
|
double sx, sy, minsx, maxsx, minsy, maxsy, area, ra, ga, ba, aa, pw, ph, pa;
|
|
|
|
u32 dy, dx;
|
|
|
|
video::SColor pxl;
|
|
|
|
|
2020-06-01 15:22:04 +02:00
|
|
|
// Cache rectangle boundaries.
|
2015-03-09 14:32:11 +01:00
|
|
|
double sox = srcrect.UpperLeftCorner.X * 1.0;
|
|
|
|
double soy = srcrect.UpperLeftCorner.Y * 1.0;
|
|
|
|
double sw = srcrect.getWidth() * 1.0;
|
|
|
|
double sh = srcrect.getHeight() * 1.0;
|
|
|
|
|
|
|
|
// Walk each destination image pixel.
|
|
|
|
// Note: loop y around x for better cache locality.
|
|
|
|
core::dimension2d<u32> dim = dest->getDimension();
|
|
|
|
for (dy = 0; dy < dim.Height; dy++)
|
|
|
|
for (dx = 0; dx < dim.Width; dx++) {
|
|
|
|
|
|
|
|
// Calculate floating-point source rectangle bounds.
|
|
|
|
// Do some basic clipping, and for mirrored/flipped rects,
|
|
|
|
// make sure min/max are in the right order.
|
|
|
|
minsx = sox + (dx * sw / dim.Width);
|
2020-06-01 15:22:04 +02:00
|
|
|
minsx = rangelim(minsx, 0, sox + sw);
|
2015-03-09 14:32:11 +01:00
|
|
|
maxsx = minsx + sw / dim.Width;
|
2020-06-01 15:22:04 +02:00
|
|
|
maxsx = rangelim(maxsx, 0, sox + sw);
|
2015-03-09 14:32:11 +01:00
|
|
|
if (minsx > maxsx)
|
|
|
|
SWAP(double, minsx, maxsx);
|
|
|
|
minsy = soy + (dy * sh / dim.Height);
|
2020-06-01 15:22:04 +02:00
|
|
|
minsy = rangelim(minsy, 0, soy + sh);
|
2015-03-09 14:32:11 +01:00
|
|
|
maxsy = minsy + sh / dim.Height;
|
2020-06-01 15:22:04 +02:00
|
|
|
maxsy = rangelim(maxsy, 0, soy + sh);
|
2015-03-09 14:32:11 +01:00
|
|
|
if (minsy > maxsy)
|
|
|
|
SWAP(double, minsy, maxsy);
|
|
|
|
|
|
|
|
// Total area, and integral of r, g, b values over that area,
|
|
|
|
// initialized to zero, to be summed up in next loops.
|
|
|
|
area = 0;
|
|
|
|
ra = 0;
|
|
|
|
ga = 0;
|
|
|
|
ba = 0;
|
|
|
|
aa = 0;
|
|
|
|
|
|
|
|
// Loop over the integral pixel positions described by those bounds.
|
|
|
|
for (sy = floor(minsy); sy < maxsy; sy++)
|
|
|
|
for (sx = floor(minsx); sx < maxsx; sx++) {
|
|
|
|
|
|
|
|
// Calculate width, height, then area of dest pixel
|
|
|
|
// that's covered by this source pixel.
|
|
|
|
pw = 1;
|
|
|
|
if (minsx > sx)
|
|
|
|
pw += sx - minsx;
|
|
|
|
if (maxsx < (sx + 1))
|
|
|
|
pw += maxsx - sx - 1;
|
|
|
|
ph = 1;
|
|
|
|
if (minsy > sy)
|
|
|
|
ph += sy - minsy;
|
|
|
|
if (maxsy < (sy + 1))
|
|
|
|
ph += maxsy - sy - 1;
|
|
|
|
pa = pw * ph;
|
|
|
|
|
|
|
|
// Get source pixel and add it to totals, weighted
|
|
|
|
// by covered area and alpha.
|
|
|
|
pxl = src->getPixel((u32)sx, (u32)sy);
|
|
|
|
area += pa;
|
|
|
|
ra += pa * pxl.getRed();
|
|
|
|
ga += pa * pxl.getGreen();
|
|
|
|
ba += pa * pxl.getBlue();
|
|
|
|
aa += pa * pxl.getAlpha();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the destination image pixel to the average color.
|
|
|
|
if (area > 0) {
|
|
|
|
pxl.setRed(ra / area + 0.5);
|
|
|
|
pxl.setGreen(ga / area + 0.5);
|
|
|
|
pxl.setBlue(ba / area + 0.5);
|
|
|
|
pxl.setAlpha(aa / area + 0.5);
|
|
|
|
} else {
|
|
|
|
pxl.setRed(0);
|
|
|
|
pxl.setGreen(0);
|
|
|
|
pxl.setBlue(0);
|
|
|
|
pxl.setAlpha(0);
|
|
|
|
}
|
|
|
|
dest->setPixel(dx, dy, pxl);
|
|
|
|
}
|
|
|
|
}
|