Popular Post Rick Brewster Posted October 17, 2023 Popular Post Share Posted October 17, 2023 This is a GPU-accelerated effect that renders a blur (Gaussian or Bokeh) with a radius that increases as it gets further from the center of the image. This can be used to focus on a subject by blurring the area around it. I'm planning to make this a built-in effect starting with Paint.NET v5.1 (it still needs some usability work, IMO). For now, enjoy it as a plugin! NOTE: At higher radius and quality levels, this effect can be very GPU intensive! Download: IrisBlur.1.0.zip (source code is below!) This effect will be placed in the Blurs menu within Effects: Making use of the new GaussianBlurMapEffect and BokehBlurMapEffect, which are Direct2D effects available for all GPU plugins to use starting with Paint.NET v5.0.10, this effect creates a "blur map" consisting of a radial gradient that starts at 0 in the center and ramps up to the specified radius value at the edges. Here's an example using this freely licensed stock photograph (credit Sheng L): There's also a checkbox to reverse the blur, which produces a rather strange but interesting look. I'll use this other freely available stock photo (credit Joshua Rawson-Harris) as an example: Gamma can also be modified, just like with the built-in Gaussian and Bokeh blur effects. This can be used to accentuate the brightness of the blurred areas. When you crank up the gamma, you will probably also need to crank up the quality in order to avoid some rendering artifacts (just like you have to do with the built-in Gaussian and Bokeh blur effects, btw). Here are how the various blur and ramp parameters affect the blur radius. The x-axis is distance from the center of the image (or the modified center point), and the y-axis is the blur radius. The fovea value is used to specify the non-blurred region at the center, then the ramp length determines the length of the ramp between 0% and 100% blur, and then the ramp exponent determines how quickly the blur radius increases. Source Code and Developer Notes A "blur map" is an image where the alpha value of each pixel is used as the radius value, either directly or normalized in the range [0,1] and then scaled to the specified maximum blur radius (use the MapIsNormalized property to choose). See documentation links above for more information. It's important to note that the "blur map" effects are not limited to a radial gradient for the blur map. That's just what this Paint.NET GPU effect is doing. You could write your own GPU effect that uses a linear gradient (e.g. blurry at the top, in-focus at the bottom), or a sine-wave of some sorts, or just about anything. Want the blur radius to be proportional to the brightness? Then plug Environment.SourceImage into a GrayscaleEffect and then use that as the "map" input for the blur map effect. The blur map effects are implemented as approximations. It's computationally prohibitive to actually render each pixel with its own blur radius. Instead of doing that, several copies of the image are separately blurred at increasing blur radius values (the number of copies is determined by the quality slider, which is used to set the RadiusLevels or StandardDeviationLevels properties). Then, the blur map is used to select which 2 of these images should be cross-faded in order to produce an approximation of the correct blur radius at that pixel. It is not possible to avoid blurring any part of the image -- all pixels must be blurred at all levels, so the performance of these effects is greatly influenced by those two "levels" properties. Here's the source code so you can play around with it yourself. CodeLab v6.10+ is required. Spoiler // Name: Iris Blur // Submenu: Blurs // Author: Rick Brewster // Title: Iris Blur // Version: 1.0 // Desc: Renders a blur (Bokeh or Gaussian) that uses a smaller radius at the center and gradually increases to a large radius at the edges. // Keywords: blur|gaussian|bokeh|gradient|iris|gpu // URL: https://forums.getpaint.net/topic/123583-iris-blur/ // Working Space Color Context #region UICode PanSliderControl Center = new Vector2Double(0, 0); // Center ListBoxControl<BlurTypeEnum> BlurType = 0; // Blur Type|Gaussian|Bokeh DoubleSliderControl Fovea = 0.1; // [0,1] Fovea DoubleSliderControl BlurRadius = 50; // [0,300] Blur Radius DoubleSliderControl BlurRadiusRampLength = 1; // [0.01,3] Radius Ramp Length DoubleSliderControl BlurRadiusRampExponent = 1.5; // [0.25,4] Radius Ramp Exponent DoubleSliderControl Gamma = 2.2; // [0.01,6] Gamma CheckboxControl Reversed = false; // Reversed IntSliderControl Quality = 4; // [1,8] Quality #endregion enum BlurTypeEnum { Gaussian = 0, Bokeh = 1 } protected override IDeviceImage OnCreateOutput(IDeviceContext deviceContext) { // Normally GPU effect plugins operate in linear gamma space (1.0), which means // that the input image (active layer) is converted from sRGB (gamma 2.2) to // scRGB, aka linear sRGB. For this effect we want to allow the user to specify // the gamma, similarly to the built-in Gaussian and Bokeh effects. For // performance, "Working Space Color Context" is specified at the top, which // prevents this automatic gamma conversion (the default is "Working Space Linear"). // We can then use the ConvertGammaEffect to do the gamma conversion based on // the Gamma slider. Without these we would need additional rendering steps // (additional effects in the graph), which would reduce performance a little. // You can still do this without specifying Working Space Color Context, but // you will lose some performance. Not very much on most GPUs, but it can affect // slower ones, especially older Intel iGPUs, and when using CPU rendering. RectFloat renderBounds = Environment.Selection.RenderBounds; BlurTypeEnum blurType = (BlurTypeEnum)BlurType; // Blur is in units of standard deviations for Gaussian, and pixels for Radius // The StandardDeviation class can be used to convert between the two. float blurValue = (float)(blurType == BlurTypeEnum.Bokeh ? BlurRadius : StandardDeviation.FromRadius(BlurRadius)); if (blurValue == 0.0) { // Zero blur is a no-op, so just jump straight to that return Environment.SourceImage; } Point2Float centerNorm = (Point2Float)Center; Point2Float center = renderBounds.Center + ((Vector2Float)centerNorm * (Vector2Float)renderBounds.Center); // The "fovea" is the center part of the image where there is no blur. It is specified // as a normalized value. 0.1 is 10% of the image radius, 1.0 is 100% of it. The image // "radius" is the maximum of its width and height, divided by 2. float fovea = (float)Fovea; float sourceRadius = Math.Max(renderBounds.Width, renderBounds.Height) / 2.0f; float blurRadiusRampLength = (float)BlurRadiusRampLength; float blurRadiusRampExponent = (float)BlurRadiusRampExponent; float gamma = (float)Gamma; int quality = Quality; /* Quality is used to determine the value of the Optimization property for Gaussian Blur ("GBOpt"), and the Quality property for Bokeh Blur (the value is used directly). GBOpt values of 1,2,3,4 map to the Optimization values of Speed, Balanced, Quality, HighQuality The tessellation quality is used for the Levels property on the blur map effects This specifies how many copies of the image are blended together, each one at an increasing level of blur, in order to simulate having a per-pixel blur radius. The sequence is calculated as TessQ[1]=1, TessQ[2]=2, TessQ[n] = TessQ[n-1] + (TessQ[n-1] - TessQ[n-2] + 1) (in other words, the delta increases by 1 for each successive value) Quality GBOpt TessQ +------+-------+-------+ | 1 | 1 | 1 | +------+-------+-------+ | 2 | 2 | 2 | +------+-------+-------+ | 3 | 3 | 4 | +------+-------+-------+ | 4 | 3 | 7 | +------+-------+-------+ | 5 | 3 | 11 | +------+-------+-------+ | 6 | 4 | 16 | +------+-------+-------+ | 7 | 4 | 22 | +------+-------+-------+ | 8 | 4 | 29 | +------+-------+-------+ */ int tessellationQuality = Quality switch { 1 => 1, 2 => 2, 3 => 4, 4 => 7, 5 => 11, 6 => 16, 7 => 22, 8 => 29, _ => 29 }; // We don't yet have support for custom pixel shaders in CodeLab, so we instead // will string together some of the HLSL effects. Each HLSL effect instance // executes 1 HLSL instruction. Direct2D is able to link these simple effects // together in a way that maintains reasonable performance by avoiding extra // rendering passes. It's not as good as proper inlining, or writing a custom // shader directly, but it's good enough. IDeviceEffect perPixelRadius; { // ((((distance(center,scenePos)/(rampLength*sourceRadius))-fovea)/fovea)^rampExponent)*radius // First, plot the Euclidean distance from the center point // The "scene position" is just the (x,y) coordinate for that pixel (but // with a center-of-pixel offset, e.g. (1.5,1.5) instead of (1.0,1.0)) perPixelRadius = new HlslBinaryFunctionEffect( deviceContext, HlslBinaryFunction.Distance, new Vector4Float((Vector2Float)center, Vector2Float.Zero), new ScenePositionEffect(deviceContext, ScenePositionFormat.XY00)); // Normalize [0,sourceRadius] to [0,1] perPixelRadius = new HlslBinaryOperatorEffect( deviceContext, perPixelRadius, HlslBinaryOperator.Multiply, new Vector4Float((float)(1.0 / ((double)blurRadiusRampLength * sourceRadius)))); if (Reversed) { perPixelRadius = new HlslBinaryOperatorEffect( deviceContext, new Vector4Float(1), HlslBinaryOperator.Subtract, perPixelRadius); } // Subtract fovea perPixelRadius = new HlslBinaryOperatorEffect( deviceContext, perPixelRadius, HlslBinaryOperator.Subtract, new Vector4Float(fovea)); // Divide by fovea. So if fovea is 0.2, the previous operations would leave us with values in the range [-0.2, +0.8] // We want to stretch the [0, 0.8] range to [0, 1] perPixelRadius = new HlslBinaryOperatorEffect( deviceContext, perPixelRadius, HlslBinaryOperator.Multiply, new Vector4Float(1.0f / (fovea == 1.0f ? 1.0f : (1.0f - fovea)))); // Saturate to [0,1] perPixelRadius = new HlslUnaryFunctionEffect( deviceContext, HlslUnaryFunction.Saturate, perPixelRadius); // Apply the exponent if (blurRadiusRampExponent != 1.0f) { perPixelRadius = new HlslBinaryFunctionEffect( deviceContext, HlslBinaryFunction.Pow, perPixelRadius, new Vector4Float(blurRadiusRampExponent)); } // Finally, multiply by the blur radius perPixelRadius = new HlslBinaryOperatorEffect( deviceContext, perPixelRadius, HlslBinaryOperator.Multiply, new Vector4Float(blurValue)); } IDeviceImage sourceImage = Environment.SourceImage; { ConvertGammaEffect gammaEffect = new ConvertGammaEffect(deviceContext); gammaEffect.Properties.Input.Set(sourceImage); gammaEffect.Properties.Mode.SetValue(ConvertGammaMode.CustomExponent); gammaEffect.Properties.CustomExponent.SetValue(gamma); sourceImage = gammaEffect; } IDeviceEffect blurEffect; switch (blurType) { case BlurTypeEnum.Gaussian: GaussianBlurMapOptimization optimization = Quality switch { <= 1 => GaussianBlurMapOptimization.Speed, <= 2 => GaussianBlurMapOptimization.Balanced, <= 5 => GaussianBlurMapOptimization.Quality, <= 8 => GaussianBlurMapOptimization.HighQuality, _ => GaussianBlurMapOptimization.HighQuality }; GaussianBlurMapEffect gaussianBlur = new GaussianBlurMapEffect(deviceContext); gaussianBlur.Properties.StandardDeviationMap.Set(perPixelRadius); gaussianBlur.Properties.StandardDeviationLevels.SetValue(tessellationQuality); gaussianBlur.Properties.MaxStandardDeviation.SetValue(blurValue); gaussianBlur.Properties.MapIsNormalized.SetValue(false); gaussianBlur.Properties.Optimization.SetValue(optimization); gaussianBlur.Properties.BorderMode.SetValue(BorderMode.Hard); blurEffect = gaussianBlur; break; case BlurTypeEnum.Bokeh: BokehBlurMapEffect bokehBlur = new BokehBlurMapEffect(deviceContext); bokehBlur.Properties.RadiusMap.Set(perPixelRadius); bokehBlur.Properties.RadiusLevels.SetValue(tessellationQuality); bokehBlur.Properties.MaxRadius.SetValue(blurValue); bokehBlur.Properties.MapIsNormalized.SetValue(false); bokehBlur.Properties.Quality.SetValue(quality); bokehBlur.Properties.EdgeMode.SetValue(BokehBlurMapEdgeMode.Mirror); blurEffect = bokehBlur; break; default: throw new InternalErrorException(); } blurEffect.SetInput(0, sourceImage); IDeviceImage outputImage = blurEffect; { ConvertGammaEffect invGammaEffect = new ConvertGammaEffect(deviceContext); invGammaEffect.Properties.Input.Set(outputImage); invGammaEffect.Properties.Mode.SetValue(ConvertGammaMode.InverseCustomExponent); invGammaEffect.Properties.CustomExponent.SetValue(gamma); outputImage = invGammaEffect; } return outputImage; } 6 1 3 Quote The Paint.NET Blog: https://blog.getpaint.net/ Donations are always appreciated! https://www.getpaint.net/donate.html Link to comment Share on other sites More sharing options...
Pixey Posted October 18, 2023 Share Posted October 18, 2023 What a lovely effect @Rick Brewster 😍 5 Quote How I made Jennifer & Halle in Paint.net My Gallery | My Deviant Art "Rescuing one animal may not change the world, but for that animal their world is changed forever!" anon. Link to comment Share on other sites More sharing options...
AndrewDavid Posted October 21, 2023 Share Posted October 21, 2023 @Rick Brewster Iris BlurTest image - 5837 X 7098 - played with controls to have time to look at task manager. Clearly CPU in use. Performed the same with GPU Hue rotate to see what GPU Usage should look like. Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.