Wielowątkowe przetwarzanie pliku graficznego

0

Witam. Piszę aplikację w JAVIE, nakładającą filtry na wczytany obraz, działającą na wątkach. Ogólny szkielet aplikacji mam, ale działający na jednym wątku (zapisu do pliku nie implementowałem jeszcze, bo nie wiem, co zostanie z kodu po dodaniu wątków). Podział wczytanego obrazka jest realizowany przez funkcję getSubimage (podział na 4 równe części w pionie), ale niestety nie mam pojęcia, jak przechwycić wynik podziału i zastosować na uzyskanych częściach zdefiniowane filtry. Bez wątków program był banalny, ale z wątkami okazał się być ciężki dla mnie do ukończenia. Moje przemyślenia są widoczne częściowo w komentarzach w kodzie. Zwracam się teraz z prośbą do osób znających JAVĘ o pomoc w skończeniu tego przykładu.

Aktualnie kod wygląda tak:
FiltryGraficzne.java

import java.awt.Container;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ByteLookupTable;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.image.LookupOp;
import java.awt.image.RescaleOp;
import java.io.File;

import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

public class FiltryGraficzne {
	public static void main(String[] args) {
		JFrame frame = new Okno();
		frame.setVisible(true);
	}
}

/////////////////////////////////////////////
//~~~~~~~~~~~~~~~~klasa Okno~~~~~~~~~~~~~~~//
/////////////////////////////////////////////
class Okno extends JFrame implements ActionListener {
	/**
	 * 
	 */
	private static final long serialVersionUID = 2800017321906936650L;

	public Okno() {
		setTitle("Filtry Graficzne");
		setSize(300, 400);
		addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				System.exit(0);
			}
		});

		Container contentPane = getContentPane();
		panel = new Operacje();
		contentPane.add(panel, "Center");

		JMenu fileMenu = new JMenu("Plik");
		openItem = new JMenuItem("Otwórz");
		openItem.addActionListener(this);
		fileMenu.add(openItem);

		// saveItem = new JMenuItem("Save");
		// saveItem.addActionListener(this);
		// fileMenu.add(saveItem);

		exitItem = new JMenuItem("Zakończ");
		exitItem.addActionListener(this);
		fileMenu.add(exitItem);

		JMenu editMenu = new JMenu("Edycja");
		blurItem = new JMenuItem("Rozmycie (Blur)");
		blurItem.addActionListener(this);
		editMenu.add(blurItem);

		sharpenItem = new JMenuItem("Wyostrzenie (Sharpen)");
		sharpenItem.addActionListener(this);
		editMenu.add(sharpenItem);

		brightenItem = new JMenuItem("Rozjaśnienie (Brighten)");
		brightenItem.addActionListener(this);
		editMenu.add(brightenItem);

		edgeDetectItem = new JMenuItem("Wykrywanie krawędzi (Edge detect)");
		edgeDetectItem.addActionListener(this);
		editMenu.add(edgeDetectItem);

		negativeItem = new JMenuItem("Negatyw");
		negativeItem.addActionListener(this);
		editMenu.add(negativeItem);

		rotateItem = new JMenuItem("Obrót (Rotate)");
		rotateItem.addActionListener(this);
		editMenu.add(rotateItem);
		
		JMenu helpMenu = new JMenu("Pomoc");
		infoItem = new JMenuItem("O programie");
		infoItem.addActionListener(this);
		helpMenu.add(infoItem);

		JMenuBar menuBar = new JMenuBar();
		menuBar.add(fileMenu);
		menuBar.add(editMenu);
		menuBar.add(helpMenu);
		setJMenuBar(menuBar);
	}

	public void actionPerformed(ActionEvent evt) {
		Object source = evt.getSource();
		if (source == openItem) {
			JFileChooser chooser = new JFileChooser();
			chooser.setCurrentDirectory(new File("."));

			chooser.setFileFilter(new javax.swing.filechooser.FileFilter() {
				public boolean accept(File f) {
					String name = f.getName().toLowerCase();
					return name.endsWith(".gif") || name.endsWith(".jpg")
							|| name.endsWith(".jpeg") || name.endsWith(".png")
							|| name.endsWith(".bmp") || f.isDirectory();
				}

				public String getDescription() {
					return "Pliki graficzne";
				}
			});

			int r = chooser.showOpenDialog(this);
			if (r == JFileChooser.APPROVE_OPTION) {
				String name = chooser.getSelectedFile().getAbsolutePath();
				panel.loadImage(name);
			}
		} else if (source == exitItem)
			System.exit(0);
		else if (source == blurItem)
			panel.blur();
		else if (source == sharpenItem)
			panel.sharpen();
		else if (source == brightenItem)
			panel.brighten();
		else if (source == edgeDetectItem)
			panel.edgeDetect();
		else if (source == negativeItem)
			panel.negative();
		else if (source == rotateItem)
			panel.rotate();
		else if (source == infoItem)
			panel.authors();
	}

	private Operacje panel;

	private JMenuItem openItem;

	private JMenuItem exitItem;

	private JMenuItem blurItem;

	private JMenuItem sharpenItem;

	private JMenuItem brightenItem;

	private JMenuItem edgeDetectItem;

	private JMenuItem negativeItem;

	private JMenuItem rotateItem;
	
	private JMenuItem infoItem;
}

/////////////////////////////////////////////////
//~~~~~~~~~~~~~~~~klasa Operacje~~~~~~~~~~~~~~~//
/////////////////////////////////////////////////
class Operacje extends JPanel {
	/**
	 * 
	 */
	private static final long serialVersionUID = 350570824125187877L;

	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		if (image != null)
			g.drawImage(image, 0, 0, null);
	}

	public void loadImage(String name) {
		Image loadedImage = Toolkit.getDefaultToolkit().getImage(name);
		MediaTracker tracker = new MediaTracker(this);
		tracker.addImage(loadedImage, 0);
		try {
			tracker.waitForID(0);
		} catch (InterruptedException e) {
		}
		image = new BufferedImage(loadedImage.getWidth(null), loadedImage
				.getHeight(null), BufferedImage.TYPE_INT_RGB);
		Graphics2D g2 = image.createGraphics();
		g2.drawImage(loadedImage, 0, 0, null);

		repaint();
	}

	private void filter(BufferedImageOp op) {
		// dzielenie obrazu na n-czesci
		// robione met. getSubimage()
		
		final int N = 4;
		
		//~~~~podział~~~//
		BufferedImage tab[] = new BufferedImage[N];
		for (int i = 0; i < N; i++) {
			// tab[i] =  image.getSubimage((i/N)*image.getWidth(), 0, ((i/N)*image.getWidth())+1/N, image.getHeight())  ;
		} 
		//~~~~koniec podziału~~~//

		// trzeba zrobic tablice na wyniki (N obrazkow)
		
		BufferedImage filteredImage = new BufferedImage(image.getWidth(), image
				.getHeight(), image.getType());
		// przetwarzanie rownolegle czesci obrazu
		Thread watki[] = new Thread[N];

		for (int i = 0; i < N; i++) {
			watki[i] = new Thread(new Runnable() {
				public void run() {
					// op.filter(image, filteredImage);
				}
			});
			watki[i].start();
		}

		// poczekac na wszystkie watki az zakoncza
		for (int i = 0; i < N; i++) {
			try {
				watki[i].join();
			} catch (Exception e) {
			}
		}

		image = filteredImage;
		repaint();

	}

	private void convolve(float[] elements) {
		Kernel kernel = new Kernel(3, 3, elements);
		ConvolveOp op = new ConvolveOp(kernel);
		filter(op);
	}

	public void blur() {
		float weight = 1.0f / 9.0f;
		float[] elements = new float[9];
		for (int i = 0; i < 9; i++)
			elements[i] = weight;
		convolve(elements);
	}

	public void sharpen() {
		float[] elements = { 0.0f, -1.0f, 0.0f, -1.0f, 5.f, -1.0f, 0.0f, -1.0f,
				0.0f };
		convolve(elements);
	}

	void edgeDetect() {
		float[] elements = { 0.0f, -1.0f, 0.0f, -1.0f, 4.f, -1.0f, 0.0f, -1.0f,
				0.0f };
		convolve(elements);
	}

	public void brighten() {
		float a = 1.5f;
		float b = -20.0f;
		RescaleOp op = new RescaleOp(a, b, null);
		filter(op);
	}

	void negative() {
		byte negative[] = new byte[256];
		for (int i = 0; i < 256; i++)
			negative[i] = (byte) (255 - i);
		ByteLookupTable table = new ByteLookupTable(0, negative);
		LookupOp op = new LookupOp(table, null);
		filter(op);
	}

	void rotate() {
		AffineTransform transform = AffineTransform.getRotateInstance(Math
				.toRadians(5), image.getWidth() / 2, image.getHeight() / 2);
		AffineTransformOp op = new AffineTransformOp(transform,
				AffineTransformOp.TYPE_BILINEAR);
		filter(op);
	}

	public void authors () {
		final String ABOUT_TEXT =
			 "<html>" + 
			 "Wielowątkowe przetwarzanie pliku graficznego<br><br>" +
			 "Program nakłada filtry na wczytany plik graficzny wykorzystując w tym celu wątki" +
			 "</html>";
		JOptionPane.showMessageDialog(Operacje.this, ABOUT_TEXT);
	}

	private BufferedImage image;
	// private BufferedImage lastImage;
}

Jeśli ktoś ma jakiś pomysł jak to skończyć, to niech pisze śmiało, jestem otwarty na wszelkie propozycje.

0

Działa poprawnie, ale dla niektórych filtrów pojawiają się artefakty na łączeniach.
Wykorzystuje ExecutorService - pulę wątków.
Poprawiłem też dzielenie obrazka na części, gdyż poprzedni algorytm był niepoprawny.

Pamiętaj getSubimage nie tworzy nowego obrazu, ono daje tylko widok pewnej części obrazu oryginalnego. Zmiana w części spowoduje zmianę w całości. Dlatego nie ma konieczności zwracania czegokolwiek.

	private ExecutorService service = Executors.newCachedThreadPool();

        private void filter(final BufferedImageOp op) {
		final int N = 4;
	        BufferedImage filteredImage = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
		
		/* Przygotowanie zadań */
		final List<Callable<Void>> tasks = new ArrayList<Callable<Void>>();
		for (int i = 0; i < N; i++) {
			int start = (int)(i*image.getWidth()/((float)(N)));
			int width = Math.min(image.getWidth() - start, (int)(image.getWidth()/((float)(N))));
			final BufferedImage subimageSource = image.getSubimage(start, 0, width, image.getHeight());
			final BufferedImage subimageDestination = filteredImage.getSubimage(start, 0, width, image.getHeight());
			tasks.add(new Callable<Void>(){
				public Void call(){
					op.filter(subimageSource, subimageDestination);
					return null;
				}
			});
		}

		/* Wykonanie zadań */
		try{
			service.invokeAll(tasks);
		}catch (InterruptedException ex){
			System.exit(1);
		}
            
                image = filteredImage;
                repaint();
        }
0

__krzysiek85, jesteś wielki [browar] , sam bym tego w nigdy nie rozwiązał - za słabo znam JAVĘ. Jeśli byłbyś tak miły i mógłbyś mi jeszcze podpowiedzieć w jaki sposób dodać UNDO (cofnięcie operacji), zapis, oraz licznik czasu wykonania operacji.

Jedyna znana mi metoda pomiaru czasu to:

long start = System.currentTimeMillis();
....
doSomething();
....
long end = System.currentTimeMillis();

System.out.println("Execution time was "+(end-start)+" ms.");

ale nie jestem pewien gdzie powstawiać poszczególne fragmenty aby zadziałało przy wielu wątkach prawidłowo.

Wybacz, jeśli zadaję głupie pytania, dopiero poznaję wielowątkowość, i sporo rzeczy jest dla mnie jeszcze niezrozumiałych/wielu rzeczy nie wiem.

0

Co do undo, to zainteresuj się wzorcem projektowym command.

0

Wzorzec command nic tu nie da, bo są to operacje nieodwracalne.

Pamiętaj poprzednie wersje obrazka na stosie.

tzn.
Przed wykonaniem operacji na obrazku wykonaj kopię obrazka i wrzuć ją na stos.
Przy undo pobierz obraz ze stosu i wstaw jako aktualny.

Możesz też ograniczyć wielkość stosu i usuwać stare obrazki, gdy stos przekroczy z góry określoną liczbę elementów.


Co do mierzenia czasu, to jedynym miejscem, gdzie działają wątki to linijka "service.invokeAll(tasks);". Wszystko wcześniej to przygotowanie zadań.
Tylko użyj nanoTime() zamiast currentTimeMillis(), bo jest dokładniejszy.

0

Faktycznie, sorry za wprowadzenie w błąd.

0

Z pomiarem czasu sobie poradziłem, dzięki za podpowiedź, ale z undo nie daję rady, po skopiowaniu obrazka na stos i późniejszej próbie odczytania go (poprzez użycie undo) nic się nie dzieje, operacja nie jest cofana.

w klasie Operacje dodałem:

private Stack<Image> stack= new Stack<Image>();
...
stack.add(loadedImage);
...
public void undo() {
	stack.pop();
	repaint();
}

przypuszczam, że spaprałem to nieźle.

Chciałem jeszcze dorobić zapis zmodyfikowanego pliku, ale nie wiem jak zrobić aby wyświetlić najpierw okno pozwalające wybrać lokację do zapisu (zapis do z góry zdefiniowanej lokacji jest prosty, ale chciałem zrobić coś bardziej "normalnego") i ewentualnie jeden z kilku formatów plików (jeśli będzie to zbyt złożone, to wystarczy tylko .jpg)</image></image>

0
__krzysiek85 napisał(a)

Wzorzec command nic tu nie da, bo są to operacje nieodwracalne.

Pamiętaj poprzednie wersje obrazka na stosie.

To może wzorzec memento? :)

0
private Stack<BufferedImage> stack= new Stack<BufferedImage>();//musisz zmienić typ na BufferedImage
...
stack.add(loadedImage);
...
public void undo() {
	if (!stack.empty()){
		image=stack.pop(); //musisz przypisać na image
		repaint();
	}
}

Co do zapisywania obrazka, to zapoznaj się z klasą ImageIO.

0

Dzięki za całą pomoc, program zakończyłem, zapis zaczerpnąłem z tego kodu:
http://www.cs.princeton.edu/introcs/98simulation/Picture.java.html
zmodyfikowałem go tylko na potrzeby mojego programu. Jeszcze raz wielkie dzięki.

1 użytkowników online, w tym zalogowanych: 0, gości: 1