Fast image scaling  
Author Message
Beth





PostPosted: 2005-10-5 6:41:00 Top

java-programmer, Fast image scaling Hi all,
I'm writing a program which involves drawing several copies of the same
image inside of a JPanel. I have a listener on the mouse wheel to
"zoom in/out", so that every image scales up or down in size at the
same time. I'm having many problems getting this to run smoothly.

I first tried to scale the image just once (since it is drawn many
times) by calling getScaledInstance, saving the resulting image, and
then drawing that lots of times. However, even though I am using
Image.SCALE_FAST, it sometimes takes a while to update the image when
scaling to larger sizes. (Note, the image is 440x250, and I'm never
scaling it larger than double its size.)

The second thing I tried was to use an AffineTransform to scale the
graphics object just prior to drawing the image. This gave slightly
better results during the actual scaling, but it caused random hitches
(half-second pauses where all movement in the program completely
stops), even when I wasn't actively changing the scale. I finally
deduced that the scaling causes some garbage to be created, and the
hitches happened when the garbage collector ran. (I also tried without
the AffineTransform, and just using the width/height arguments in
drawImage to scale on-the-fly, but the results were the same.) Please
also note that when I don't scale the image at all, the drawing is
perfectly smooth and fast.

What I'm doing doesn't seem to be nearly as intensive as what most
people are capable of doing, so clearly I'm doing something wrong. If
anyone could point me in the right direction, I'd be most appreciative!
Thanks!

 
Boudewijn Dijkstra





PostPosted: 2005-10-5 23:58:00 Top

java-programmer >> Fast image scaling "Beth" <email***@***.com> schreef in bericht
news:email***@***.com...
> Hi all,
> I'm writing a program which involves drawing several copies of the same
> image inside of a JPanel. I have a listener on the mouse wheel to
> "zoom in/out", so that every image scales up or down in size at the
> same time. I'm having many problems getting this to run smoothly.
>
> I first tried to scale the image just once (since it is drawn many
> times) by calling getScaledInstance, saving the resulting image, and
> then drawing that lots of times. However, even though I am using
> Image.SCALE_FAST, it sometimes takes a while to update the image when
> scaling to larger sizes. (Note, the image is 440x250, and I'm never
> scaling it larger than double its size.)

Zooming in is far slower than zooming out. Create a VolatileImage at 2?size
and use that as a master image. Then a create correctly scaled working copy
from that, each time the scale changes.


 
Roedy Green





PostPosted: 2005-10-6 7:53:00 Top

java-programmer >> Fast image scaling On 4 Oct 2005 15:40:38 -0700, "Beth" <email***@***.com> wrote or
quoted :

>What I'm doing doesn't seem to be nearly as intensive as what most
>people are capable of doing, so clearly I'm doing something wrong. If
>anyone could point me in the right direction, I'd be most appreciative!

Here is the code I use for allowing you to magnify images. The slider
lets you magnify smoothly. I am doing nothing special. This code
just uses the magnifying ability of drawImage. You might hook this up
and try out to see if it gives you any better than you are getting
now. If so, compare code to figure out what we are doing differently.


package com.mindprod.image;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.MediaTracker;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.prefs.Preferences;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
* Displays an image.
User can use a slider to make the
* image bigger or smaller.
*
* @author Roedy Green
* @version 1.0
* @since 2004-04-04
*/
public class MagnaViewer extends JFrame
{

/**
* layout
* ----- slider----- save
* ------image-----------
*
* When image is bigger than fits, it has scroll bars.
*/

/**
* Constructor
*
* @param image image to display
* @param minDim minumim size ever display an image.
* @param maxDim maximum size of the magnified image that we
* scroll over.
* @param saveName suggested name to save the file under, without
* directory.
*/
public MagnaViewer( BufferedImage image, Dimension minDim,
Dimension maxDim, String saveName )
{
this.minDim = minDim;
this.maxDim = maxDim;
this.magnifiedDim = maxDim;
this.saveName = saveName;

setImage ( image );
}
private String saveName;
/**
* the smallest we ever draw an image
*/
private Dimension minDim;

/**
* the largest we allow a magnified image to go, don't necessarily
display it all.
*/
private Dimension maxDim;

/**
* The width of the current Image, unmagnified
*/
private int imageWidth;
/**
* the height of the current image, unmagnified.
*/
private int imageHeight;
/**
* The width of the image as painted,
* may be wider than scrollPane
*/
private int paintedImageWidth;

/**
* The height of the image as currently painted,
* may be taller than scrollPane. Used to avoid
* needlessly repainting the image the same size as before.
*/
private int paintedImageHeight;

/**
* the current magnification we are about to use to paint.
*/
private double magnification;

/**
* how wide the magnified image will be.
* May be wider than the scrollPane.
*/
private int magnifiedImageWidth;
/**
* how tall the magnified image will be.
* May be tall than the scrollPane
*/
private int magnifiedImageHeight;

/**
* how wide and tall the magnified image will be.
* May be bigger than the scrollPane
*/
private Dimension magnifiedDim;

/**
* the image we are displaying
*/
private BufferedImage image;
/**
* slider to control magnification.
*/
private JSlider slider;

/**
* save button to save image to disk
*/
private JButton save;

/**
* scrollPane to allow image to be bigger than frame,
* and let us pan over it.
*/
private JScrollPane scrollPane;
/**
* Panel upon which the image is drawn.
*/
private ImagePanel imagePanel;

/**
* Set or change the current Image to display.
* setImage does a MediaTracker to ensure the Image is loaded.
* You don't have to.
* If you don't plan to use the old image again you should
* do a getImage().flush();
*
* @param image the new Image to be displayed.
* If the image jpg may have recently changed, don't use
* getImage to create it, use
* URL.openConnection()
* URLConnection.setUseCaches( false )
* Connection.getContent
* Component.createImage
*
*/
private void setImage( BufferedImage image )
{
// even if Image object is same, we use it since it may have
changed state.

this.image = image;

if ( image != null )
{
MediaTracker tracker;
try
{
// wait until image is fully loaded.
// and so that paint will be instantaneous, rather than
gradual as
// the image arrives.
// MediaTracker notifies of progress via our
Component.ImageObsever interface
tracker = new MediaTracker( this );
tracker.addImage( image, 0 );
tracker.waitForID( 0 );
}
catch ( InterruptedException e )
{
}
}
imageWidth = image.getWidth( this );
imageHeight = image.getHeight( this );

/* force repaint even if same size */
paintedImageWidth = -1;
paintedImageHeight = -1;

setSliderBounds();
sliderMoved();

} // end setImage

/**
* Set the low and high bounds on the slider.
* Set the slider to no magnification.
*/
private void setSliderBounds()
{
if ( slider == null )
{
return;
}
/* calculate the minium and maximum magnification we will use */
/* slider uses 100 to represent 1.00 magnification */
double minMag = Math.max( (double) minDim.width / (double)
imageWidth,
(double) minDim.height /
(double)imageHeight);

double maxMag = Math.min( (double) maxDim.width / (double)
imageWidth,
(double) maxDim.height / (double)
imageHeight);
slider.setMinimum( (int)( minMag*100.0 + .5 ));
slider.setMaximum( (int)( maxMag*100.0 + .5 ));
slider.setValue( 100 /* 1.00 scaled by 100 */ );
}

/**
* The slider moved. Magnification may have changed.
*/
private void sliderMoved()
{
if ( slider == null )
{
magnification = 1;
}
else
{
magnification = slider.getValue() / 100.0;
// 100 -> 1.00 no magnification
}
magnifiedImageWidth = (int)(imageWidth * magnification + 0.5);
magnifiedImageHeight = (int)(imageHeight * magnification + 0.5);
if ( magnifiedImageWidth != paintedImageWidth ||
magnifiedImageHeight != paintedImageHeight )
{
magnifiedDim = new Dimension( magnifiedImageWidth,
magnifiedImageHeight );
if ( scrollPane != null )
{
// if changes size
// make scrollPane rething whethen it needs scrollbars
scrollPane.setVerticalScrollBarPolicy (
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED );
scrollPane.setHorizontalScrollBarPolicy (
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );
scrollPane.revalidate();
scrollPane.repaint();
}
}
}

private void saveImage ()
{
// ask user where to save.
// persisted in registry directory last used
Preferences userPrefs = Preferences.userRoot().node(
"/xxxxx/magnaviewer" );

File suggestedFile = new File (userPrefs.get( "SAVEDIR", "C:" ),
saveName );

JFileChooser fc = new JFileChooser () ;
fc.setSelectedFile( suggestedFile );
// filter out all but JPG file from view
fc.addChoosableFileFilter( new JpgFileFilter() );
int result = fc.showSaveDialog( this );
switch ( result )
{
case JFileChooser.APPROVE_OPTION:
File file = fc.getSelectedFile();
try
{
// magnify which has side effect of turning back to
plain Image.
Image magnifiedImage
= image.getScaledInstance( magnifiedImageWidth,
magnifiedImageHeight,
BufferedImage.SCALE_SMOOTH);

// convert back to BufferedImage
BufferedImage bufferedImage = new BufferedImage (
magnifiedImageWidth,
magnifiedImageHeight,
BufferedImage.TYPE_INT_BGR );
bufferedImage.createGraphics().drawImage(
magnifiedImage, 0, 0, this /* observer */ );

// write out the BufferedImage as a JPEG
ImageIO.write( bufferedImage,
"JPEG" /* format desired */ ,
file );
}
catch ( IOException e )
{
Log.println( Log.NON_FATAL_ERROR, "Image file not saved
successfully." );
}
// save same directory for next time.
// persisted in registry directory last used
userPrefs.put( "SAVEDIR",
fc.getCurrentDirectory().getAbsolutePath() );
break;
case JFileChooser.CANCEL_OPTION:
case JFileChooser.ERROR_OPTION:
break;
default:
}
}
/**
* second stage of initialisation.
*/
public void addNotify()
{
super.addNotify();

Container contentPane = this.getContentPane();
contentPane.setLayout( new GridBagLayout() );

// dummy, will be set to better values. as soon as image size
known.
slider = new JSlider( JSlider.HORIZONTAL, 10 /* low */ , 300 /*
high */ , 100 /* init */);
slider.setPaintTicks( false );
slider.setPaintLabels( false );
slider.setPaintTrack( false );

save = ResourceGetter.createTransparentGifButton( "save", this
);

imagePanel = new ImagePanel();

// default uses bars as needed.
scrollPane = new JScrollPane( imagePanel );

// x y w h wtx
wty anchor fill T L
B R padx pady
contentPane.add( slider, new GridBagConstraints( 0, 0, 1, 1,
1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.BOTH, new
Insets( 2, 4, 2, 2 ), 0, 0 ) );

// x y w h wtx
wty anchor fill T L
B R padx pady
contentPane.add( save, new GridBagConstraints( 1, 0, 1, 1, 0.0,
0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 2,
4, 2, 2 ), 0, 0 ) );

// x y w h
wtx wty anchor fill T L B R padx pady
contentPane.add( scrollPane, new GridBagConstraints( 0, 1, 2, 1,
1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new
Insets( 2, 4, 2, 2 ), 0, 0 ) );

hookListeners();
setSliderBounds();
sliderMoved();
validate();
}

/**
* attach various listeners.
*/
private void hookListeners()
{
hookSlider();
hookSaveButton();
hookWindowClosing();
}

/**
* hook up listener for slider
*/
private void hookSlider()
{
// user changed the slider
ChangeListener sliderListener = new ChangeListener()
{
/**
* Invoked when the target of the listener has changed its
state.
*
* @param event not used
*/
public void stateChanged( ChangeEvent event )
{
sliderMoved();
}
};
slider.addChangeListener( sliderListener );
}

/**
* hook up listener for save button
*/
private void hookSaveButton()
{
ActionListener saveListener = new ActionListener()
{
/**
* invoked when save button hit.
*
* @param e event used to figure out which button was
pressed.
*/
public void actionPerformed( ActionEvent e )
{
saveImage();
}
};
save.addActionListener( saveListener );
}

/**
* create and attach anonymous inner class event handler for window
closing.
*/
private void hookWindowClosing()
{
this.setDefaultCloseOperation( JFrame.DO_NOTHING_ON_CLOSE );
// what happens when user closes the LiveFeedFrame.
WindowListener windowListener = new WindowAdapter()
{
/**
* anonymous WindowAdapter class
*
* @param w not used
*/
public void windowClosing ( WindowEvent w )
{
if ( image != null )
{
image.flush();
}
MagnaViewer.this.dispose();

} // end windowClosing
}; // end anonymous class

this.addWindowListener( windowListener );
} // end hookWindowClosing

/**
* inner class panel with just the image
*
* @author Roedy Green
* @version 1.0
* @since 2004-05-04
*/
class ImagePanel extends JPanel
{
/**
* custom paint, draws the Image
*
* @param g graphics representing screen where image
displayed.
*/
public void paintComponent ( Graphics g )
{
super.paintComponent( g );
/* If we overflow, no problem, drawImage will clip. */
// this does not complete the job, just starts it.
// We are notified of progress through our Component
ImageObserver interface.
Dimension dim = getSize();
// centre in panel. If too big to fit put it upper left
corner and let
// scroll deal with the slop.
g.drawImage ( image, /* Image to
draw */
Math.max( 0, ( dim.width - magnifiedImageWidth
) / 2), /* x */
Math.max( 0, ( dim.height -
magnifiedImageHeight ) / 2), /* y */
magnifiedImageWidth, /* width */
magnifiedImageHeight, /* height */
this ); /* this
ImagePanel component */
paintedImageWidth = magnifiedImageWidth;
paintedImageHeight = magnifiedImageHeight;
}

/**
* Preferred Layout size.
*
* @return the recommended dimensions to display the Image.
*/
public Dimension getPreferredSize()
{
return magnifiedDim;
}

/**
* Minimum Layout size.
*
* @return the recommended dimensions to display the Image.
*/
public Dimension getMinimumSize()
{
return magnifiedDim;
}

/**
* Maximum Layout size.
*
* @return the recommended dimensions to display the Image.
*/
public Dimension getMaximumSize()
{
return magnifiedDim;
}

} // end ImagePanel

}
--
Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.
 
 
Roedy Green





PostPosted: 2005-10-6 8:51:00 Top

java-programmer >> Fast image scaling On Wed, 5 Oct 2005 17:58:06 +0200, "Boudewijn Dijkstra"
<email***@***.com> wrote or quoted :

>Zooming in is far slower than zooming out. Create a VolatileImage at 2?size
>and use that as a master image. Then a create correctly scaled working copy
>from that, each time the scale changes.

Zooming in means magnifiying as if you were getting closer to the
image. It has to create new pixels by interpolation. Zooming out is
easier. You are dropping data.
--
Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.
 
 
Roedy Green





PostPosted: 2005-10-6 9:00:00 Top

java-programmer >> Fast image scaling On Wed, 5 Oct 2005 17:58:06 +0200, "Boudewijn Dijkstra"
<email***@***.com> wrote or quoted :

>Zooming in is far slower than zooming out. Create a VolatileImage at 2?size
>and use that as a master image. Then a create correctly scaled working copy
>from that, each time the scale changes.

You actually have to create two images, a VolatileImage and backup to
reconstruct the Volatile image in case it is lost. One of the
annoyances of a VolatileImage is the video card hardware might at any
time decide it needs its RAM for a higher priority purpose.

VolatileImage is the ultimate in speed since the image is stored the
proprietary format of the video card inside the video card and has
access to the image processing hardware in the card for copying or
magnifying. Some cards have 512-bit processors, and bit
addressability compared with 32-bit for a Pentium.

I did one program that way for smooth scrolling of a giant video
display for the Las Vegas Hilton. Ironically, to create the illusion
of majestic smoothness, you need to be very fast.
--
Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.
 
 
Beth





PostPosted: 2005-10-7 1:29:00 Top

java-programmer >> Fast image scaling Thanks so much for your help everyone.
I've tried the VolatileImage method, and it seemed to be faster, until
I created the VolatileImage with transparency... then my garbage
collection pauses came back for some odd reason.
I also tried out the demo code, Roedy, and it was blindingly fast. I
haven't found anything in the code that looks very different from mine,
so now I am starting to worry that my garbage collection problems may
be caused by something that isn't image-scaling related (although
changing image scaling methods does affect it...).
Another note: I was testing all this code (including Roedy's) on a
Linux AMD64 machine. When I tried it on my Win2k machine, my garbage
collection problems disappeared. More investigation seems necessary.
Thank you!

 
 
Roedy Green





PostPosted: 2005-10-7 3:21:00 Top

java-programmer >> Fast image scaling On 6 Oct 2005 10:28:33 -0700, "Beth" <email***@***.com> wrote or
quoted :

> More investigation seems necessary.
>Thank you!

There are profilers to help you sort this out. It make be some
platform specific code is "frothing" -- spitting out millions of tiny
objects.

http://mindprod.com/jgloss/profiler.html
--
Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.
 
 
Thomas Fritsch





PostPosted: 2005-10-7 19:51:00 Top

java-programmer >> Fast image scaling Roedy Green wrote:
> There are profilers to help you sort this out. It make be some
> platform specific code is "frothing" -- spitting out millions of tiny
> objects.
Ironically BufferedImage.getRGB(int x, int y) itself is such a "frothing
monster". It creates a tiny temporary pixel object (typically a new
byte[3] array in case of RGB image) each time it is called.
I found this, when I profiled an image processing application of mine,
which made heavy use of getRGB(x,y).

My work-around was to avoid
int rgb = image.getRGB(x, y);

but instead define a member variable
private Object pixelData = null;
and to reuse that object for each pixel of the image
pixelData = image.getRaster().getDataElements(x, y, pixelData);
int rgb = image.getColorModel().getRGB(pixelData);

I got an incredible performance-boost this way.

--
"Thomas:Fritsch$ops:de".replace(':','.').replace('$','@')

 
 
Andrey Kuznetsov





PostPosted: 2005-10-8 2:52:00 Top

java-programmer >> Fast image scaling > My work-around was to avoid
> int rgb = image.getRGB(x, y);
>
> but instead define a member variable
> private Object pixelData = null;
> and to reuse that object for each pixel of the image
> pixelData = image.getRaster().getDataElements(x, y, pixelData);
> int rgb = image.getColorModel().getRGB(pixelData);
>
> I got an incredible performance-boost this way.

really good idea!

Thanks Thomas!

--
Andrey Kuznetsov
http://uio.imagero.com Unified I/O for Java
http://reader.imagero.com Java image reader
http://jgui.imagero.com Java GUI components and utilities


 
 
Roedy Green





PostPosted: 2005-10-8 9:17:00 Top

java-programmer >> Fast image scaling On Fri, 07 Oct 2005 11:51:01 GMT, Thomas Fritsch
<email***@***.com> wrote or quoted :

>My work-around was to avoid
> int rgb = image.getRGB(x, y);
>
>but instead define a member variable
> private Object pixelData = null;
>and to reuse that object for each pixel of the image
> pixelData = image.getRaster().getDataElements(x, y, pixelData);
> int rgb = image.getColorModel().getRGB(pixelData);

thanks Thomas. I have used that code as an example of frothing and
what you can do about it at http://mindprod.com/jgloss/frothing.html
--
Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.