Avatar of Crazy_Bytes
Crazy_BytesFlag for Germany asked on

How to optimize Image loading?

Hi everyone,

i wrote an application to create and to manage websites. The app also contains a module to generate and display galeries. I use threads to load each image and join them so that only one image will be loaded at the same time. The images are resized and displayed as thumbnails.

As long as the images are small (about 40 kBytes(.jpg-Format)) with an average size of 640 x 480 pixel with the default memory settings for the JVM,  there are no problems no matter how large a galery is.

But when I use high res images with about 5 MBytes and an average size of 2400 x 1800 pixel or even higher, the load process of the galery crashes after the 10th to 20th picture even when I give the JVM a maximum memory between 512 MBytes and 2048 MBytes.

I create an instance of the image through a buffered image and create a scaled instance into an ImageIcon. After this I dispose the buffered image and set the buffered image to null and manually call the garbage collector afterwards. Due to the gc call the load time increased.

Has anybody a hint how to optimize the load process?

Currently I am not at my developer pc, when i get back to it I will post the loading routine to ease assistance.

Thanks and greetings,
CB

Java

Avatar of undefined
Last Comment
CEHJ

8/22/2022 - Mon
SOLUTION
Dejan Pažin

Log in or sign up to see answer
Become an EE member today7-DAY FREE TRIAL
Members can start a 7-Day Free trial then enjoy unlimited access to the platform
Sign up - Free for 7 days
or
Learn why we charge membership fees
We get it - no one likes a content blocker. Take one extra minute and find out why we block content.
See how we're fighting big data
Not exactly the question you had in mind?
Sign up for an EE membership and get your own personalized solution. With an EE membership, you can ask unlimited troubleshooting, research, or opinion questions.
ask a question
ASKER CERTIFIED SOLUTION
CEHJ

Log in or sign up to see answer
Become an EE member today7-DAY FREE TRIAL
Members can start a 7-Day Free trial then enjoy unlimited access to the platform
Sign up - Free for 7 days
or
Learn why we charge membership fees
We get it - no one likes a content blocker. Take one extra minute and find out why we block content.
See how we're fighting big data
Not exactly the question you had in mind?
Sign up for an EE membership and get your own personalized solution. With an EE membership, you can ask unlimited troubleshooting, research, or opinion questions.
ask a question
ASKER
Crazy_Bytes

First of thanks for reply! :)

@dejanpazin: There are no problems with the thread handling. I changed the image loading into a for loop,
so that the app is forced to load images one by one for testing.The result was the same.

@CEHJ: The gallery module is meant for creating galleries. So if you want to create one, you specify a source directory with images (preferably a local hard drive or mount in a LAN). After that the module creates a preview of all images inside the source directory. Then you cnn select or deselect images and alter settings for the gallery. If everything is to your liking, the module creates the gallery, resizing the images and writing them to target directory, containing the page displaying the images. Afterwards you can publish you gallery by transferring your gallery  to your web folder.

I attached a test class that uses the same loading algorhythm (loadImage(int)) like my galery module. I hope this helps to ease assistance.

Thanks and greeting,
CB
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
 
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
 
/**
 * This test class creates an online catalog.
 * 
 * @author CB, crazy-bytes.com (C) 2007
 * @version 1.4
 * @since JDK / JRE 1.5
 */
@SuppressWarnings("serial")
public class TestCatalog 
	extends JPanel 
	implements ActionListener {
	// Catalog root directory
	private static final String CAT_ROOT_DIR = "C:\\Temp\\TestCatalog\\";
	// Catalog image directory
	private static final String CAT_IMG_DIR = CAT_ROOT_DIR + "images\\";
	private static final DecimalFormat THREE_DIGITS = new DecimalFormat("000");
	private static final int MAX_WIDTH = 379;
	private static final int MAX_HEIGHT = 539;
	private static final int MAX_PAGE = getMaximumPage();
 
	private JLabel lbl_left_page = new JLabel();
	private JLabel lbl_right_page = new JLabel();
	private JLabel lbl_left_index = new JLabel();
	private JLabel lbl_right_index = new JLabel();
	private JLabel lbl_goto = new JLabel("Go to page");
	private JTextField txt_goto = new JTextField(3);
	
	private JButton btn_prev = new JButton("previous");
	private JButton btn_next = new JButton("next");
	private JButton btn_goto = new JButton("\u00bb"); // ">>" character
	private int current_page = 0;
	
	public TestCatalog() {
		init();
	}
	
	private void init() {
		setLayout(new BorderLayout(0, 0));
		
		btn_next.addActionListener(this);
		btn_prev.addActionListener(this);
		
		// Create top navi bar
		JPanel pnl_top_nav = new JPanel(new BorderLayout(5, 5));
		pnl_top_nav.add(btn_prev, BorderLayout.WEST);
		pnl_top_nav.add(btn_next, BorderLayout.EAST);
		
		JPanel pnl_pages = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
		pnl_pages.add(lbl_left_page);
		pnl_pages.add(lbl_right_page);
		
		// Create bottom navi bar
		JPanel pnl_goto = new JPanel(new FlowLayout(FlowLayout.CENTER));
		pnl_goto.add(lbl_goto);
		pnl_goto.add(txt_goto);
		pnl_goto.add(btn_goto);
		
		JPanel pnl_bottom_nav = new JPanel(new BorderLayout());
		pnl_bottom_nav.add(lbl_left_index, BorderLayout.WEST);
		pnl_bottom_nav.add(pnl_goto, BorderLayout.CENTER);
		pnl_bottom_nav.add(lbl_right_index, BorderLayout.EAST);
		
		// Create display
		Dimension dim = new Dimension(MAX_WIDTH, MAX_HEIGHT);
		lbl_left_page.setPreferredSize(dim);
		lbl_right_page.setPreferredSize(dim);
		
		add(pnl_top_nav, BorderLayout.NORTH);
		add(pnl_pages, BorderLayout.CENTER);
		add(pnl_bottom_nav, BorderLayout.SOUTH);
		
		loadPage(current_page);
	}
	
	/**
	 * Loads the desired page and its opposite page. If the desired
	 * page number is invalid nothing happens. Even pages are
	 * displayed left and odd pages right.
	 * 
	 * @param page the page that shall be displayed
	 */
	private void loadPage(int page) {
		// Ignore imvalid pages.
		if(page < 0 || page > MAX_PAGE) {
			return;
		}
		
		if(page % 2 == 0) {
			loadPage(lbl_left_page, lbl_left_index, page);
			loadPage(lbl_right_page, lbl_right_index, page + 1);
		}
		else {
			loadPage(lbl_left_page, lbl_left_index, page - 1);
			loadPage(lbl_right_page, lbl_right_index, page);
		}
		
		current_page = page;
	}
	
	/**
	 * Loads a desired page into the desired label. The index of the page is
	 * display in the index label.
	 * 
	 * @param page_display the label that shall display the desired page
	 * @param index_display the label that displays the page number
	 * @param page the desired page that shall be displayed
	 */
	private void loadPage(JLabel page_display, JLabel index_display, int page) {
		try {
			page_display.setText("");
			if(page > MAX_PAGE) {
				page_display.setIcon(null);
				index_display.setText("");
			}
			else {
				page_display.setIcon(loadImage(page));
				index_display.setText("Page " + page + " of " + MAX_PAGE);
			}
		}
		catch(Exception e) {
			page_display.setText("An error occured whil loading page " + page + ".");
			e.printStackTrace();
		}
	}
	
	private Icon loadImage(int page) throws IOException {
		File file = new File(CAT_IMG_DIR + THREE_DIGITS.format(page) + ".jpg");
		BufferedImage bi = ImageIO.read(file);
		int width = bi.getWidth(null);
		int height = bi.getHeight(null);
		int new_width;
		int new_height;
		// Default ratio is 1 (equals 100% of the image size) which results in no
		// resizing.
		double ratio = 1;
		// If the image is larger than the maximum bounds resize it to fit, but 
		// keep ratio between width and height to avoid distortion.
		if (width > MAX_WIDTH &&
			height > MAX_HEIGHT) {
			// If width > height use width to resize to fit MAX_WIDTH.
			if (width > height) {
				ratio = MAX_WIDTH * 100 / width;
			}
			// Else use height to resize to fit MAX_HEIGHT.
			else {
				ratio = MAX_HEIGHT * 100 / height;
			}
		}
		else if (width > MAX_WIDTH) {
			ratio = MAX_WIDTH * 100 / width;
		}
		else if (height > MAX_HEIGHT) {
			ratio = MAX_HEIGHT * 100 / height;
		}
		
		Icon ico = null;
		
		// If resizing is needes, create a scaled instance.
		if(ratio != 1) {
			// Calculate new image size
			new_width = (int) Math.round(width * ratio);
			new_height = (int) Math.round(height * ratio);
 
			// WORK AROUND
			// Set new width and height to 1 since sometimes the BufferedImage filter
			// is not able to read the width and height right and returns 0, which can
			// cause a division by zero error when scaling to the new width and height.
			new_width = new_width > 0 ? new_width : 1;
			new_height = new_height > 0 ? new_height : 1;
			ico = new ImageIcon(bi.getScaledInstance(new_width, new_height, Image.SCALE_DEFAULT));
		}
		// Else just return the image.
		else {
			ico = new ImageIcon(bi);
		}
		
		// Free resources and return the image.
		bi.flush();
		bi = null;
		System.gc();
		return ico;
	}
 
	@Override
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == btn_prev) {
			loadPage(current_page - 2);
		}
		if(e.getSource() == btn_next) {
			loadPage(current_page + 2);
		}
	}
 
	/**
	 * Every page is indexed from 000 to nnn. Since the page amount can vary
	 * from catalog to catalog, this methode always determines the highest
	 * page number when this class is invoked.
	 * 
	 * @return the highest page number of the catalog
	 */
	private static int getMaximumPage() {
		int max_page = 0;
		File img_dir = new File(CAT_IMG_DIR);
		if (img_dir.exists() &&
			img_dir.isDirectory()) {
			File[] files = img_dir.listFiles();
			for(File file : files) {
				if (file.isFile() &&
				    file.getName().matches("\\d\\d\\d.jpg")) {
					int num = Integer.parseInt(file.getName().substring(0, 3));
					if(num > max_page) max_page = num;
				}
			}
		}
		System.out.println(max_page);
		return max_page;
	}
 
	public static void main(String[] args) {
		final JFrame f = new JFrame("TestCatalog");
		f.getContentPane().add(new TestCatalog());
		f.addWindowListener(new WindowAdapter() {
		    public void windowClosing(WindowEvent windowEvent) {
			f.dispose();
			System.exit(0);
		    }
		});
		f.pack();
		f.setResizable(false);
	
		// Go!!!
		f.setVisible(true);
	}
}

Open in new window

ASKER
Crazy_Bytes

I am thinking about to change loadImage(int), so that it always cuts a tile of the original image and resizes it.
After this the tiles are rejoined. Maybe this will improve performance and memory usage.

I will post a new snippet if I succeed the task.

Greetings,
CB
CEHJ

Sorry about that - i was thinking Web for some reason ;-)

Can you tell me - does it happen if getScaledInstance is not called - even with large images?
This is the best money I have ever spent. I cannot not tell you how many times these folks have saved my bacon. I learn so much from the contributors.
rwheeler23
ASKER
Crazy_Bytes

When I load the images just like this:
      ...
      File file = new File(CAT_IMG_DIR + THREE_DIGITS.format(page) + ".jpg");
      BufferedImage bi = ImageIO.read(file);
      Icon ico = new ImageIcon(bi);
      ...
and skip the getScaledInstanc(int, int, int) step, the load process is much shortened and the java heap space error occurs later than when I use scaling. I tried some images that are smaller than the originals and larger than my test images. When ever I use getScaledInstance(int, int, int) the memory usage in the task manager bloats up.
CEHJ

Try it this way and see if you get on any better:

http://www.exampledepot.com/egs/java.awt.image/CreateTxImage.html
ASKER
Crazy_Bytes

I switched to AffineTransform, but it's like using BufferedImage.getScaledInstance. There is also a java heap space error.
Get an unlimited membership to EE for less than $4 a week.
Unlimited question asking, solutions, articles and more.
CEHJ

You might try a more specialized imaging library for doing the scaling such as JAI
ASKER
Crazy_Bytes

I found some major calculating errors

When calculating the ratio (line 160 to 176 in my 1st code snippet), I multiplied the desired amount with 100 which returns a percentage > 1. so instead of downsizing an image I upsized it. which was the root cause för the memory to bloat up.

The second minor error was also during the calculation of the ratio. Since MAX_WIDTH, MAX_HEIGHT, width and height are all integers, the calculation result is also an integer. So the ratio was e.g somthing like 44.0, 0.0, etc(no values after the point).

After doing this, the code for loadImage(int) looks like my 2nd code snippet and works fine, but is still memory intensive.
	private Icon loadImage(int page) throws IOException {
		File file = new File(CAT_IMG_DIR + THREE_DIGITS.format(page) + ".jpg");
		BufferedImage bi = ImageIO.read(file);
		int width = bi.getWidth(null);
		int height = bi.getHeight(null);
		int new_width;
		int new_height;
		// Default ratio is 1 (equals 100% of the image size) which results in no
		// resizing.
		double ratio = 1;
		// If the image is larger than the maximum bounds resize it to fit, but 
		// keep ratio between width and height to avoid distortion.
		if (width > MAX_WIDTH &&
			height > MAX_HEIGHT) {
			// If width > height use width to resize to fit MAX_WIDTH.
			if (width > height) {
				ratio = MAX_WIDTH / (double) width;
			}
			// Else use height to resize to fit MAX_HEIGHT.
			else {
				ratio = MAX_HEIGHT / (double) height;
			}
		}
		else if (width > MAX_WIDTH) {
			ratio = MAX_WIDTH / (double) width;
		}
		else if (height > MAX_HEIGHT) {
			ratio = MAX_HEIGHT / (double) height;
		}
		
		Icon ico = null;
		
		// If resizing is needes, create a scaled instance.
		if(ratio != 1) {
			// Calculate new image size
			new_width = (int) Math.round(width * ratio);
			new_height = (int) Math.round(height * ratio);
 
			// WORK AROUND
			// Set new width and height to 1 since sometimes the BufferedImage filter
			// is not able to read the width and height right and returns 0, which can
			// cause a division by zero error when scaling to the new width and height.
			new_width = new_width > 0 ? new_width : 1;
			new_height = new_height > 0 ? new_height : 1;
			ico = new ImageIcon(bi.getScaledInstance(new_width, new_height, Image.SCALE_DEFAULT));
		}
		// Else just return the image.
		else {
			ico = new ImageIcon(bi);
		}
		
		// Free resources and return the image.
		bi.flush();
		bi = null;
		System.gc();
		return ico;
	}
 
	@Override
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == btn_prev) {
			loadPage(current_page - 2);
		}
		if(e.getSource() == btn_next) {
			loadPage(current_page + 2);
		}
	}
 
	/**
	 * Every page is indexed from 000 to nnn. Since the page amount can vary
	 * from catalog to catalog, this methode always determines the highest
	 * page number when this class is invoked.
	 * 
	 * @return the highest page number of the catalog
	 */
	private static int getMaximumPage() {
		int max_page = 0;
		File img_dir = new File(CAT_IMG_DIR);
		if (img_dir.exists() &&
			img_dir.isDirectory()) {
			File[] files = img_dir.listFiles();
			for(File file : files) {
				if (file.isFile() &&
				    file.getName().matches("\\d\\d\\d.jpg")) {
					int num = Integer.parseInt(file.getName().substring(0, 3));
					if(num > max_page) max_page = num;
				}
			}
		}
		System.out.println(max_page);
		return max_page;
	}
 
	public static void main(String[] args) {
		final JFrame f = new JFrame("TestCatalog");
		f.getContentPane().add(new TestCatalog());
		f.addWindowListener(new WindowAdapter() {
		    public void windowClosing(WindowEvent windowEvent) {
			f.dispose();
			System.exit(0);
		    }
		});
		f.pack();
		f.setResizable(false);
	
		// Go!!!
		f.setVisible(true);
	}
}

Open in new window

ASKER
Crazy_Bytes

I finished changing the loadImage(int) method to tile resizing. Now the method takes a little longer to work, but is much less memory intensive.

See my third code snippet below.

Greetings,
CB
	private Icon loadImage(int page) throws IOException {
		File file = new File(CAT_IMG_DIR + THREE_DIGITS.format(page) + ".jpg");
		BufferedImage img = ImageIO.read(file);
		int width = img.getWidth(null);
		int height = img.getHeight(null);
		// Default ratio is 1 (equals 100% of the image size) which results in no
		// resizing.
		double ratio = 1;
		// If the image is larger than the maximum bounds resize it to fit, but 
		// keep ratio between width and height to avoid distortion.
		if (width > MAX_WIDTH &&
			height > MAX_HEIGHT) {
			// If width > height use width to resize to fit MAX_WIDTH.
			if (width > height) {
				ratio = MAX_WIDTH / (double) width;
			}
			// Else use height to resize to fit MAX_HEIGHT.
			else {
				ratio = MAX_HEIGHT / (double) height;
			}
		}
		else if (width > MAX_WIDTH) {
			ratio = MAX_WIDTH / (double) width;
		}
		else if (height > MAX_HEIGHT) {
			ratio = MAX_HEIGHT / (double) height;
		}
		
		Icon ico = null;
		
		// If resizing is needes, create a scaled instance.
		if(ratio != 1) {
			// Define tile size for each tile of the original image, so
			// each tile has a size of tile_size x tile_size pixels 
			int tile_size = 50;
			// Calculate new image size
			int new_width = (int) Math.round(width * ratio);
			int new_height = (int) Math.round(height * ratio);
			// WORK AROUND
			// Set new width and height to 1 since sometimes the BufferedImage filter
			// is not able to read the width and height right and returns 0, which can
			// cause a division by zero error when scaling to the new width and height.
			new_width = new_width > 0 ? new_width : 1;
			new_height = new_height > 0 ? new_height : 1;
 
			BufferedImage new_img = new BufferedImage(new_width, new_height, BufferedImage.TYPE_INT_RGB);
			Graphics2D g = new_img.createGraphics();
			// Count tiles along x and y axis
			int y_tile_count = (int) Math.ceil(img.getHeight(null) / tile_size);
			int x_tile_count = (int) Math.ceil(img.getWidth(null) / tile_size);
			int new_tile_size = (int) Math.round(tile_size * ratio);
			// Read each tile and resize it accordingly.
			for(int y = 0; y < y_tile_count; y++) {
				for(int x = 0; x < x_tile_count; x++) {
					int w = x < x_tile_count - 1 ? tile_size : width - (x * tile_size);
					int h = y < y_tile_count - 1 ? tile_size : height - (y * tile_size);
					// Read tile.
					BufferedImage tile = img.getSubimage(x * tile_size, y * tile_size, w, h);
					// Resize it.
					w = (int) Math.round(w * ratio);
					h = (int) Math.round(h * ratio);
					Image scaled_tile = tile.getScaledInstance(w, h, BufferedImage.SCALE_SMOOTH);
					// Free resources.
					tile.flush();
					// write tile into new output image.
					g.drawImage(scaled_tile, x * new_tile_size, y * new_tile_size, null);
				}
			}
			ico = new ImageIcon(new_img);
			new_img.flush();
		}
		// Else just return the image.
		else {
			ico = new ImageIcon(img);
		}
		
		// Free resources and return the image.
		img.flush();
		img = null;
		return ico;
	}

Open in new window

I started with Experts Exchange in 2004 and it's been a mainstay of my professional computing life since. It helped me launch a career as a programmer / Oracle data analyst
William Peck
ASKER
Crazy_Bytes

Since I fixed the problem myself, i decided to split the points accordingly to the participation. There were 5 replies, so there will be 25 points per reply. 1 reply from dejanpazin equals 25 points and 4 replies from CEHJ equals 100 points.

Thanks for your help and greetings,
CB
CEHJ

Glad you've got an improvement. Still may be worth trying other libraries