Problems with downsampling images


#1

Hi everyone,

I am trying to download some 256x256 ARGB pngs to 32x32 (or similar low resolution, not necessarily proportional).

I used first Image::rescaled(int newWidth, int newHeight, Graphics::highResamplingQuality) (I already know there is no difference between Graphics::highResamplingQuality and Graphics::mediumResamplingQuality when referring to downsample images)

and then I applied some kind of blurring or Anti Aliasing with ImageConvolutionKernel::createGaussianBlur(0.5) because the oblique edges were so sharp.

The result is OK, but it is far below the one achived taking the original PNGs, downsample using Gimp, or other program (using linear or cubic interpolation) and putting into the program without rescaling.

It seems that downsampling with Image::rescaled(int newWidth, int newHeight, Graphics::highResamplingQuality) does not use any “neighbour” pixels to soften edges.

Any help?

Thanks,

Oscar


#2

care to post some comparison images? Without comparison images I can’t guess what you’re objecting to, and different image types will give different resultsL resizing photos != resizing icons != resizing text.

(Now I’m sure you’re aware of this, but your post above is ambiguous: you’re doing the gaussian before the down-sample right?)

The JUCE native stuff is, I suspect, heavily prioritized for speed. I don’t use the inbuilt resampling much anymore because I tend to live in greyscale right up until presentation. Last time I looked, which was probably around 1.4, bilinear was the high quality mode, and in nearly all cases that’s good enough. More importantly, JUCE was capable of resizing 512x512 images to around 1000x1000 comfortably above 20fps even on the old Athlon 1.1GHz I was using at the time.

You can of course roll your own bi-cubic, but depending on the source material Lanczos might give better results. Like I say, it all comes down to the properties of the source material, and how realtime the rendering needs to be. If you’re resizing icons, well don’t; that’s what an image editor is for. If you’re showing UI thumbnails, try Lanczos[1]. If you’re showing photo thumbnails, go bi-cubic, or throw a sharpening or edge enhancement filter[2] over your gaussian/resampled image, which gets you in the same ball park as the more exotic algorithms anyway.

[1] In most implementations Lanczos has a tendency to edge enhance. For source material that has lots of high contrast edges, this could be beneficial.
[2] since you already have the gaussian an unsharp mask is the easiest to implement too.


#3

If you’re reducing image size, and if speed isn’t important, then a simple trick is to do it in multiple stages - i.e. repeatedly reduce its size by no more than half, until it reaches the size you want.


#4

Hi,

I was thinking to do that trick when I saw that reducing at half size keeps quality perfect, just before seeing Jules’ answer. And speed is not a problem.
I am lucky because I was thinking to implement and entire algorithm. I am sorry I did not tell that all images were icons.

Thanks to all,

Oscar


#5

This one: http://code.google.com/p/imageresampler/ makes a good downsampler for images, especially with the lanczos3 kernel (and it also deals with gamma correction, as explained here: http://www.4p8.com/eric.brasseur/gamma.html ). Using it with juce is quite straightforward, I’ll share the code if anyone is interested


#6

I am!


#7

Here it is – I have put all the imageresampler code in a ‘resampler’ namespace , so you may have to remove a few ‘resampler::’ in the code below to get it to build:

[code]juce::Image resizeImage(const Image &src, int dst_width, int dst_height, const char *filter_name, float source_gamma)
{
int n = 1;
if (src.isRGB()) n = 3; else if (src.isARGB()) n = 4; else jassert(src.isSingleChannel());

const int max_components = 4;
int src_width = src.getWidth(), src_height = src.getHeight();

if (std::max(src_width, src_height) > RESAMPLER_MAX_DIMENSION || n > max_components) {
printf(“Image is too large!\n”);
return Image::null;
}

Image dest(src.getFormat(), dst_width, dst_height, false);

// Filter scale - values < 1.0 cause aliasing, but create sharper looking mips.
const float filter_scale = 1.0f;//.75f;

float srgb_to_linear[256];
for (int i = 0; i < 256; ++i) {
float v = i * 1.0f/255.0f;
srgb_to_linear[i] = (float)pow(v, source_gamma);
}

const int linear_to_srgb_table_size = 4096;
unsigned char linear_to_srgb[linear_to_srgb_table_size];

const float inv_linear_to_srgb_table_size = 1.0f / linear_to_srgb_table_size;
const float inv_source_gamma = 1.0f / source_gamma;

for (int i = 0; i < linear_to_srgb_table_size; ++i)
{
int k = (int)(255.0f * pow(i * inv_linear_to_srgb_table_size, inv_source_gamma) + .5f);
if (k < 0) k = 0; else if (k > 255) k = 255;
linear_to_srgb[i] = (unsigned char)k;
}

resampler::Resampler* resamplers[max_components];
std::vector samples[max_components];

for (int i = 0; i < n; i++) {
resamplers[i] = new resampler::Resampler(src_width, src_height, dst_width, dst_height,
resampler::Resampler::BOUNDARY_CLAMP, 0.0f, 1.0f, filter_name,
i == 0 ? 0 : resamplers[0]->get_clist_x(),
i == 0 ? 0 : resamplers[0]->get_clist_y(), filter_scale, filter_scale);
samples[i].resize(src_width);
}

Image::BitmapData bm_src(src, Image::BitmapData::readOnly);
Image::BitmapData bm_dst(dest, Image::BitmapData::writeOnly);

int dst_y = 0;

//cerr << "Resampling to " << dst_width << “x” << dst_height << “\n”;

for (int src_y = 0; src_y < src_height; src_y++) {
const unsigned char* pSrc = bm_src.getLinePointer(src_y);
for (int x = 0; x < src_width; x++) {
if (n == 4) {
PixelARGB &p = (PixelARGB)(pSrc);
p.unpremultiply();
samples[0][x] = srgb_to_linear[p.getBlue()];
samples[1][x] = srgb_to_linear[p.getGreen()];
samples[2][x] = srgb_to_linear[p.getRed()];
samples[3][x] = p.getAlpha()* (1.0f/255.0f);
pSrc += 4;
} else {
for (int c = 0; c < n; c++) {
samples[c][x] = srgb_to_linear[*pSrc++];
}
}
}

 for (int c = 0; c < n; c++) {
   if (!resamplers[c]->put_line(&samples[c][0])) {
     printf("Out of memory!\n");
     return Image::null;
   }
 }         
 for ( ; ; ) {
   int c;
   for (c = 0; c < n; c++) {
     const float* pOutput_samples = resamplers[c]->get_line();
     if (!pOutput_samples)
       break;
  
     const bool alpha_channel = (c == 3) || ((n == 2) && (c == 1));
     assert(dst_y < dst_height);
     unsigned char* pDst = bm_dst.getLinePointer(dst_y) + c;
        
     for (int x = 0; x < dst_width; x++) {
       if (alpha_channel) {
         int c = (int)(255.0f * pOutput_samples[x] + .5f);
         if (c < 0) c = 0; else if (c > 255) c = 255;
         *pDst = (unsigned char)c;
       } else {
         int j = (int)(linear_to_srgb_table_size * pOutput_samples[x] + .5f);
         if (j < 0) j = 0; else if (j >= linear_to_srgb_table_size) j = linear_to_srgb_table_size - 1;
         *pDst = linear_to_srgb[j];
       }
       pDst += n;
     }
   }     
   if (c < n)
     break; 
   
   if (n == 4) {
     PixelARGB* p = (PixelARGB*)bm_dst.getLinePointer(dst_y);
     for (int x = 0; x < dst_width; x++) {
       p[x].premultiply();
     }
   }

   dst_y++;
 }

}
return dest;
}
[/code]