Intro to AS3 Workers (Part 2): Image Processing

[UPDATE] Adobe has removed support for ByteArray.shareable until Player 11.5. The source code has been updated to work around that restriction, but have left the example code intact since it will eventually work fine.

In Part 1 of my Intro to AS3 Workers series I looked at the fundamentals of AS3 workers, including the various communication methods, and showed an example of a simple Hello World worker.

In this next post, I’ll take it a step further and show how you can start doing something useful, like image processing! In this case, I’ll be applying a Sharpen Filter to a large bitmap, while the main UI thread continues to render at 30fps.

DEMO: Single-Threaded
Here you can take a look at what we’re going to make. This version does not use workers, and you can see that the slider locks up completely as the image is processed:

Note: If you can’t see the SWF, make sure you have downloaded Flash Player 11.4. Chrome user’s, don’t forget to disable the old version.

DEMO: Multi-Threaded
Here is the same demo, but using a Worker. You can see the UI renders at a smooth 30fps:

Note: If you can’t see the SWF, make sure you have downloaded Flash Player 11.4. Chrome user’s, don’t forget to disable the old version.

The Code

Before actually jumping into the code, it’s important to plan ahead. Especially when dealing with concurrency, we really need to create an efficient system for marshaling data between the workers, otherwise your main thread will begin to bog down due to the heavy cost of serializing and de-serializing data.

After a bit of a thought, I decided to architect the app like so:

  • The bitmapData would be shared with the worker using  a shareable byteArray
  • We will use the bitmapData.setPixels(), and bitmapData.copyPixelsToByteArray() API’s to convert the bitmap > byteArray, and vice versa.
  • The main thread will issue “SHARPEN” commands to the worker, and the worker will send “SHARPEN_COMPLETE” when it’s done
  • The worker will use a 500ms timer to check whether it needs to run a new Sharpen operation. This prevents excessive Sharpen operations.

The Document Class
First up is the constructor, here we’ll use the same loaderInfo.bytes trick as we did in the last tutorial. This constructor is run twice, the second instance is the worker, and it creates a SharpenWorkerinstance which will handle all communication back to the main thread.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ImageWorkerExample extends Sprite
{
...
public function ImageWorkerExample(){
	//Main thread
	if(Worker.current.isPrimordial){
		initUi();
		initWorker();
	} 
	//If not the main thread, we're the worker
	else {
		sharpenWorker = new SharpenWorker();
	}
}

Lets continue to walk through the main class before looking at Sharpen Worker.

The call to initUi() simply creates a slider, and an image, and adds them to the stage. No need to look at that here.

The next function is initWorker(), take a look at that with comments inlined:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 
protected function initWorker():void {
	//Create worker from the main swf 
	worker = WorkerDomain.current.createWorker(loaderInfo.bytes);
 
	//Create message channel TO the worker
	mainToBack = Worker.current.createMessageChannel(worker);
 
	//Create message channel FROM the worker, add a listener.
	backToMain = worker.createMessageChannel(Worker.current);
	backToMain.addEventListener(Event.CHANNEL_MESSAGE, onBackToMain, false, 0, true);
 
	//Now that we have our two channels, inject them into the worker as shared properties.
	//This way, the worker can see them on the other side.
	worker.setSharedProperty("backToMain", backToMain);
	worker.setSharedProperty("mainToBack", mainToBack);
 
	//Init worker with width/height of image
	worker.setSharedProperty("imageWidth", origImage.width);
	worker.setSharedProperty("imageHeight", origImage.height);
 
	//Convert image data to (shareable) byteArray, and share it with the worker.
	imageBytes = new ByteArray();
	imageBytes.shareable = true;
	origImage.copyPixelsToByteArray(origImage.rect, imageBytes);
	worker.setSharedProperty("imageBytes", imageBytes);
 
	//Lastly, start the worker.
	worker.start();
}

In the above code, we did a few things:

  • Created a worker using our own loaderInfo.bytes
  • Created message channels so that we can talk to the Worker, and vice-versa
  • Copied the image data into a shareable byteArray
  • ‘Shared’ the image data with the worker
  • Started the worker

The next step in the main class, is to ask the worker to sharpen something, and to respond when the worker has completed the task.

First, we’ll wire up the Slider CHANGE handler:

0
1
2
3
4
5
 
protected function onSliderChanged(value:Number):void {
	//Send the sharpen command to our worker.
	mainToBack.send("SHARPEN");
	mainToBack.send(value);
}

Next, we’ll handle the completion from the worker:

0
1
2
3
4
5
6
7
 
protected function onBackToMain(event:Event):void {
	var msg:String = backToMain.receive();
	if(msg == "SHARPEN_COMPLETE"){
		imageBytes.position = 0;
		image.bitmapData.setPixels(image.bitmapData.rect, imageBytes);
	}
}

That is it for the main class!

The Worker

The constructor for the worker is fairly straight forward:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 
public class SharpenWorker extends Sprite
{
 
...
 
public function SharpenWorker(){
	//Get reference to current worker (ourself)
	var worker:Worker = Worker.current;
 
	//Listen on mainToBack for SHARPEN events
	mainToBack = worker.getSharedProperty("mainToBack");
	mainToBack.addEventListener(Event.CHANNEL_MESSAGE, onMainToBack);
	//Use backToMain to dispatch SHARPEN_COMPLETE
	backToMain = worker.getSharedProperty("backToMain");
 
	//Get the image data from the shareProperty pool
	imageBytes = worker.getSharedProperty("imageBytes");
	var w:int = worker.getSharedProperty("imageWidth");
	var h:int = worker.getSharedProperty("imageHeight");
 
	imageBytes.position = 0;
	imageData = new BitmapData(w, h, false, 0x0);
	backToMain.send(imageBytes.length);
	imageData.setPixels(imageData.rect, imageBytes);
 
	//Create timmer to check whether sharpenValue has been dirtied
	timer = new Timer(500);
	timer.addEventListener(TimerEvent.TIMER, onTimer, false, 0, true);
	timer.start();
}

Here we did a few things:

  • Snagged a reference to the shared message channels from the main thread
  • Initialized a bitmapData object using the shared byteArray
  • Stored a reference to the byteArray internally, so we can write to it from now on.
  • Created a Timer to check for invalidation

Next, we need to respond to SHARPEN request. That’s done in the listener for the mainToBack message channel:

0
1
2
3
4
5
6
7
8
9
10
11
 
protected function onMainToBack(event:Event):void {
	if(mainToBack.messageAvailable){
		//Get the message type.
		var msg:* = mainToBack.receive();
		//Sharpen
		if(msg == "SHARPEN"){
			targetSharpen = mainToBack.receive();
		}
 
	}
}

Note that we don’t really do anything here other than to save off the target sharpenValue. It would be wasteful to apply a sharpen every single time it’s requested. The sharpen operation can take over 500ms to complete, but the main UI thread can issue sharpen requests @ 33ms, so obviously this would create a huge backlog if we responded to each one! Eventually your worker would likely crash.

Instead, we’ll apply the SharpenFilter inside of the TIMER handler, which is running at 500ms:

0
1
2
3
4
5
6
7
8
9
10
11
12
 
protected function onTimer(event:TimerEvent):void {
	if(targetSharpen == currentSharpen){ return; } //Don't sharpen unless the value has changed
	currentSharpen = targetSharpen;
 
	//Sharpen image and copy into ByteArray
	var data:BitmapData = ImageUtils.SharpenImage(imageData, currentSharpen);
	imageBytes.length = 0;
	data.copyPixelsToByteArray(data.rect, imageBytes);
 
	//Notify main thread that the sharpen operation is complete
	backToMain.send("SHARPEN_COMPLETE");
}

That’s all there is to that! The main thread will issue a SHARPEN command with a value, the worker will get it, update the shared byteArray and send back SHARPEN_COMPLETE. Upon receiving SHARPEN_COMPLETE the main thread will update the image using the shared byteArray.

Note: If you’re wondering about the sharpen operation itself, that is handled by an excellent gskinner.com filter.

You can download the full project files here:

Happy multi-threading :D

Written by

9 Comments to “Intro to AS3 Workers (Part 2): Image Processing”

  1. greg says:

    Thanks for the tutorial.
    Workers are not working on mobiles, right?

  2. [...] AS3多线程快速入门(二):图像处理[译] 添加评论[作者:Dom Chen 分类:ActionScript, Flash ] 原文链接:http://esdot.ca/site/2012/intro-to-as3-workers-part-2-image-processing [...]

  3. stormsen74 says:

    Hi, thanks for the tutorial – i can`t see any changes of the image (sharpening) in the multithreading example…

  4. [...] 在系列教程的第二部分中,我们研究了在一个在独立线程里执行图像处理的例子。 [...]

  5. robo says:

    Hi, thanks for the tutorial – i can`t see any changes of the image (sharpening) in the multithreading example too.
    I debug you code ,breakpoint nevert goto SharpenWorker: onTimer to sharp function.
    maybe something work or not support transfer bytearry now .

  6. Ulaş Binici says:

    Something is missing bec’ When i run document class (ImageWorkerExample.as) it doesn’t seem like crete backWorker for SharpenWorker.as ?

  7. Ulaş Binici says:

    its ok its my fold my player version was 11.4.400 i replaced 11.4.402

  8. Thanks again for these tutorials. I thought I’d be clever and spawn multiple workers – turns out it brings my machine to its knees :)

    http://flexmonkey.blogspot.co.uk/2012/09/multiple-actionscript-workers-for-image.html

    simon

  9. Martin says:

    Hi, I tried the code and flash player 11.5.502.149 , but it doesnt seem to work.. besides shareable for bytearray, is there something else needed to do?

    the messages dont seem to return from the worker..

    any ideas?

    thanks!!

  10. allen says:

    i can`t see any changes of the image (sharpening) in the multithreading example too….is that a joke?

Leave a Reply

Message