AXOPEN

Multithreading et BufferedImage – JAVA – Comparaison d’image

Multithread et bufferedImage

Dans cet article nous allons voir comment réaliser une comparaison des pixels de deux BufferedImage en multithread pour accélérer la performance.

L’utilisation du multithreading pour le traitement des images apporte une réelle amélioration des performances.

L’exemple de la comparaison n’est pas l’exemple le plus percutant mais il permet de se rendre compte de l’amélioration subtentiel que peut amener le multithreading mais aussi sa simplicité de mise en oeuvre depuis JAVA 7.

Dans cet exemple, nous allons prendre deux BuffereImage créés à partir d’un PNG (donc avec une valeur de transparence). Nous allons comparé, pixel après pixel les deux images et écrire une nouvelle image avec un couleur rouge pour chaque pixel différent entre les deux images sources.

RecursiveAction

Premièrement il faut créer une classe implémentant java.util.concurrent.RecursiveAction. C’est interface nous oblige à créer une méthode compute qui va réaliser le traitement. Cette méthode compute se comporte de la manière suivante. Si la taille tu traitement est trop gros, elle divise la charge en deux sous tâches qui seront à leur tour mutlithreadé. Si les sous tâches sont encore trop grosses alors elles seront encore divisées en deux sous tâches. (De manière récursive). En gros, on veut traiter sur un thread qu’une petite partie de l’image. Chaque thread sur chaque processeur aura la responsabilité de comparer quelques pixels de l’image.

Donc le premier if de la méthode compute sert à définir si le traitement  est trop gros pour être fait sur cette tâche. Le else du if permet de réaliser simplement une comparaison des pixels entre les deux images. En n’oubliant pas la valeur de transparence.

class CompareMultiThreadAction extends RecursiveAction {

	int mOffsetMin;
	int mOffsetMax;

	private static final long serialVersionUID = 1L;
	int splitSize;

	byte[] pixelSource;
	byte[] pixelOpening;
	int width;
	int height;

	public CompareMultiThreadAction(int pOffsetMin, int pOffsetMax,
			int pCuttingPoint, byte[] pixels, byte[] pixelsModified, int pWidth, int pHeight) {
		// Structuring element dimensions
		splitSize = pCuttingPoint;
		mOffsetMin = pOffsetMin;
		mOffsetMax = pOffsetMax;
		pixelSource = pixels;
		pixelOpening = pixelsModified;
		width = pWidth;
		height = pHeight;

	}

	@Override
	protected void compute() {

		if ((mOffsetMax - mOffsetMin) > splitSize) {
			// task is huge so divide in half

			int mid = (mOffsetMax - mOffsetMin) / 2;
			invokeAll(asList(new CompareMultiThreadAction(mOffsetMin,
					mOffsetMin + mid, splitSize, pixelSource, pixelOpening,
					pixelClosing, width, height), new CompareMultiThreadAction(
					mOffsetMin + mid + 1, mOffsetMax, splitSize, pixelSource,
					pixelOpening, width, height)));
		} else {
			// Image dimensions

			boolean lChangeOpening = false;
			// Si on a trouvé un pixel de finesse
			boolean lChangeClosing = false;
			byte lApha_ref, lblue_ref, lVert_ref, lRouge_ref, lApha_open, lblue_open, tlVert_open, lRouge_open;

			int offset = mOffsetMin;
			int y = (mOffsetMin / width);
			int x = (mOffsetMin % width);
			boolean first = true;


			for (; y < height; y++) {

				if (first)
					first = false;
				else
					x = 0;
				for (; x < width; x++, offset++) {

					lApha_ref = pixelSource[offset * 4];
					lblue_ref = pixelSource[offset * 4 + 1];
					lVert_ref = pixelSource[offset * 4 + 2];
					lRouge_ref = pixelSource[offset * 4 + 3];

					lApha_open = pixelOpening[offset * 4];
					lblue_open = pixelOpening[offset * 4 + 1];
					tlVert_open = pixelOpening[offset * 4 + 2];
					lRouge_open = pixelOpening[offset * 4 + 3];
					
					lChangeOpening = false;

					// Change in opening pixel
					if (lApha_ref != lApha_open || lblue_ref != lblue_open
							|| lVert_ref != tlVert_open
							|| lRouge_ref != lRouge_open) {
						lChangeOpening = true;
					}

					
					if (lChangeOpening) {

						pixelSource[offset * 4] = -1;
						pixelSource[offset * 4 + 1] = 0;
						pixelSource[offset * 4 + 2] = 0;
						pixelSource[offset * 4 + 3] = -1;
					}

					if (x * y >= mOffsetMax) {
						// System.out.println("process " + (x * y -
						// mOffsetMin));
						return;
					}
				}

			}

		}
	}
}

 

ForkJoinPool

Avec cette classe nous effectuons le travail, maintenant il reste à voir comment appeler la classe depuis votre application. Pour ce faire, nous allons dans un premier temps créer un ForkJoinPool sur lequel nous allons invoker notre task.

Mais d’abord, nous allons définir le cuttingPoint (ou nombre de pixel que nous souhaitons traiter sur chaque processeur). Pour cela, nous allons diviser le nombre de pixel global de l’image par le nombre de processeur disponible par la JVM. Pour trouver le nomrbe de processeurs disponibles, il suffit de faire un Runtime.getRuntime().availableProcessors().

Ainsi le traitement va se diviser jusqu’à obtenir un thread sur chaque processus de la machine qui traiteront chaqu’un le bon nombre  de pixel.

public void compareImageMulti(BufferedImage pImageSource,
			BufferedImage pImageOpening, BufferedImage pImageClosing) {
		ForkJoinPool fjpool = new ForkJoinPool(64);
		final int width = pImageSource.getWidth();
		final int height = pImageSource.getHeight();
		int cuttingPoint = (width * height)
				/ Runtime.getRuntime().availableProcessors();
		byte[] pixels = ((DataBufferByte) pImageSource.getRaster()
				.getDataBuffer()).getData();
		byte[] pixel2s = ((DataBufferByte) pImageOpening.getRaster()
				.getDataBuffer()).getData();
	
		RecursiveAction task = new CompareMultiThreadAction(0, width * height,
				cuttingPoint, pixels, pixel2s, width, height);

		fjpool.invoke(task);

		return ;

	}

 

On voit donc que c’est extrêment simple de faire du multithreading avec des bufferedimages car ceci revient à travailler sur des tableaux de pixels (DataBufferByte) qui s’avère être extrêmement performant à l’usage.