[Contribution] Texture2D blur in c#

Based on this one from Eric Willis:
http://notes.ericwillis.com/2009/10/blur-an-image-with-csharp/

	private Texture2D Blur(Texture2D image, int blurSize)
	{
	    Texture2D blurred = new Texture2D(image.width, image.height);
	  
	    // look at every pixel in the blur rectangle
	    for (int xx = 0; xx < image.width; xx++)
	    {
	        for (int yy = 0; yy < image.height; yy++)
	        {
	            float avgR = 0, avgG = 0, avgB = 0, avgA = 0;
	            int blurPixelCount = 0;
	 
	            // average the color of the red, green and blue for each pixel in the
	            // blur size while making sure you don't go outside the image bounds
	            for (int x = xx; (x < xx + blurSize  x < image.width); x++)
	            {
	                for (int y = yy; (y < yy + blurSize  y < image.height); y++)
	                {
	                    Color pixel = image.GetPixel(x, y);
	 
	                    avgR += pixel.r;
	                    avgG += pixel.g;
	                    avgB += pixel.b;
						avgA += pixel.a;
	 
	                    blurPixelCount++;
	                }
	            }
	 
	            avgR = avgR / blurPixelCount;
	            avgG = avgG / blurPixelCount;
	            avgB = avgB / blurPixelCount;
				avgA = avgA / blurPixelCount;
	 
	            // now that we know the average for the blur size, set each pixel to that color
	            for (int x = xx; x < xx + blurSize  x < image.width; x++)
	                for (int y = yy; y < yy + blurSize  y < image.height; y++)
	                    blurred.SetPixel(x, y, new Color(avgR, avgG, avgB, avgA));
	        }
	    }
	 	blurred.Apply();
	    return blurred;
	}
2 Likes

Thanks for this, it really helped me out!

It was a bit slow for my liking, but I remembered stumbling across this page a few months ago, and thought I would give some optimisation a try.

I’ve separated the blur into two passes, one horisontal and one vertical, as shown on the site. Furthermore I’ve made it blur in regards to pixels on all sides, and not just in front / on top. Oh, and I have converted your code to javascript.

The best results i have had, in terms of getting a compromise between speed and quality, is by having a relatively low resolution texture and doing multiple blur iterations (2 iterations on a 128x128 texture), with a radius of 5.

It’s just a couple of hours work so far, and I’m sure it can be further improved upon! Thanks again :slight_smile:

private var avgR : float = 0; 
   	private var avgG : float = 0; 
    private var avgB : float = 0; 
	private var avgA : float = 0;
    private var blurPixelCount = 0;

    function FastBlur(image : Texture2D, radius : int, iterations : int) : Texture2D {
    	var tex : Texture2D = image;
    	for (var i = 0; i < iterations; i++) {
    		tex = BlurImage(tex, radius, true);
			tex = BlurImage(tex, radius, false);
		}
		return tex;
    }

    function BlurImage(image : Texture2D, blurSize : int, horizontal : boolean) : Texture2D {
        var blurred : Texture2D = new Texture2D(image.width, image.height);
        var _W : int = image.width;
        var _H : int = image.height;
        var xx : int; var yy : int; var x : int; var y : int;

        if (horizontal) {
        // Horizontal
        for (yy = 0; yy < _H; yy++) {
        	for (xx = 0; xx < _W; xx++) {
  				ResetPixel();
  				//Right side of pixel
                for (x = xx; (x < xx + blurSize  x < _W); x++) {
                    AddPixel(image.GetPixel(x, yy));
                }
                //Left side of pixel
                for (x = xx; (x > xx - blurSize  x > 0); x--) {
                    AddPixel(image.GetPixel(x, yy));
                }
                CalcPixel();
                for (x = xx; x < xx + blurSize  x < _W; x++) {
                	blurred.SetPixel(x, yy, new Color(avgR, avgG, avgB, 1.0));
                }
            }
        }
    	}
    	else {
        // Vertical
        for (xx = 0; xx < _W; xx++) {
        	for (yy = 0; yy < _H; yy++) {
        		ResetPixel();
        		//Over pixel
                for (y = yy; (y < yy + blurSize  y < _H); y++) {
                    AddPixel(image.GetPixel(xx, y));
                }
                //Under pixel
                for (y = yy; (y > yy - blurSize  y > 0); y--) {
                    AddPixel(image.GetPixel(xx, y));
                }
                CalcPixel();
                for (y = yy; y < yy + blurSize  y < _H; y++) {
                	blurred.SetPixel(xx, y, new Color(avgR, avgG, avgB, 1.0));
                }
            }
        }
    	}

        blurred.Apply();
        return blurred;
    }

    function AddPixel(pixel : Color) {
        avgR += pixel.r;
        avgG += pixel.g;
        avgB += pixel.b;
        blurPixelCount++;
    }

    function ResetPixel() {
    	avgR = 0.0;
  		avgG = 0.0;
  		avgB = 0.0;
  		blurPixelCount = 0;
    }

    function CalcPixel() {
        avgR = avgR / blurPixelCount;
        avgG = avgG / blurPixelCount;
        avgB = avgB / blurPixelCount;
    }

I tested Fogman’s code but it shifts a solid circle as an input texture to the lower left corner. The bigger the blurSize, the bigger the shift.

The code Steffan Poulsen provides doesn’t have the shift bug and it is faster to boot. I converted it back into C# as well :sunglasses:

private float avgR = 0;
private float avgG = 0; 
private float avgB = 0; 
private float avgA = 0;
private float blurPixelCount = 0; 

Texture2D FastBlur(Texture2D image, int radius, int iterations){

    Texture2D tex = image;

    for (var i = 0; i < iterations; i++) {

        tex = BlurImage(tex, radius, true);
        tex = BlurImage(tex, radius, false);
    }

    return tex;
}

Texture2D BlurImage(Texture2D image, int blurSize, bool horizontal){

    Texture2D blurred = new Texture2D(image.width, image.height);
    int _W = image.width;
    int _H = image.height;
	int xx, yy, x, y;

    if (horizontal) {

		for (yy = 0; yy < _H; yy++) {

			for (xx = 0; xx < _W; xx++) {

				ResetPixel();

				//Right side of pixel
				for (x = xx; (x < xx + blurSize  x < _W); x++) {

					AddPixel(image.GetPixel(x, yy));
				}

				//Left side of pixel
				for (x = xx; (x > xx - blurSize  x > 0); x--) {

					AddPixel(image.GetPixel(x, yy));
				}

				CalcPixel();

				for (x = xx; x < xx + blurSize  x < _W; x++) {

					blurred.SetPixel(x, yy, new Color(avgR, avgG, avgB, 1.0f));
				}
			}
		}
    }

    else {

		for (xx = 0; xx < _W; xx++) {

			for (yy = 0; yy < _H; yy++) {

				ResetPixel();

				//Over pixel
				for (y = yy; (y < yy + blurSize  y < _H); y++) {

					AddPixel(image.GetPixel(xx, y));
				}

				//Under pixel
				for (y = yy; (y > yy - blurSize  y > 0); y--) {

					AddPixel(image.GetPixel(xx, y));
				}

				CalcPixel();

				for (y = yy; y < yy + blurSize  y < _H; y++) {

					blurred.SetPixel(xx, y, new Color(avgR, avgG, avgB, 1.0f));
				}
			}
		}
    }

    blurred.Apply();
    return blurred;
}

void AddPixel(Color pixel) {

    avgR += pixel.r;
    avgG += pixel.g;
    avgB += pixel.b;
    blurPixelCount++;
}

void ResetPixel() {

    avgR = 0.0f;
    avgG = 0.0f;
    avgB = 0.0f;
    blurPixelCount = 0;
}

void CalcPixel() {

    avgR = avgR / blurPixelCount;
    avgG = avgG / blurPixelCount;
    avgB = avgB / blurPixelCount;
}

Small modification for @Elecman 's version:

using UnityEngine;
using System.Collections;

public class Blur : MonoBehaviour {
   
    private float avgR = 0;
    private float avgG = 0;
    private float avgB = 0;
    private float avgA = 0;
    private float blurPixelCount = 0;
   
    public int radius =2;
    public int iterations =2;
   
    private Texture2D tex;
   
    // Use this for initialization
    void Start () {
        tex = renderer.material.mainTexture as Texture2D;
       
    }
   
    // Update is called once per frame
    void Update () {
       
        if (Input.GetKeyDown(KeyCode.Space))
            renderer.material.mainTexture = FastBlur( tex, radius, iterations);
    }
   
   
    Texture2D FastBlur(Texture2D image, int radius, int iterations){
        Texture2D tex = image;
       
        for (var i = 0; i < iterations; i++) {
           
            tex = BlurImage(tex, radius, true);
            tex = BlurImage(tex, radius, false);
           
        }
       
        return tex;
    }
   
   
   
    Texture2D BlurImage(Texture2D image, int blurSize, bool horizontal){
       
        Texture2D blurred = new Texture2D(image.width, image.height);
        int _W = image.width;
        int _H = image.height;
        int xx, yy, x, y;
       
        if (horizontal) {
            for (yy = 0; yy < _H; yy++) {
                for (xx = 0; xx < _W; xx++) {
                    ResetPixel();
                   
                    //Right side of pixel
                   
                    for (x = xx; (x < xx + blurSize && x < _W); x++) {
                        AddPixel(image.GetPixel(x, yy));
                    }
                   
                    //Left side of pixel
                   
                    for (x = xx; (x > xx - blurSize && x > 0); x--) {
                        AddPixel(image.GetPixel(x, yy));
                       
                    }
                   
                   
                    CalcPixel();
                   
                    for (x = xx; x < xx + blurSize && x < _W; x++) {
                        blurred.SetPixel(x, yy, new Color(avgR, avgG, avgB, 1.0f));
                       
                    }
                }
            }
        }
       
        else {
            for (xx = 0; xx < _W; xx++) {
                for (yy = 0; yy < _H; yy++) {
                    ResetPixel();
                   
                    //Over pixel
                   
                    for (y = yy; (y < yy + blurSize && y < _H); y++) {
                        AddPixel(image.GetPixel(xx, y));
                    }
                    //Under pixel
                   
                    for (y = yy; (y > yy - blurSize && y > 0); y--) {
                        AddPixel(image.GetPixel(xx, y));
                    }
                    CalcPixel();
                    for (y = yy; y < yy + blurSize && y < _H; y++) {
                        blurred.SetPixel(xx, y, new Color(avgR, avgG, avgB, 1.0f));
                       
                    }
                }
            }
        }
       
        blurred.Apply();
        return blurred;
    }
    void AddPixel(Color pixel) {
        avgR += pixel.r;
        avgG += pixel.g;
        avgB += pixel.b;
        blurPixelCount++;
    }
   
    void ResetPixel() {
        avgR = 0.0f;
        avgG = 0.0f;
        avgB = 0.0f;
        blurPixelCount = 0;
    }
   
    void CalcPixel() {
        avgR = avgR / blurPixelCount;
        avgG = avgG / blurPixelCount;
        avgB = avgB / blurPixelCount;
    }
}

Add script and texture to quad or plane for example. Texture needs to be marked as read / write. Hit play and press space to blur.

2 Likes

am i doing something wrong? all of these script fail at the first use of the variable x.

Looks like new forum software has erased && symbols (double ampersand) from code. I edited my post, but if you wish to use other versions, you need to fix all this kind of parts from the code:

 //Left side of pixel
                for (x = xx; (x > xx - blurSize  x > 0); x--) {

TO:

 //Left side of pixel
                for (x = xx; (x > xx - blurSize && x > 0); x--) {

My optimized version

class LinearBlur
    {
        private float _rSum = 0;
        private float _gSum = 0;
        private float _bSum = 0;

        private Texture2D _sourceImage;
        private int _sourceWidth;
        private int _sourceHeight;
        private int _windowSize;

        public Texture2D Blur(Texture2D image, int radius, int iterations)
        {
            _windowSize = radius * 2 + 1;
            _sourceWidth = image.width;
            _sourceHeight = image.height;

            var tex = image;

            for (var i = 0; i < iterations; i++)
            {
                tex = OneDimensialBlur(tex, radius, true);
                tex = OneDimensialBlur(tex, radius, false);
            }

            return tex;
        }

        private Texture2D OneDimensialBlur(Texture2D image, int radius, bool horizontal)
        {
            _sourceImage = image;

            var blurred = new Texture2D(image.width, image.height, image.format, false);

            if (horizontal)
            {
                for (int imgY = 0; imgY < _sourceHeight; ++imgY)
                {
                    ResetSum();

                    for (int imgX = 0; imgX < _sourceWidth; imgX++)
                    {
                        if (imgX == 0)
                            for (int x = radius * -1; x <= radius; ++x)
                                AddPixel(GetPixelWithXCheck(x, imgY));
                        else
                        {
                            var toExclude = GetPixelWithXCheck(imgX - radius - 1, imgY);
                            var toInclude = GetPixelWithXCheck(imgX + radius, imgY);

                            SubstPixel(toExclude);
                            AddPixel(toInclude);
                        }

                        blurred.SetPixel(imgX, imgY, CalcPixelFromSum());
                    }
                }
            }

            else
            {
                for (int imgX = 0; imgX < _sourceWidth; imgX++)
                {
                    ResetSum();

                    for (int imgY = 0; imgY < _sourceHeight; ++imgY)
                    {
                        if (imgY == 0)
                            for (int y = radius * -1; y <= radius; ++y)
                                AddPixel(GetPixelWithYCheck(imgX, y));
                        else
                        {
                            var toExclude = GetPixelWithYCheck(imgX, imgY - radius - 1);
                            var toInclude = GetPixelWithYCheck(imgX, imgY + radius);

                            SubstPixel(toExclude);
                            AddPixel(toInclude);
                        }

                        blurred.SetPixel(imgX, imgY, CalcPixelFromSum());
                    }
                }
            }

            blurred.Apply();
            return blurred;
        }

        private Color GetPixelWithXCheck(int x, int y)
        {
            if (x <= 0) return _sourceImage.GetPixel(0, y);
            if (x >= _sourceWidth) return _sourceImage.GetPixel(_sourceWidth - 1, y);
            return _sourceImage.GetPixel(x, y);
        }

        private Color GetPixelWithYCheck(int x, int y)
        {
            if (y <= 0) return _sourceImage.GetPixel(x, 0);
            if (y >= _sourceHeight) return _sourceImage.GetPixel(x, _sourceHeight - 1);
            return _sourceImage.GetPixel(x, y);
        }

        private void AddPixel(Color pixel)
        {
            _rSum += pixel.r;
            _gSum += pixel.g;
            _bSum += pixel.b;
        }

        private void SubstPixel(Color pixel)
        {
            _rSum -= pixel.r;
            _gSum -= pixel.g;
            _bSum -= pixel.b;
        }

        private void ResetSum()
        {
            _rSum = 0.0f;
            _gSum = 0.0f;
            _bSum = 0.0f;
        }

        Color CalcPixelFromSum()
        {
            return new Color(_rSum / _windowSize, _gSum / _windowSize, _bSum / _windowSize);
        }
    }
2 Likes

Is it any better/ more optimized than Elecman’s code?

Could someone please please explain how to test this? I have an image and I want to test it out in my scene, however when I want to set the mainTexture of the image it says its Read only. Im googling hours and hours, sorry I’m new to Unity, but I would really need your help.

Could you tell me how did you magate to test this? Im using Raw image but I dont know how much to set for radius to have the whole image blurred… And nothing is vissible :frowning:

My small benchmark test shown that “LinearBlur” by @Cardinalby much FASTER than @Elecman 's blur (about 8 to 10 times faster!). May be it does not so nice blur, but speed was more important for me.
However, posted code isn`t fully optimized and has a memory leak.

var blurred = new Texture2D(image.width, image.height, image.format, false);

Creating new Texture2D in loop is not a good idea. GC wont get them, even if its a local variable. Memory allocates every time and you can free it by calling DestroyImmediate(blurred); (this is the only way AFAIK).
So Id suggest to make this class static and use 2 static textures for all purposes. Also we can avoid calling this every pass: blurred.Apply();` and call it one time in the end.

IJob + BurstCompiled

Note: you can quite easily improve this job so it won’t perform any conversions by getting rid of these pedestrian Color[ ] inputs/outputs completely and replacing them with just an input NativeArray of texture.GetRawTextureData

using Unity.Mathematics;
using Unity.Collections;

[Unity.Burst.BurstCompile]
struct LinearBlurJob : Unity.Jobs.IJob
{
    NativeArray<float4> source, blurred;//float4 is simd, Color isn't, hence in/out conversions (trading memory for speed here)
    int width, height, boxSize, iterations, radius;
    public ColorArrayJob ( UnityEngine.Color[] source , int width , int height , int radius , int iterations )
    {
        this.source = new NativeArray<float4>( source.Length , Allocator.TempJob , NativeArrayOptions.UninitializedMemory );
        for( int i=source.Length-1 ; i!=-1 ; i-- )
        {
            var col = source[ i ];
            this.source[ i ] = new float4{ x=col.r , y=col.g , z=col.b , w=col.a };
        }
        this.blurred = new NativeArray<float4>( this.source , Allocator.TempJob );
        this.radius = radius;
        this.boxSize = radius * 2 + 1;
        this.width = width;
        this.height = height;
        this.iterations = iterations;
    }
    void Unity.Jobs.IJob.Execute ()
    {
        for( int iteration=0 ; iteration<iterations ; iteration++ )
        {
            // horizontal blur:
            blurred.CopyTo( source );
            for( int Y=0 ; Y<height ; ++Y )
            {
                float4 sum = default(float4);
                {
                    for( int x=-radius ; x<=radius ; ++x ) sum += GetValueSafe( source , x , Y );
                    blurred[ To1dIndex(0,Y) ] = sum/boxSize;
                }
                for( int X=1 ; X<width ; X++ )
                {
                    sum -= GetValueSafe( source , X-radius-1 , Y );
                    sum += GetValueSafe( source , X + radius , Y );
                    blurred[ To1dIndex(X,Y) ] = sum/boxSize;
                }
            }
            // vertical blur:
            blurred.CopyTo( source );
            for( int X=0 ; X<width ; X++ )
            {
                float4 sum = default(float4);
                {
                    for( int y=-radius ; y<=radius ; ++y ) sum += GetValueSafe( source , X , y );
                    blurred[ To1dIndex(X,0) ] = sum/boxSize;
                }
                for( int Y=1 ; Y<height ; ++Y )
                {
                    sum -= GetValueSafe( source , X , Y-radius-1 );
                    sum += GetValueSafe( source , X , Y+radius );
                    blurred[ To1dIndex(X,Y) ] = sum/boxSize;
                }
            }
        }
    }
    float4 GetValueSafe ( NativeArray<float4> array , int x , int y ) => array[ To1dIndex( math.clamp( x , 0 , width-1 ) , math.clamp( y , 0 , height-1 ) ) ];
    int To1dIndex ( int x , int y ) => y * width + x;
    public UnityEngine.Color[] GetResultsAndClose ()
    {
        UnityEngine.Color[] results = new UnityEngine.Color[ blurred.Length ];
        for( int i=blurred.Length-1 ; i!=-1 ; i-- )
        {
            var f4 = blurred[ i ];
            results[ i ] = new UnityEngine.Color{ r=f4.x , g=f4.y , b=f4.z , a=f4.w };
        }
        source.Dispose();
        blurred.Dispose();
        return results;
    }
}
1 Like

Here is an example of a mentioned improved IJob that blurs texture.GetRawTextureData (aka ushort) where texture format is R16 (16bit grayscale):
NativeUInt16Job

1 Like

How do I use it to blur my 2D Texture?

LinearBlur.ColorArrayJob is definitely a simplest option if you’re looking for that:

var blurJob = new LinearBlur.ColorArrayJob(
    texture.GetPixels() ,
    texture.width ,
    texture.height ,
    Mathf.Min( texture.width , texture.height )/200 ,
    5
);
blurJob.Schedule().Complete();
texture.SetPixels( blurJob.GetResultsAndClose() );
texture.Apply();

It allocates Color[ ] but it gets job done without much thinking about what is what.

Every other option requires you to think about data structures and matching those 1:1 precisely. For example, LinearBlur.NativeUInt16Job is dedicated for TextureFormat.R16 only and wont work with anything else. I created my auxiliary doc page so you can familiarize yourself with data received from texture.GetRawTextureData there: RawTextureDataProcessingExamples (start here)

EDIT: missing line added ( @nbg_yalta )

1 Like

Still not get it… my texture is RGB24 format, I tryied to use script from #12 post, but there is an error with ColorArrayJob method doesnt return anything… Also you have mentioned about improvement, but I have no idea how to do that…

(one line was missing, fixed, try now)

RGB24 struct looks like this:

struct RGB24 { public byte r, g, b; }

Simple as that. Naming etc. for these structures doesn’t matter. It’s memory layout what counts (byte order/map) and what you do with them after that.

1 Like

Excellent thread! It was helpful for me. Keep up!

Works great, thanks

Works but… keeps throwing warnings:
Internal: JobTempAlloc has allocations that are more than 4 frames old - this is not allowed and likely a leak
and errors
A Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to get more details.