// Workshop 2 part 2 // This program extends workshop 1 part 3 // Here, we are experimenting with convolution filters. // The program lets the user open an image, // displaying the original on the left, and the result on the right. // The user can specify the filter's kernel, and see the results. import javax.swing.*; import java.awt.*; import java.io.*; import java.awt.image.*; import javax.imageio.*; import javax.swing.table.*; import java.awt.event.*; import java.lang.Object; // This class extends the swing JComponent, and also implements the Scrollable interface // // Notes on this 'Scrollable interface': // We want our ImageViewer to be in a window that has scroll bars at the sides // so that if our image is larger than our window, we can scroll around to see it. // To do this, after we have made an instance of our ImageViewer in the main function, // we will put it into a 'JScrollPane' object. // However, the JScrollPane needs to know a few things about our class. // Rather than having to specify these manually when we add the ImageViewer to the JScrollPane, // we let our ImageViewer implement the 'Scrollable' interface. // This interface defines a few functions that JScrollPane will call on our class to query its behaviour. // // For more information on scroll panes: // The Java Tutorial on scroll panes may be found at: // http://java.sun.com/docs/books/tutorial/uiswing/components/scrollpane.html#scrollable // The Scrollable interface is documented at: // http://java.sun.com/javase/6/docs/api/javax/swing/Scrollable.html class ImageViewer extends JComponent implements Scrollable { // We will be applying filters to our image, but we will actually want to filter the original each time. // Therefore we need to keep a copy of our original image. protected BufferedImage originalImage, displayedImage; public void ImageViewer() { // Set to null in constructor displayedImage = originalImage = null; } // This function will load our image from a file public boolean loadfile(File file) { try { // Nice and easy: reads the file // and loads it into our image BufferedImage temp = ImageIO.read(file); // Okay, it's probably best to ignore this... // still here? okay, this is a nasty hack to get around a java bug // (bug 4957775 for the curious) // basically, we cannot reliably perform a convolution operation on a BufferedImage // that we have loaded using ImageIO // To get around this, we create a new BufferedImage, and copy the loaded data into it! // Crazy, I know, but it works.. originalImage = new BufferedImage(temp.getWidth(),temp.getHeight(),BufferedImage.TYPE_INT_RGB); originalImage.setData(temp.getData()); displayedImage = originalImage; return true; } catch( IOException e ) { // Something went wrong with the image loading return false; } } // This function applies a filter operation to our image public void applyFilter(BufferedImageOp op) { // If we have an image, and are given an operation if( originalImage != null && op != null ) // perform the filter operation on the original, saving the output to our displayedImage displayedImage = op.filter(originalImage,null); } // As part of JComponent, this function is called // whenever the component needs to be drawn to the screen public void paint(Graphics g) { if( displayedImage != null ) // Draw our displayedImage, if we have one. g.drawImage(displayedImage,0,0,null); // Note that if we have not yet performed a filter operation, displayedImage == originalImage // as specified in the loadfile function. } // ------------------------------------------------------------------------ // These functions implement the Scrollable interface // For more information, please consult the java documentation // Used to query what size we are public Dimension getPreferredSize() { // If we have a fully loaded image, we return its size. // Note that we use the displayedImage, since this is the one we are displaying, // and it may be a different size to the original (there is a resize filter) if( displayedImage != null ) return new Dimension(displayedImage.getWidth(),displayedImage.getHeight()); else return new Dimension(0,0); } // Called when the user clicks on the scroll bar's buttons public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { // We return how many pixels we want to scroll by return 10; } // Called when the user clicks on the scroll bar's empty slider part public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { // Again, we return how many pixels we want to scroll by return 20; } // Again, used to query what size we are public Dimension getPreferredScrollableViewportSize() { // We return the same value as the getPreferredSize function, so we just call this instead: return getPreferredSize(); } // Returns if we are forcing ourselves to the width of the scroll pane (i.e. disabling horizontal scrolling) // E.g. Useful if we are writing a text application that supports word-wrapping, // as we wouldn't want to scroll horizontally. // However, we're not writing a text application, so we don't want this ;) public boolean getScrollableTracksViewportWidth() { return false; } // Returns if we are forcing ourselves to the height of the scroll pane (i.e. disabling vertical scrolling) // As above, but vertically public boolean getScrollableTracksViewportHeight() { return false; } // ------------------------------------------------------------------------ } class part2 { public static void main(String[] args) { // This will hold the file that we will be opening File file; // This is a very useful Java 2D class: // We create an instance of the file chooser... JFileChooser chooser = new JFileChooser(); // ...and then tell it to appear to our user. // It will present the user with an "open dialog" // file browser / chooser window. // The user selects a file, and then the function returns. int chooser_result = chooser.showOpenDialog(null); // What we do next depends on what the user clicked switch(chooser_result) { // If the user clicked on the "Open" button: case JFileChooser.APPROVE_OPTION: // Retrieve the selected file file = chooser.getSelectedFile(); // We can print something out to help us debug if need be System.out.println("Using file: " + file.getPath()); break; default: // Something went wrong - perhaps the user clicked "Cancel" System.out.println("Open dialog did not return a file"); // Returning from main will exit return; } // If program control reaches this point here, we have asked the user // to find a file for us to open, and we have retrieved that file. // We are going to create two image viewers // One will show the original version, the other will show the filtered version // (Later on an inner class needs to access the filtered viewer, so we declare it as final) ImageViewer originalViewer = new ImageViewer(); final ImageViewer filteredViewer = new ImageViewer(); // Send the file to both our image viewers // Our load function returns true if the file loaded okay... if( originalViewer.loadfile(file) && filteredViewer.loadfile(file) ) System.out.println("File loaded"); else { // ... and false if it failed System.out.println("File failed to load"); return; } // A JFrame is a framed window for our desktop. // We shall create one of these so that we can display our image. // The string we give it will appear as the window title JFrame frame = new JFrame("My Image Viewer"); // We are putting our image viewers into JScrollPanes. // These basically provide scroll bars, so we can scroll around our // image viewers if the image is too large for the window. // For more information, see the excellent description on: // http://java.sun.com/docs/books/tutorial/uiswing/components/scrollpane.html // We create the scroll panes, giving them the objects that we want to put inside. // We call them left and right because we want to put them next to each other... JScrollPane leftScroller = new JScrollPane( originalViewer ); JScrollPane rightScroller = new JScrollPane( filteredViewer ); // To put our image viewers next to each other, we will use something called a JSplitPane. // This literally splits our window in two, and allows us to place components either side. // Again, for more information, please see: // http://java.sun.com/docs/books/tutorial/uiswing/components/splitpane.html // We have to specify whether we want the split horizontal or vertical, // and we give our scrolling image viewers as arguements JSplitPane imageSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftScroller, rightScroller); // We can manually place the split divider. // We want our window to be 640 by 480, so let's tell it to split directly in half: imageSplit.setDividerLocation(320); // We want to provide some sort of data input method so that // the user can specify the kernel of the convolution filter. // To do this, we will use a table. // We want to provide the table with default values Float[][] data = new Float[3][3]; // The headers will not be used String[] headers = new String[] {"a","b","c"}; // Create our table // (Again, notice that this is also final) final JTable table = new JTable(data,headers) { // Our kernel must take a float array as its parameter, // so we will force our table's values to stay as floats. // We do this by overriding one of its functions: // this function returns the class type of a column, // so we force it to always return a Float type. public Class getColumnClass(int column) { return Float.class; } }; // We will also provide a button for the user to press // once they have finished entering their kernel data. JButton goButton = new JButton("Go!"); // We need to specify what happens when the user clicks on our button. // The abstract inner class instance of ActionListener listens for events from our button goButton.addActionListener( new ActionListener() { // This function is called whenever our button is pressed public void actionPerformed(ActionEvent event) { // We first need to retrieve the data from our table: float[] currentData = new float[9]; for(int i = 0; i < 3; i++ ) for( int j = 0; j < 3; j++ ) currentData[i*3+j] = ((Float)table.getValueAt(i,j)).floatValue(); // Now, we can create the convolution filter. // This takes a Kernel instance as a parameter. // We give our table data to the kernel, specifying its size ConvolveOp convolutionFilter = new ConvolveOp( new Kernel(3,3,currentData)); // We now have a convolution filter, and can apply it to our image viewer: filteredViewer.applyFilter(convolutionFilter); // It is inefficient to be constantly re-drawing our window, // so the underlying system only re-draws it when it really needs to. // (for example, if we scroll around our image etc.) // Therefore, we need to specify that our filtered image viewer needs to be redrawn. // To do this, we call its repaint function, (defined as part of the JComponent class). // For efficiency reasons, the function allows us to specify what parts need updating. // Since our filter works on the whole image, we tell it to redraw the whole thing: filteredViewer.repaint(0,filteredViewer.getX(),filteredViewer.getY(),filteredViewer.getWidth(),filteredViewer.getHeight()); } } ); // A JPanel is a general purpose container... JPanel panel = new JPanel(); // ...which we shall use to hold our table and button: panel.add(table); panel.add(goButton); // For more information on panels, see: // http://java.sun.com/docs/books/tutorial/uiswing/components/panel.html // We are going to create another split pane, this time vertical though. // It will hold our first split pane (which contains our image viewers), and our panel (which contains our table and button) JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, imageSplit,panel); // Again, set the divider: split.setDividerLocation(240); // the rest is the same as workshop 1 part 3, except that we add our 'split' to the frame's content pane // In Java 2D, windows have multiple 'panes'. // We need to put our viewer on one of these to allow it to be displayed. // There are a variety of different panes available, // but we usually want to use the "content pane" as it's for general purpose use. frame.getContentPane().add(split); // Setting window size frame.setSize(640,480); // We need to specify what to do when the user clicks on the close widget. // We shall exit. frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); // Finally, make our window visible. frame.setVisible(true); } }