//-------------------------------------------------------------- #region Convolution //-------------------------------------------------------------- /// <summary> /// Applies the a filter kernel to the specified image. /// </summary> /// <param name="image">The image.</param> /// <param name="kernel">The filter kernel. (Needs to be square.)</param> /// <param name="wrapMode">The texture address mode.</param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> or <paramref name="kernel"/> is <see langword="null"/>. /// </exception> /// <exception cref="ArgumentException"> /// <paramref name="kernel"/> is non-square. /// </exception> public static void Convolve(Image image, float[,] kernel, TextureAddressMode wrapMode) { if (image == null) throw new ArgumentNullException("image"); if (kernel == null) throw new ArgumentNullException("kernel"); if (kernel.GetLength(0) != kernel.GetLength(1)) throw new ArgumentException("Filter kernel needs to be square.", "kernel"); int width = image.Width; int height = image.Height; int kernelSize = kernel.GetLength(0); int kernelOffset = kernelSize / 2; var tmpImage = new Image(width, height, image.Format); Buffer.BlockCopy(image.Data, 0, tmpImage.Data, 0, image.Data.Length); // ReSharper disable AccessToDisposedClosure using (var tempImage4F = new ImageAccessor(tmpImage)) using (var image4F = new ImageAccessor(image)) { #if SINGLE_THREADED for (int y = 0; y < height; y++) #else Parallel.For(0, height, y => #endif { for (int x = 0; x < width; x++) { // Apply 2D kernel at (x, y). Vector4F color = new Vector4F(); for (int row = 0; row < kernelSize; row++) { int srcY = y + row - kernelOffset; for (int column = 0; column < kernelSize; column++) { int srcX = x + column - kernelOffset; color += kernel[row, column] * tempImage4F.GetPixel(srcX, srcY, wrapMode); } } image4F.SetPixel(x, y, color); } } #if !SINGLE_THREADED ); #endif } // ReSharper restore AccessToDisposedClosure }
private static void Resize2D(Image srcImage, Image dstImage, bool alphaTransparency, TextureAddressMode wrapMode, PolyphaseKernel kernelX, PolyphaseKernel kernelY) { var tmpImage = new Image(dstImage.Width, srcImage.Height, srcImage.Format); // ReSharper disable AccessToDisposedClosure using (var srcImage4F = new ImageAccessor(srcImage)) using (var tmpImage4F = new ImageAccessor(tmpImage)) using (var dstImage4F = new ImageAccessor(dstImage)) { // Resize horizontally: srcImage --> tmpImage { float scale = (float)tmpImage4F.Width / srcImage4F.Width; float inverseScale = 1.0f / scale; #if SINGLE_THREADED for (int y = 0; y < tmpImage4F.Height; y++) #else Parallel.For(0, tmpImage4F.Height, y => #endif { // Apply polyphase kernel horizontally. for (int x = 0; x < tmpImage4F.Width; x++) { float center = (x + 0.5f) * inverseScale; int left = (int)Math.Floor(center - kernelX.Width); int right = (int)Math.Ceiling(center + kernelX.Width); Debug.Assert(right - left <= kernelX.WindowSize); float totalRgbWeights = 0.0f; Vector4F sum = new Vector4F(); for (int i = 0; i < kernelX.WindowSize; i++) { Vector4F color = srcImage4F.GetPixel(left + i, y, wrapMode); //if (Numeric.IsNaN(color.X) || Numeric.IsNaN(color.Y) || Numeric.IsNaN(color.Z) || Numeric.IsNaN(color.W) // || color.X < 0 || color.Y < 0 || color.Z < 0 || color.W < 0 // || color.X > 1 || color.Y > 1 || color.Z > 1 || color.W > 1) // Debugger.Break(); const float alphaEpsilon = 1.0f / 256.0f; float alpha = alphaTransparency ? color.W + alphaEpsilon : 1.0f; float weight = kernelX.Weights[x, i]; float rgbWeight = weight * alpha; totalRgbWeights += rgbWeight; sum.X += color.X * rgbWeight; sum.Y += color.Y * rgbWeight; sum.Z += color.Z * rgbWeight; sum.W += color.W * weight; //if (Numeric.IsNaN(sum.X) || Numeric.IsNaN(sum.Y) || Numeric.IsNaN(sum.Z) || Numeric.IsNaN(sum.W) // || sum.X < 0 || sum.Y < 0 || sum.Z < 0 || sum.W < 0 // || sum.X > 1 || sum.Y > 1 || sum.Z > 1 || sum.W > 1) // Debugger.Break(); } float f = 1 / totalRgbWeights; sum.X *= f; sum.Y *= f; sum.Z *= f; //if (Numeric.IsNaN(sum.X) || Numeric.IsNaN(sum.Y) || Numeric.IsNaN(sum.Z) || Numeric.IsNaN(sum.W) // || sum.X < 0 || sum.Y < 0 || sum.Z < 0 || sum.W < 0 // || sum.X > 1 || sum.Y > 1 || sum.Z > 1 || sum.W > 1) // Debugger.Break(); tmpImage4F.SetPixel(x, y, sum); } } #if !SINGLE_THREADED ); #endif } // Resize vertically: tmpImage --> dstImage { float scale = (float)dstImage4F.Height / tmpImage4F.Height; float inverseScale = 1.0f / scale; #if SINGLE_THREADED for (int x = 0; x < dstImage4F.Width; x++) #else Parallel.For(0, dstImage4F.Width, x => #endif { // Apply polyphase kernel vertically. for (int y = 0; y < dstImage4F.Height; y++) { float center = (y + 0.5f) * inverseScale; int left = (int)Math.Floor(center - kernelY.Width); int right = (int)Math.Ceiling(center + kernelY.Width); Debug.Assert(right - left <= kernelY.WindowSize); float totalRgbWeights = 0.0f; Vector4F sum = new Vector4F(); for (int i = 0; i < kernelY.WindowSize; i++) { Vector4F color = tmpImage4F.GetPixel(x, left + i, wrapMode); const float alphaEpsilon = 1.0f / 256.0f; float alpha = alphaTransparency ? color.W + alphaEpsilon : 1.0f; float weight = kernelY.Weights[y, i]; float rgbWeight = weight * alpha; totalRgbWeights += rgbWeight; sum.X += color.X * rgbWeight; sum.Y += color.Y * rgbWeight; sum.Z += color.Z * rgbWeight; sum.W += color.W * weight; } float f = 1 / totalRgbWeights; sum.X *= f; sum.Y *= f; sum.Z *= f; dstImage4F.SetPixel(x, y, sum); } } #if !SINGLE_THREADED ); #endif } } // ReSharper restore AccessToDisposedClosure }
//-------------------------------------------------------------- /// <summary> /// Applies the a filter kernel to the specified image. /// </summary> /// <param name="image">The image.</param> /// <param name="kernel">The filter kernel. (Needs to be square.)</param> /// <param name="wrapMode">The texture address mode.</param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> or <paramref name="kernel"/> is <see langword="null"/>. /// </exception> /// <exception cref="ArgumentException"> /// <paramref name="kernel"/> is non-square. /// </exception> public static void Convolve(Image image, float[,] kernel, TextureAddressMode wrapMode) { if (image == null) throw new ArgumentNullException("image"); if (kernel == null) throw new ArgumentNullException("kernel"); if (kernel.GetLength(0) != kernel.GetLength(1)) throw new ArgumentException("Filter kernel needs to be square.", "kernel"); int width = image.Width; int height = image.Height; int kernelSize = kernel.GetLength(0); int kernelOffset = kernelSize / 2; var tmpImage = new Image(width, height, image.Format); Buffer.BlockCopy(image.Data, 0, tmpImage.Data, 0, image.Data.Length); // ReSharper disable AccessToDisposedClosure using (var tempImage4F = new ImageAccessor(tmpImage)) using (var image4F = new ImageAccessor(image)) { #if SINGLE_THREADED for (int y = 0; y < height; y++) #else Parallel.For(0, height, y => #endif { for (int x = 0; x < width; x++) { // Apply 2D kernel at (x, y). Vector4F color = new Vector4F(); for (int row = 0; row < kernelSize; row++) { int srcY = y + row - kernelOffset; for (int column = 0; column < kernelSize; column++) { int srcX = x + column - kernelOffset; color += kernel[row, column] * tempImage4F.GetPixel(srcX, srcY, wrapMode); } } image4F.SetPixel(x, y, color); } } #if !SINGLE_THREADED ); #endif } // ReSharper restore AccessToDisposedClosure }
/// <summary> /// Applies color keying to the specified image (<see cref="DataFormat.R32G32B32A32_FLOAT"/>). /// </summary> /// <param name="image">The image (<see cref="DataFormat.R32G32B32A32_FLOAT"/>).</param> /// <param name="colorKey">Color of the color key.</param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> is <see langword="null"/>. /// </exception> public static void ApplyColorKey(Image image, Vector4F colorKey) { if (image == null) throw new ArgumentNullException("image"); // ReSharper disable AccessToDisposedClosure using (var image4F = new ImageAccessor(image)) { #if SINGLE_THREADED for (int y = 0; y < image4F.Height; y++) #else Parallel.For(0, image4F.Height, y => #endif { for (int x = 0; x < image4F.Width; x++) { Vector4F color = image4F.GetPixel(x, y); if (Vector4F.AreNumericallyEqual(color, colorKey)) image4F.SetPixel(x, y, new Vector4F(color.X, color.Y, color.Z, 0)); } } #if !SINGLE_THREADED ); #endif } }
/// <summary> /// Rotates the image counter-clockwise 0°, 90°, 180°, or 270°. /// </summary> /// <param name="srcImage">The unrotated image.</param> /// <param name="dstImage">The rotated image.</param> /// <param name="degrees"> /// The angle in degrees. Allowed values: -270, -180, -90, 0, 90, 180, 270 /// </param> private static void Rotate(Image srcImage, Image dstImage, int degrees) { Debug.Assert(srcImage != null); Debug.Assert(dstImage != null); Debug.Assert(srcImage.Format == dstImage.Format); using (var src = new ImageAccessor(srcImage)) using (var dst = new ImageAccessor(dstImage)) { int width = srcImage.Width; int height = srcImage.Height; switch (degrees) { case -360: case 0: case 360: Debug.Assert(srcImage.Data.Length == dstImage.Data.Length); Buffer.BlockCopy(srcImage.Data, 0, dstImage.Data, 0, srcImage.Data.Length); break; case -180: case 180: Debug.Assert(srcImage.Width == dstImage.Width || srcImage.Height == dstImage.Height); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) dst.SetPixel(width - x - 1, height - y - 1, src.GetPixel(x, y)); break; case -270: case 90: Debug.Assert(srcImage.Width == dstImage.Height || srcImage.Height == dstImage.Width); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) dst.SetPixel(y, width - x - 1, src.GetPixel(x, y)); break; case -90: case 270: Debug.Assert(srcImage.Width == dstImage.Height || srcImage.Height == dstImage.Width); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) dst.SetPixel(height - y - 1, x, src.GetPixel(x, y)); break; default: Debug.Fail("Invalid rotation angle."); break; } } }
/// <summary> /// Scales the alpha values of the image to create the desired alpha test coverage. /// </summary> /// <param name="image">The image (<see cref="DataFormat.R32G32B32A32_FLOAT"/>).</param> /// <param name="referenceAlpha">The reference alpha.</param> /// <param name="desiredCoverage">The desired alpha test coverage.</param> /// <param name="premultipliedAlpha"> /// <see langword="true"/> if texture uses premultiplied alpha. /// </param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> is <see langword="null"/>. /// </exception> public static void ScaleAlphaToCoverage(Image image, float referenceAlpha, float desiredCoverage, bool premultipliedAlpha) { if (image == null) throw new ArgumentNullException("image"); // To reach the desired alpha test coverage, all alpha values need to be scaled // by a certain factor. One solution to find the scale factor is by doing a binary // search. float minAlphaScale = 0.0f; float maxAlphaScale = 8.0f; float alphaScale = 4.0f; // In the NVIDIA Texture Tools the binary search is limited to a hardcoded number // of steps. const int numberOfSteps = 10; for (int i = 0; i < numberOfSteps; i++) { float currentCoverage = GetAlphaTestCoverage(image, referenceAlpha, alphaScale); if (currentCoverage < desiredCoverage) minAlphaScale = alphaScale; else if (currentCoverage > desiredCoverage) maxAlphaScale = alphaScale; else break; alphaScale = (minAlphaScale + maxAlphaScale) / 2.0f; } if (premultipliedAlpha) { // ReSharper disable AccessToDisposedClosure using (var image4F = new ImageAccessor(image)) { #if SINGLE_THREADED for (int y = 0; y < image4F.Height; y++) #else Parallel.For(0, image4F.Height, y => #endif { for (int x = 0; x < image4F.Width; x++) { Vector4F color = image4F.GetPixel(x, y); float alpha = color.W; if (alpha > Numeric.EpsilonF) { // Undo premultiplication. float oneOverAlpha = 1 / alpha; color.X *= oneOverAlpha; color.Y *= oneOverAlpha; color.Z *= oneOverAlpha; // Scale alpha. alpha = MathHelper.Clamp(alpha * alphaScale, 0, 1); // Premultiply alpha. color.X *= alpha; color.Y *= alpha; color.Z *= alpha; color.W = alpha; } image4F.SetPixel(x, y, color); } } #if !SINGLE_THREADED ); #endif } // ReSharper restore AccessToDisposedClosure } else { // ReSharper disable AccessToDisposedClosure using (var image4F = new ImageAccessor(image)) { #if SINGLE_THREADED for (int y = 0; y < image4F.Height; y++) #else Parallel.For(0, image4F.Height, y => #endif { for (int x = 0; x < image4F.Width; x++) { Vector4F color = image4F.GetPixel(x, y); color.W = MathHelper.Clamp(color.W * alphaScale, 0, 1); image4F.SetPixel(x, y, color); } } #if !SINGLE_THREADED ); #endif } // ReSharper restore AccessToDisposedClosure } }
/// <summary> /// Expands the image from unsigned normalized values [0, 1] to signed normalized values /// [-1, 1]. (Assumes input data is normal map!) /// </summary> /// <param name="image">The image (<see cref="DataFormat.R32G32B32A32_FLOAT"/>).</param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> is <see langword="null"/>. /// </exception> public static void UnpackNormals(Image image) { if (image == null) throw new ArgumentNullException("image"); // ReSharper disable AccessToDisposedClosure using (var image4F = new ImageAccessor(image)) { #if SINGLE_THREADED for (int y = 0; y < image4F.Height; y++) #else Parallel.For(0, image4F.Height, y => #endif { for (int x = 0; x < image4F.Width; x++) { // Only for normal map: (byte)128 maps to (float)0. Vector4F color = image4F.GetPixel(x, y); color.X = color.X * 255 / 128 - 1; color.Y = color.Y * 255 / 128 - 1; color.Z = color.Z * 255 / 128 - 1; image4F.SetPixel(x, y, color); } } #if !SINGLE_THREADED ); #endif } // ReSharper restore AccessToDisposedClosure }
/// <summary> /// Prepares a normal map for compression using DXT5 (a.k.a. DXT5nm). /// </summary> /// <param name="image">The image (<see cref="DataFormat.R32G32B32A32_FLOAT"/>).</param> /// <param name="invertY"><see langword="true"/> to invert the y component.</param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> is <see langword="null"/>. /// </exception> public static void ProcessNormals(Image image, bool invertY) { if (image == null) throw new ArgumentNullException("image"); float sign = invertY ? -1 : 1; // ReSharper disable AccessToDisposedClosure using (var image4F = new ImageAccessor(image)) { #if SINGLE_THREADED for (int y = 0; y < image4F.Height; y++) #else Parallel.For(0, image4F.Height, y => #endif { for (int x = 0; x < image4F.Width; x++) { Vector4F v = image4F.GetPixel(x, y); Vector3F normal = new Vector3F(v.X, v.Y, v.Z); // Renormalize normals. (Important for higher mipmap levels.) if (!normal.TryNormalize()) normal = new Vector3F(0, 0, 1); // Convert to DXT5nm (xGxR). v.X = 0.0f; v.Y = sign * normal.Y * 0.5f + 0.5f; v.Z = 0.0f; v.W = normal.X * 0.5f + 0.5f; image4F.SetPixel(x, y, v); } } #if !SINGLE_THREADED ); #endif } // ReSharper restore AccessToDisposedClosure }
/// <summary> /// Premultiplies the alpha value of the specified image. /// </summary> /// <param name="image">The image (<see cref="DataFormat.R32G32B32A32_FLOAT"/>).</param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> is <see langword="null"/>. /// </exception> public static void PremultiplyAlpha(Image image) { if (image == null) throw new ArgumentNullException("image"); // ReSharper disable AccessToDisposedClosure using (var image4F = new ImageAccessor(image)) { #if SINGLE_THREADED for (int y = 0; y < image4F.Height; y++) #else Parallel.For(0, image4F.Height, y => #endif { for (int x = 0; x < image4F.Width; x++) { Vector4F color = image4F.GetPixel(x, y); if (color.W < 1.0f) { color.X *= color.W; color.Y *= color.W; color.Z *= color.W; image4F.SetPixel(x, y, color); } } } #if !SINGLE_THREADED ); #endif } // ReSharper restore AccessToDisposedClosure }
/// <summary> /// Converts the specified image from linear space to gamma space. /// </summary> /// <param name="image">The image (<see cref="DataFormat.R32G32B32A32_FLOAT"/>).</param> /// <param name="gamma">The gamma value.</param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> is <see langword="null"/>. /// </exception> public static void LinearToGamma(Image image, float gamma) { if (image == null) throw new ArgumentNullException("image"); if (Numeric.AreEqual(gamma, 1.0f)) return; if (gamma <= 0) { string message = string.Format( CultureInfo.InvariantCulture, "Invalid gamma value {0}. The gamma correction value must be greater than 0.", gamma); throw new ArgumentException(message, "gamma"); } float gammaCorrection = 1.0f / gamma; // ReSharper disable AccessToDisposedClosure using (var image4F = new ImageAccessor(image)) { #if SINGLE_THREADED for (int y = 0; y < image4F.Height; y++) #else Parallel.For(0, image4F.Height, y => #endif { for (int x = 0; x < image4F.Width; x++) { Vector4F color = image4F.GetPixel(x, y); color.X = (float)Math.Pow(color.X, gammaCorrection); color.Y = (float)Math.Pow(color.Y, gammaCorrection); color.Z = (float)Math.Pow(color.Z, gammaCorrection); image4F.SetPixel(x, y, color); } } #if !SINGLE_THREADED ); #endif } // ReSharper restore AccessToDisposedClosure }
/// <summary> /// Mirrors the image vertically. /// </summary> /// <param name="image">The image.</param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> is <see langword="null"/>. /// </exception> public static void FlipY(Image image) { if (image == null) throw new ArgumentNullException("image"); using (var image4F = new ImageAccessor(image)) { int width = image4F.Width; int height = image4F.Height; int halfHeight = height / 2; for (int y0 = 0; y0 < halfHeight; y0++) { for (int x = 0; x < width; x++) { int y1 = height - y0 - 1; Vector4F color0 = image4F.GetPixel(x, y0); Vector4F color1 = image4F.GetPixel(x, y1); image4F.SetPixel(x, y0, color1); image4F.SetPixel(x, y1, color0); } } } }
/// <summary> /// Mirrors the image horizontally. /// </summary> /// <param name="image">The image.</param> /// <exception cref="ArgumentNullException"> /// <paramref name="image"/> is <see langword="null"/>. /// </exception> public static void FlipX(Image image) { if (image == null) throw new ArgumentNullException("image"); using (var image4F = new ImageAccessor(image)) { int width = image4F.Width; int height = image4F.Height; int halfWidth = width / 2; for (int y = 0; y < height; y++) { for (int x0 = 0; x0 < halfWidth; x0++) { int x1 = width - x0 - 1; Vector4F color0 = image4F.GetPixel(x0, y); Vector4F color1 = image4F.GetPixel(x1, y); image4F.SetPixel(x0, y, color1); image4F.SetPixel(x1, y, color0); } } } }