Browse Source

Use PixelAccessor

Former-commit-id: 9b7174022ac6745534acff3ed69718d143e91e6f
Former-commit-id: f8d1e182dec104b3c77fec79cd4d7c00a299a70f
Former-commit-id: 98f22a9d62b5133aeba4b4525a71b2e6c2b18133
pull/1/head
James Jackson-South 10 years ago
parent
commit
3dbad0c44a
  1. 37
      src/ImageProcessorCore/Common/Helpers/ImageMaths.cs
  2. 32
      src/ImageProcessorCore/Filters/Alpha.cs
  3. 51
      src/ImageProcessorCore/Filters/BackgroundColor.cs
  4. 10
      src/ImageProcessorCore/Filters/Binarization/Threshold.cs
  5. 59
      src/ImageProcessorCore/Filters/Blend.cs
  6. 32
      src/ImageProcessorCore/Filters/Brightness.cs
  7. 9
      src/ImageProcessorCore/Filters/ColorMatrix/ColorMatrixFilter.cs
  8. 35
      src/ImageProcessorCore/Filters/Contrast.cs
  9. 16
      src/ImageProcessorCore/Filters/Convolution/Convolution2DFilter.cs
  10. 10
      src/ImageProcessorCore/Filters/Convolution/Convolution2PassFilter.cs
  11. 10
      src/ImageProcessorCore/Filters/Convolution/ConvolutionFilter.cs
  12. 31
      src/ImageProcessorCore/Filters/Glow.cs
  13. 31
      src/ImageProcessorCore/Filters/Invert.cs
  14. 68
      src/ImageProcessorCore/Filters/Pixelate.cs
  15. 28
      src/ImageProcessorCore/Filters/Vignette.cs
  16. 37
      src/ImageProcessorCore/Formats/Bmp/BmpEncoderCore.cs
  17. 2
      src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs
  18. 43
      src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs
  19. 21
      src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs
  20. 103
      src/ImageProcessorCore/IImage.cs
  21. 106
      src/ImageProcessorCore/IImageBase.cs
  22. 97
      src/ImageProcessorCore/Image.cs
  23. 207
      src/ImageProcessorCore/ImageBase.cs
  24. 8
      src/ImageProcessorCore/ImageExtensions.cs
  25. 164
      src/ImageProcessorCore/PixelAccessor.cs
  26. 31
      src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs
  27. 36
      src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs
  28. 30
      src/ImageProcessorCore/Samplers/Processors/CropProcessor.cs
  29. 55
      src/ImageProcessorCore/Samplers/Processors/EntropyCropProcessor.cs
  30. 185
      src/ImageProcessorCore/Samplers/Processors/ResizeProcessor.cs
  31. 150
      src/ImageProcessorCore/Samplers/Processors/RotateFlip.cs
  32. 43
      src/ImageProcessorCore/Samplers/Processors/RotateProcessor.cs
  33. 43
      src/ImageProcessorCore/Samplers/Processors/SkewProcessor.cs
  34. 7
      tests/ImageProcessorCore.Benchmarks/Image/GetSetPixel.cs
  35. 10
      tests/ImageProcessorCore.Benchmarks/Samplers/Crop.cs
  36. 10
      tests/ImageProcessorCore.Benchmarks/Samplers/Resize.cs
  37. 24
      tests/ImageProcessorCore.Tests/Formats/BitmapTests.cs
  38. 127
      tests/ImageProcessorCore.Tests/Formats/EncoderDecoderTests.cs
  39. 10
      tests/ImageProcessorCore.Tests/Formats/PngTests.cs
  40. 16
      tests/ImageProcessorCore.Tests/Processors/Filters/FilterTests.cs
  41. 33
      tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

37
src/ImageProcessorCore/Common/Helpers/ImageMaths.cs

@ -171,35 +171,35 @@ namespace ImageProcessorCore
Point topLeft = new Point(); Point topLeft = new Point();
Point bottomRight = new Point(); Point bottomRight = new Point();
Func<ImageBase, int, int, float, bool> delegateFunc; Func<PixelAccessor, int, int, float, bool> delegateFunc;
// Determine which channel to check against // Determine which channel to check against
switch (channel) switch (channel)
{ {
case RgbaComponent.R: case RgbaComponent.R:
delegateFunc = (imageBase, x, y, b) => Math.Abs(imageBase[x, y].R - b) > Epsilon; delegateFunc = (pixels, x, y, b) => Math.Abs(pixels[x, y].R - b) > Epsilon;
break; break;
case RgbaComponent.G: case RgbaComponent.G:
delegateFunc = (imageBase, x, y, b) => Math.Abs(imageBase[x, y].G - b) > Epsilon; delegateFunc = (pixels, x, y, b) => Math.Abs(pixels[x, y].G - b) > Epsilon;
break; break;
case RgbaComponent.A: case RgbaComponent.A:
delegateFunc = (imageBase, x, y, b) => Math.Abs(imageBase[x, y].A - b) > Epsilon; delegateFunc = (pixels, x, y, b) => Math.Abs(pixels[x, y].A - b) > Epsilon;
break; break;
default: default:
delegateFunc = (imageBase, x, y, b) => Math.Abs(imageBase[x, y].B - b) > Epsilon; delegateFunc = (pixels, x, y, b) => Math.Abs(pixels[x, y].B - b) > Epsilon;
break; break;
} }
Func<ImageBase, int> getMinY = imageBase => Func<PixelAccessor, int> getMinY = pixels =>
{ {
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
if (delegateFunc(imageBase, x, y, componentValue)) if (delegateFunc(pixels, x, y, componentValue))
{ {
return y; return y;
} }
@ -209,13 +209,13 @@ namespace ImageProcessorCore
return 0; return 0;
}; };
Func<ImageBase, int> getMaxY = imageBase => Func<PixelAccessor, int> getMaxY = pixels =>
{ {
for (int y = height - 1; y > -1; y--) for (int y = height - 1; y > -1; y--)
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
if (delegateFunc(imageBase, x, y, componentValue)) if (delegateFunc(pixels, x, y, componentValue))
{ {
return y; return y;
} }
@ -225,13 +225,13 @@ namespace ImageProcessorCore
return height; return height;
}; };
Func<ImageBase, int> getMinX = imageBase => Func<PixelAccessor, int> getMinX = pixels =>
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
if (delegateFunc(imageBase, x, y, componentValue)) if (delegateFunc(pixels, x, y, componentValue))
{ {
return x; return x;
} }
@ -241,13 +241,13 @@ namespace ImageProcessorCore
return 0; return 0;
}; };
Func<ImageBase, int> getMaxX = imageBase => Func<PixelAccessor, int> getMaxX = pixels =>
{ {
for (int x = width - 1; x > -1; x--) for (int x = width - 1; x > -1; x--)
{ {
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
if (delegateFunc(imageBase, x, y, componentValue)) if (delegateFunc(pixels, x, y, componentValue))
{ {
return x; return x;
} }
@ -257,10 +257,13 @@ namespace ImageProcessorCore
return height; return height;
}; };
topLeft.Y = getMinY(bitmap); using (PixelAccessor bitmapPixels = bitmap.Lock())
topLeft.X = getMinX(bitmap); {
bottomRight.Y = (getMaxY(bitmap) + 1).Clamp(0, height); topLeft.Y = getMinY(bitmapPixels);
bottomRight.X = (getMaxX(bitmap) + 1).Clamp(0, width); topLeft.X = getMinX(bitmapPixels);
bottomRight.Y = (getMaxY(bitmapPixels) + 1).Clamp(0, height);
bottomRight.X = (getMaxX(bitmapPixels) + 1).Clamp(0, width);
}
return GetBoundingRectangle(topLeft, bottomRight); return GetBoundingRectangle(topLeft, bottomRight);
} }

32
src/ImageProcessorCore/Filters/Alpha.cs

@ -42,22 +42,28 @@ namespace ImageProcessorCore.Filters
int endX = sourceRectangle.Right; int endX = sourceRectangle.Right;
Vector4 alphaVector = new Vector4(1, 1, 1, alpha); Vector4 alphaVector = new Vector4(1, 1, 1, alpha);
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
startY, using (PixelAccessor targetPixels = target.Lock())
endY, {
y => Parallel.For(
{ startY,
if (y >= sourceY && y < sourceBottom) endY,
y =>
{ {
for (int x = startX; x < endX; x++) if (y >= sourceY && y < sourceBottom)
{ {
Vector4 color = Color.ToNonPremultiplied(source[x, y]).ToVector4(); for (int x = startX; x < endX; x++)
color *= alphaVector; {
target[x, y] = Color.FromNonPremultiplied(new Color(color)); Vector4 color = Color.ToNonPremultiplied(sourcePixels[x, y]).ToVector4();
color *= alphaVector;
targetPixels[x, y] = Color.FromNonPremultiplied(new Color(color));
}
this.OnRowProcessed();
} }
this.OnRowProcessed(); });
}
}); }
} }
} }
} }

51
src/ImageProcessorCore/Filters/BackgroundColor.cs

@ -41,33 +41,38 @@ namespace ImageProcessorCore.Filters
int endX = sourceRectangle.Right; int endX = sourceRectangle.Right;
Color backgroundColor = this.Value; Color backgroundColor = this.Value;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
startY, using (PixelAccessor targetPixels = target.Lock())
endY, {
y => Parallel.For(
{ startY,
if (y >= sourceY && y < sourceBottom) endY,
{ y =>
for (int x = startX; x < endX; x++)
{ {
Color color = source[x, y]; if (y >= sourceY && y < sourceBottom)
float a = color.A;
if (a < 1 && a > 0)
{ {
color = Color.Lerp(color, backgroundColor, .5f); for (int x = startX; x < endX; x++)
} {
Color color = sourcePixels[x, y];
float a = color.A;
if (Math.Abs(a) < Epsilon) if (a < 1 && a > 0)
{ {
color = backgroundColor; color = Color.Lerp(color, backgroundColor, .5f);
} }
target[x, y] = color; if (Math.Abs(a) < Epsilon)
} {
this.OnRowProcessed(); color = backgroundColor;
} }
});
targetPixels[x, y] = color;
}
this.OnRowProcessed();
}
});
}
} }
} }
} }

10
src/ImageProcessorCore/Filters/Binarization/Threshold.cs

@ -60,7 +60,10 @@ namespace ImageProcessorCore.Filters
int startX = sourceRectangle.X; int startX = sourceRectangle.X;
int endX = sourceRectangle.Right; int endX = sourceRectangle.Right;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
using (PixelAccessor targetPixels = target.Lock())
{
Parallel.For(
startY, startY,
endY, endY,
y => y =>
@ -69,14 +72,15 @@ namespace ImageProcessorCore.Filters
{ {
for (int x = startX; x < endX; x++) for (int x = startX; x < endX; x++)
{ {
Color color = source[x, y]; Color color = sourcePixels[x, y];
// Any channel will do since it's greyscale. // Any channel will do since it's greyscale.
target[x, y] = color.B >= threshold ? upper : lower; targetPixels[x, y] = color.B >= threshold ? upper : lower;
} }
this.OnRowProcessed(); this.OnRowProcessed();
} }
}); });
}
} }
} }
} }

59
src/ImageProcessorCore/Filters/Blend.cs

@ -15,7 +15,7 @@ namespace ImageProcessorCore.Filters
/// <summary> /// <summary>
/// The image to blend. /// The image to blend.
/// </summary> /// </summary>
private readonly ImageBase toBlend; private readonly ImageBase blend;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Blend"/> class. /// Initializes a new instance of the <see cref="Blend"/> class.
@ -28,7 +28,7 @@ namespace ImageProcessorCore.Filters
public Blend(ImageBase image, int alpha = 100) public Blend(ImageBase image, int alpha = 100)
{ {
Guard.MustBeBetweenOrEqualTo(alpha, 0, 100, nameof(alpha)); Guard.MustBeBetweenOrEqualTo(alpha, 0, 100, nameof(alpha));
this.toBlend = image; this.blend = image;
this.Value = alpha; this.Value = alpha;
} }
@ -44,38 +44,43 @@ namespace ImageProcessorCore.Filters
int sourceBottom = sourceRectangle.Bottom; int sourceBottom = sourceRectangle.Bottom;
int startX = sourceRectangle.X; int startX = sourceRectangle.X;
int endX = sourceRectangle.Right; int endX = sourceRectangle.Right;
Rectangle bounds = this.toBlend.Bounds; Rectangle bounds = this.blend.Bounds;
float alpha = this.Value / 100f; float alpha = this.Value / 100f;
Parallel.For( using (PixelAccessor toBlendPixels = this.blend.Lock())
startY, using (PixelAccessor sourcePixels = source.Lock())
endY, using (PixelAccessor targetPixels = target.Lock())
y => {
{ Parallel.For(
if (y >= sourceY && y < sourceBottom) startY,
{ endY,
for (int x = startX; x < endX; x++) y =>
{ {
Color color = source[x, y]; if (y >= sourceY && y < sourceBottom)
if (bounds.Contains(x, y))
{ {
Color blendedColor = this.toBlend[x, y]; for (int x = startX; x < endX; x++)
if (blendedColor.A > 0)
{ {
// Lerping colors is dependent on the alpha of the blended color Color color = sourcePixels[x, y];
float alphaFactor = alpha > 0 ? alpha : blendedColor.A;
color = Color.Lerp(color, blendedColor, alphaFactor);
}
}
target[x, y] = color; if (bounds.Contains(x, y))
} {
Color blendedColor = toBlendPixels[x, y];
this.OnRowProcessed(); if (blendedColor.A > 0)
} {
}); // Lerping colors is dependent on the alpha of the blended color
float alphaFactor = alpha > 0 ? alpha : blendedColor.A;
color = Color.Lerp(color, blendedColor, alphaFactor);
}
}
targetPixels[x, y] = color;
}
this.OnRowProcessed();
}
});
}
} }
} }
} }

32
src/ImageProcessorCore/Filters/Brightness.cs

@ -41,25 +41,29 @@ namespace ImageProcessorCore.Filters
int startX = sourceRectangle.X; int startX = sourceRectangle.X;
int endX = sourceRectangle.Right; int endX = sourceRectangle.Right;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
startY, using (PixelAccessor targetPixels = target.Lock())
endY, {
y => Parallel.For(
{ startY,
if (y >= sourceY && y < sourceBottom) endY,
y =>
{ {
for (int x = startX; x < endX; x++) if (y >= sourceY && y < sourceBottom)
{ {
Color color = Color.Expand(source[x, y]); for (int x = startX; x < endX; x++)
{
Color color = Color.Expand(sourcePixels[x, y]);
Vector3 vector3 = color.ToVector3(); Vector3 vector3 = color.ToVector3();
vector3 += new Vector3(brightness); vector3 += new Vector3(brightness);
target[x, y] = Color.Compress(new Color(vector3, color.A)); targetPixels[x, y] = Color.Compress(new Color(vector3, color.A));
}
this.OnRowProcessed();
} }
this.OnRowProcessed(); });
} }
});
} }
} }
} }

9
src/ImageProcessorCore/Filters/ColorMatrix/ColorMatrixFilter.cs

@ -28,7 +28,10 @@ namespace ImageProcessorCore.Filters
int endX = sourceRectangle.Right; int endX = sourceRectangle.Right;
Matrix4x4 matrix = this.Matrix; Matrix4x4 matrix = this.Matrix;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
using (PixelAccessor targetPixels = target.Lock())
{
Parallel.For(
startY, startY,
endY, endY,
y => y =>
@ -37,11 +40,13 @@ namespace ImageProcessorCore.Filters
{ {
for (int x = startX; x < endX; x++) for (int x = startX; x < endX; x++)
{ {
target[x, y] = this.ApplyMatrix(source[x, y], matrix); targetPixels[x, y] = this.ApplyMatrix(sourcePixels[x, y], matrix);
} }
this.OnRowProcessed(); this.OnRowProcessed();
} }
}); });
}
} }
/// <summary> /// <summary>

35
src/ImageProcessorCore/Filters/Contrast.cs

@ -42,24 +42,29 @@ namespace ImageProcessorCore.Filters
int endX = sourceRectangle.Right; int endX = sourceRectangle.Right;
Vector4 contrastVector = new Vector4(contrast, contrast, contrast, 1); Vector4 contrastVector = new Vector4(contrast, contrast, contrast, 1);
Vector4 shiftVector = new Vector4(.5f, .5f, .5f, 1); Vector4 shiftVector = new Vector4(.5f, .5f, .5f, 1);
Parallel.For(
startY, using (PixelAccessor sourcePixels = source.Lock())
endY, using (PixelAccessor targetPixels = target.Lock())
y => {
{ Parallel.For(
if (y >= sourceY && y < sourceBottom) startY,
endY,
y =>
{ {
for (int x = startX; x < endX; x++) if (y >= sourceY && y < sourceBottom)
{ {
Vector4 color = Color.Expand(source[x, y]).ToVector4(); for (int x = startX; x < endX; x++)
color -= shiftVector; {
color *= contrastVector; Vector4 color = Color.Expand(sourcePixels[x, y]).ToVector4();
color += shiftVector; color -= shiftVector;
target[x, y] = Color.Compress(new Color(color)); color *= contrastVector;
color += shiftVector;
targetPixels[x, y] = Color.Compress(new Color(color));
}
this.OnRowProcessed();
} }
this.OnRowProcessed(); });
} }
});
} }
} }
} }

16
src/ImageProcessorCore/Filters/Convolution/Convolution2DFilter.cs

@ -42,7 +42,10 @@ namespace ImageProcessorCore.Filters
int maxY = sourceBottom - 1; int maxY = sourceBottom - 1;
int maxX = endX - 1; int maxX = endX - 1;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
using (PixelAccessor targetPixels = target.Lock())
{
Parallel.For(
startY, startY,
endY, endY,
y => y =>
@ -58,8 +61,8 @@ namespace ImageProcessorCore.Filters
float gY = 0; float gY = 0;
float bY = 0; float bY = 0;
// Apply each matrix multiplier to the color components for each pixel. // Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelYHeight; fy++) for (int fy = 0; fy < kernelYHeight; fy++)
{ {
int fyr = fy - radiusY; int fyr = fy - radiusY;
int offsetY = y + fyr; int offsetY = y + fyr;
@ -73,7 +76,7 @@ namespace ImageProcessorCore.Filters
offsetX = offsetX.Clamp(0, maxX); offsetX = offsetX.Clamp(0, maxX);
Color currentColor = source[offsetX, offsetY]; Color currentColor = sourcePixels[offsetX, offsetY];
float r = currentColor.R; float r = currentColor.R;
float g = currentColor.G; float g = currentColor.G;
float b = currentColor.B; float b = currentColor.B;
@ -98,12 +101,13 @@ namespace ImageProcessorCore.Filters
float green = (float)Math.Sqrt((gX * gX) + (gY * gY)); float green = (float)Math.Sqrt((gX * gX) + (gY * gY));
float blue = (float)Math.Sqrt((bX * bX) + (bY * bY)); float blue = (float)Math.Sqrt((bX * bX) + (bY * bY));
Color targetColor = target[x, y]; Color targetColor = targetPixels[x, y];
target[x, y] = new Color(red, green, blue, targetColor.A); targetPixels[x, y] = new Color(red, green, blue, targetColor.A);
} }
this.OnRowProcessed(); this.OnRowProcessed();
} }
}); });
}
} }
} }
} }

10
src/ImageProcessorCore/Filters/Convolution/Convolution2PassFilter.cs

@ -71,7 +71,10 @@ namespace ImageProcessorCore.Filters
int maxY = sourceBottom - 1; int maxY = sourceBottom - 1;
int maxX = endX - 1; int maxX = endX - 1;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
using (PixelAccessor targetPixels = target.Lock())
{
Parallel.For(
startY, startY,
endY, endY,
y => y =>
@ -97,16 +100,17 @@ namespace ImageProcessorCore.Filters
offsetX = offsetX.Clamp(0, maxX); offsetX = offsetX.Clamp(0, maxX);
Color currentColor = source[offsetX, offsetY]; Color currentColor = sourcePixels[offsetX, offsetY];
destination += kernel[fy, fx] * currentColor; destination += kernel[fy, fx] * currentColor;
} }
} }
target[x, y] = destination; targetPixels[x, y] = destination;
} }
this.OnRowProcessed(); this.OnRowProcessed();
} }
}); });
}
} }
} }
} }

10
src/ImageProcessorCore/Filters/Convolution/ConvolutionFilter.cs

@ -31,7 +31,10 @@ namespace ImageProcessorCore.Filters
int maxY = sourceBottom - 1; int maxY = sourceBottom - 1;
int maxX = endX - 1; int maxX = endX - 1;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
using (PixelAccessor targetPixels = target.Lock())
{
Parallel.For(
startY, startY,
endY, endY,
y => y =>
@ -59,7 +62,7 @@ namespace ImageProcessorCore.Filters
offsetX = offsetX.Clamp(0, maxX); offsetX = offsetX.Clamp(0, maxX);
Color currentColor = source[offsetX, offsetY]; Color currentColor = sourcePixels[offsetX, offsetY];
float r = currentColor.R; float r = currentColor.R;
float g = currentColor.G; float g = currentColor.G;
float b = currentColor.B; float b = currentColor.B;
@ -74,11 +77,12 @@ namespace ImageProcessorCore.Filters
float green = gX; float green = gX;
float blue = bX; float blue = bX;
target[x, y] = new Color(red, green, blue); targetPixels[x, y] = new Color(red, green, blue);
} }
this.OnRowProcessed(); this.OnRowProcessed();
} }
}); });
}
} }
} }
} }

31
src/ImageProcessorCore/Filters/Glow.cs

@ -15,7 +15,7 @@ namespace ImageProcessorCore.Filters
public class Glow : ParallelImageProcessor public class Glow : ParallelImageProcessor
{ {
/// <summary> /// <summary>
/// Gets or sets the vignette color to apply. /// Gets or sets the glow color to apply.
/// </summary> /// </summary>
public Color Color { get; set; } = Color.White; public Color Color { get; set; } = Color.White;
@ -40,19 +40,24 @@ namespace ImageProcessorCore.Filters
float rY = this.RadiusY > 0 ? this.RadiusY : targetRectangle.Height / 2f; float rY = this.RadiusY > 0 ? this.RadiusY : targetRectangle.Height / 2f;
float maxDistance = (float)Math.Sqrt(rX * rX + rY * rY); float maxDistance = (float)Math.Sqrt(rX * rX + rY * rY);
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
startY, using (PixelAccessor targetPixels = target.Lock())
endY, {
y => Parallel.For(
{ startY,
for (int x = startX; x < endX; x++) endY,
y =>
{ {
float distance = Vector2.Distance(centre, new Vector2(x, y)); for (int x = startX; x < endX; x++)
Color sourceColor = target[x, y]; {
target[x, y] = Color.Lerp(glowColor, sourceColor, .5f * (distance / maxDistance)); float distance = Vector2.Distance(centre, new Vector2(x, y));
} Color sourceColor = sourcePixels[x, y];
this.OnRowProcessed(); targetPixels[x, y] = Color.Lerp(glowColor, sourceColor, .5f * (distance / maxDistance));
}); }
this.OnRowProcessed();
});
}
} }
} }
} }

31
src/ImageProcessorCore/Filters/Invert.cs

@ -22,22 +22,27 @@ namespace ImageProcessorCore.Filters
int endX = sourceRectangle.Right; int endX = sourceRectangle.Right;
Vector3 inverseVector = Vector3.One; Vector3 inverseVector = Vector3.One;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
startY, using (PixelAccessor targetPixels = target.Lock())
endY, {
y => Parallel.For(
{ startY,
if (y >= sourceY && y < sourceBottom) endY,
y =>
{ {
for (int x = startX; x < endX; x++) if (y >= sourceY && y < sourceBottom)
{ {
Color color = source[x, y]; for (int x = startX; x < endX; x++)
Vector3 vector = inverseVector - color.ToVector3(); {
target[x, y] = new Color(vector, color.A); Color color = sourcePixels[x, y];
Vector3 vector = inverseVector - color.ToVector3();
targetPixels[x, y] = new Color(vector, color.A);
}
this.OnRowProcessed();
} }
this.OnRowProcessed(); });
} }
});
} }
} }
} }

68
src/ImageProcessorCore/Filters/Pixelate.cs

@ -48,46 +48,50 @@ namespace ImageProcessorCore.Filters
// Get the range on the y-plane to choose from. // Get the range on the y-plane to choose from.
IEnumerable<int> range = EnumerableExtensions.SteppedRange(startY, i => i < endY, size); IEnumerable<int> range = EnumerableExtensions.SteppedRange(startY, i => i < endY, size);
Parallel.ForEach( using (PixelAccessor sourcePixels = source.Lock())
range, using (PixelAccessor targetPixels = target.Lock())
y => {
{ Parallel.ForEach(
if (y >= sourceY && y < sourceBottom) range,
{ y =>
for (int x = startX; x < endX; x += size)
{ {
if (y >= sourceY && y < sourceBottom)
{
for (int x = startX; x < endX; x += size)
{
int offsetX = offset; int offsetX = offset;
int offsetY = offset; int offsetY = offset;
// Make sure that the offset is within the boundary of the // Make sure that the offset is within the boundary of the
// image. // image.
while (y + offsetY >= sourceBottom) while (y + offsetY >= sourceBottom)
{ {
offsetY--; offsetY--;
} }
while (x + offsetX >= endX) while (x + offsetX >= endX)
{ {
offsetX--; offsetX--;
} }
// Get the pixel color in the centre of the soon to be pixelated area. // Get the pixel color in the centre of the soon to be pixelated area.
// ReSharper disable AccessToDisposedClosure // ReSharper disable AccessToDisposedClosure
Color pixel = source[x + offsetX, y + offsetY]; Color pixel = sourcePixels[x + offsetX, y + offsetY];
// For each pixel in the pixelate size, set it to the centre color. // For each pixel in the pixelate size, set it to the centre color.
for (int l = y; l < y + size && l < sourceBottom; l++) for (int l = y; l < y + size && l < sourceBottom; l++)
{ {
for (int k = x; k < x + size && k < endX; k++) for (int k = x; k < x + size && k < endX; k++)
{ {
target[k, l] = pixel; targetPixels[k, l] = pixel;
}
}
} }
this.OnRowProcessed();
} }
} });
this.OnRowProcessed(); }
}
});
} }
} }
} }

28
src/ImageProcessorCore/Filters/Vignette.cs

@ -40,19 +40,23 @@ namespace ImageProcessorCore.Filters
float rY = this.RadiusY > 0 ? this.RadiusY : targetRectangle.Height / 2f; float rY = this.RadiusY > 0 ? this.RadiusY : targetRectangle.Height / 2f;
float maxDistance = (float)Math.Sqrt(rX * rX + rY * rY); float maxDistance = (float)Math.Sqrt(rX * rX + rY * rY);
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
startY, using (PixelAccessor targetPixels = target.Lock())
endY, {
y => Parallel.For(
{ startY,
for (int x = startX; x < endX; x++) endY,
y =>
{ {
float distance = Vector2.Distance(centre, new Vector2(x, y)); for (int x = startX; x < endX; x++)
Color sourceColor = target[x, y]; {
target[x, y] = Color.Lerp(vignetteColor, sourceColor, 1 - .9f * distance / maxDistance); float distance = Vector2.Distance(centre, new Vector2(x, y));
} Color sourceColor = sourcePixels[x, y];
this.OnRowProcessed(); targetPixels[x, y] = Color.Lerp(vignetteColor, sourceColor, 1 - .9f * distance / maxDistance);
}); }
this.OnRowProcessed();
});
}
} }
} }
} }

37
src/ImageProcessorCore/Formats/Bmp/BmpEncoderCore.cs

@ -132,15 +132,18 @@ namespace ImageProcessorCore.Formats
amount = 4 - amount; amount = 4 - amount;
} }
switch (this.bmpBitsPerPixel) using (PixelAccessor pixels = image.Lock())
{ {
case BmpBitsPerPixel.Pixel32: switch (this.bmpBitsPerPixel)
this.Write32bit(writer, image, amount); {
break; case BmpBitsPerPixel.Pixel32:
this.Write32bit(writer, pixels, amount);
break;
case BmpBitsPerPixel.Pixel24: case BmpBitsPerPixel.Pixel24:
this.Write24bit(writer, image, amount); this.Write24bit(writer, pixels, amount);
break; break;
}
} }
} }
@ -148,18 +151,18 @@ namespace ImageProcessorCore.Formats
/// Writes the 32bit color palette to the stream. /// Writes the 32bit color palette to the stream.
/// </summary> /// </summary>
/// <param name="writer">The <see cref="EndianBinaryWriter"/> containing the stream to write to.</param> /// <param name="writer">The <see cref="EndianBinaryWriter"/> containing the stream to write to.</param>
/// <param name="image">The <see cref="ImageBase"/> containing pixel data.</param> /// <param name="pixels">The <see cref="PixelAccessor"/> containing pixel data.</param>
/// <param name="amount">The amount to pad each row by.</param> /// <param name="amount">The amount to pad each row by.</param>
private void Write32bit(EndianBinaryWriter writer, ImageBase image, int amount) private void Write32bit(EndianBinaryWriter writer, PixelAccessor pixels, int amount)
{ {
for (int y = image.Height - 1; y >= 0; y--) for (int y = pixels.Height - 1; y >= 0; y--)
{ {
for (int x = 0; x < image.Width; x++) for (int x = 0; x < pixels.Width; x++)
{ {
// Limit the output range and multiply out from our floating point. // Limit the output range and multiply out from our floating point.
// Convert back to b-> g-> r-> a order. // Convert back to b-> g-> r-> a order.
// Convert to non-premultiplied color. // Convert to non-premultiplied color.
Bgra32 color = Color.ToNonPremultiplied(image[x, y]); Bgra32 color = Color.ToNonPremultiplied(pixels[x, y]);
// We can take advantage of BGRA here // We can take advantage of BGRA here
writer.Write(color.Bgra); writer.Write(color.Bgra);
@ -177,18 +180,18 @@ namespace ImageProcessorCore.Formats
/// Writes the 24bit color palette to the stream. /// Writes the 24bit color palette to the stream.
/// </summary> /// </summary>
/// <param name="writer">The <see cref="EndianBinaryWriter"/> containing the stream to write to.</param> /// <param name="writer">The <see cref="EndianBinaryWriter"/> containing the stream to write to.</param>
/// <param name="image">The <see cref="ImageBase"/> containing pixel data.</param> /// <param name="pixels">The <see cref="PixelAccessor"/> containing pixel data.</param>
/// <param name="amount">The amount to pad each row by.</param> /// <param name="amount">The amount to pad each row by.</param>
private void Write24bit(EndianBinaryWriter writer, ImageBase image, int amount) private void Write24bit(EndianBinaryWriter writer, PixelAccessor pixels, int amount)
{ {
for (int y = image.Height - 1; y >= 0; y--) for (int y = pixels.Height - 1; y >= 0; y--)
{ {
for (int x = 0; x < image.Width; x++) for (int x = 0; x < pixels.Width; x++)
{ {
// Limit the output range and multiply out from our floating point. // Limit the output range and multiply out from our floating point.
// Convert back to b-> g-> r-> a order. // Convert back to b-> g-> r-> a order.
// Convert to non-premultiplied color. // Convert to non-premultiplied color.
Bgra32 color = Color.ToNonPremultiplied(image[x, y]); Bgra32 color = Color.ToNonPremultiplied(pixels[x, y]);
// Allocate 1 array instead of allocating 3. // Allocate 1 array instead of allocating 3.
writer.Write(new[] { color.B, color.G, color.R }); writer.Write(new[] { color.B, color.G, color.R });

2
src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs

@ -90,7 +90,7 @@ namespace ImageProcessorCore.Formats
} }
else else
{ {
encode.Encode(stream, image, this.Quality, this.Quality >= 80 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420); encode.Encode(stream, image, this.Quality, this.Quality >= 80 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420);
} }
} }
} }

43
src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs

@ -423,15 +423,15 @@ namespace ImageProcessorCore.Formats
// toYCbCr converts the 8x8 region of m whose top-left corner is p to its // toYCbCr converts the 8x8 region of m whose top-left corner is p to its
// YCbCr values. // YCbCr values.
private void toYCbCr(ImageBase m, int x, int y, Block yBlock, Block cbBlock, Block crBlock) private void toYCbCr(PixelAccessor pixels, int x, int y, Block yBlock, Block cbBlock, Block crBlock)
{ {
int xmax = m.Width - 1; int xmax = pixels.Width - 1;
int ymax = m.Height - 1; int ymax = pixels.Height - 1;
for (int j = 0; j < 8; j++) for (int j = 0; j < 8; j++)
{ {
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
{ {
YCbCr color = m[Math.Min(x + i, xmax), Math.Min(y + j, ymax)]; YCbCr color = pixels[Math.Min(x + i, xmax), Math.Min(y + j, ymax)];
int index = (8 * j) + i; int index = (8 * j) + i;
yBlock[index] = (int)color.Y; yBlock[index] = (int)color.Y;
cbBlock[index] = (int)color.Cb; cbBlock[index] = (int)color.Cb;
@ -486,17 +486,17 @@ namespace ImageProcessorCore.Formats
// writeSOS writes the StartOfScan marker. // writeSOS writes the StartOfScan marker.
private void writeSOS(ImageBase m) private void writeSOS(PixelAccessor pixels)
{ {
outputStream.Write(sosHeaderYCbCr, 0, sosHeaderYCbCr.Length); outputStream.Write(sosHeaderYCbCr, 0, sosHeaderYCbCr.Length);
switch (subsample) switch (subsample)
{ {
case JpegSubsample.Ratio444: case JpegSubsample.Ratio444:
encode444(m); encode444(pixels);
break; break;
case JpegSubsample.Ratio420: case JpegSubsample.Ratio420:
encode420(m); encode420(pixels);
break; break;
} }
@ -504,18 +504,18 @@ namespace ImageProcessorCore.Formats
emit(0x7f, 7); emit(0x7f, 7);
} }
private void encode444(ImageBase m) private void encode444(PixelAccessor pixels)
{ {
Block b = new Block(); Block b = new Block();
Block cb = new Block(); Block cb = new Block();
Block cr = new Block(); Block cr = new Block();
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
for (int y = 0; y < m.Height; y += 8) for (int y = 0; y < pixels.Height; y += 8)
{ {
for (int x = 0; x < m.Width; x += 8) for (int x = 0; x < pixels.Width; x += 8)
{ {
toYCbCr(m, x, y, b, cb, cr); toYCbCr(pixels, x, y, b, cb, cr);
prevDCY = writeBlock(b, (quantIndex)0, prevDCY); prevDCY = writeBlock(b, (quantIndex)0, prevDCY);
prevDCCb = writeBlock(cb, (quantIndex)1, prevDCCb); prevDCCb = writeBlock(cb, (quantIndex)1, prevDCCb);
prevDCCr = writeBlock(cr, (quantIndex)1, prevDCCr); prevDCCr = writeBlock(cr, (quantIndex)1, prevDCCr);
@ -523,7 +523,7 @@ namespace ImageProcessorCore.Formats
} }
} }
private void encode420(ImageBase m) private void encode420(PixelAccessor pixels)
{ {
Block b = new Block(); Block b = new Block();
Block[] cb = new Block[4]; Block[] cb = new Block[4];
@ -533,16 +533,16 @@ namespace ImageProcessorCore.Formats
for (int i = 0; i < 4; i++) cb[i] = new Block(); for (int i = 0; i < 4; i++) cb[i] = new Block();
for (int i = 0; i < 4; i++) cr[i] = new Block(); for (int i = 0; i < 4; i++) cr[i] = new Block();
for (int y = 0; y < m.Height; y += 16) for (int y = 0; y < pixels.Height; y += 16)
{ {
for (int x = 0; x < m.Width; x += 16) for (int x = 0; x < pixels.Width; x += 16)
{ {
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
int xOff = (i & 1) * 8; int xOff = (i & 1) * 8;
int yOff = (i & 2) * 4; int yOff = (i & 2) * 4;
toYCbCr(m, x + xOff, y + yOff, b, cb[i], cr[i]); toYCbCr(pixels, x + xOff, y + yOff, b, cb[i], cr[i]);
prevDCY = writeBlock(b, (quantIndex)0, prevDCY); prevDCY = writeBlock(b, (quantIndex)0, prevDCY);
} }
scale_16x16_8x8(b, cb); scale_16x16_8x8(b, cb);
@ -555,7 +555,7 @@ namespace ImageProcessorCore.Formats
// Encode writes the Image m to w in JPEG 4:2:0 baseline format with the given // Encode writes the Image m to w in JPEG 4:2:0 baseline format with the given
// options. Default parameters are used if a nil *Options is passed. // options. Default parameters are used if a nil *Options is passed.
public void Encode(Stream stream, ImageBase m, int quality, JpegSubsample subsample) public void Encode(Stream stream, ImageBase image, int quality, JpegSubsample subsample)
{ {
this.outputStream = stream; this.outputStream = stream;
this.subsample = subsample; this.subsample = subsample;
@ -570,9 +570,9 @@ namespace ImageProcessorCore.Formats
quant[i] = new byte[Block.blockSize]; quant[i] = new byte[Block.blockSize];
} }
if (m.Width >= (1 << 16) || m.Height >= (1 << 16)) if (image.Width >= (1 << 16) || image.Height >= (1 << 16))
{ {
throw new ImageFormatException($"Image is too large to encode at {m.Width}x{m.Height}."); throw new ImageFormatException($"Image is too large to encode at {image.Width}x{image.Height}.");
} }
if (quality < 1) quality = 1; if (quality < 1) quality = 1;
@ -614,13 +614,16 @@ namespace ImageProcessorCore.Formats
writeDQT(); writeDQT();
// Write the image dimensions. // Write the image dimensions.
writeSOF0(m.Width, m.Height, nComponent); writeSOF0(image.Width, image.Height, nComponent);
// Write the Huffman tables. // Write the Huffman tables.
writeDHT(nComponent); writeDHT(nComponent);
// Write the image data. // Write the image data.
writeSOS(m); using (PixelAccessor pixels = image.Lock())
{
writeSOS(pixels);
}
// Write the End Of Image marker. // Write the End Of Image marker.
buf[0] = 0xff; buf[0] = 0xff;

21
src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs

@ -123,7 +123,12 @@ namespace ImageProcessorCore.Formats
this.WritePaletteChunk(stream, header, image); this.WritePaletteChunk(stream, header, image);
this.WritePhysicalChunk(stream, image); this.WritePhysicalChunk(stream, image);
this.WriteGammaChunk(stream); this.WriteGammaChunk(stream);
this.WriteDataChunks(stream, image);
using (PixelAccessor pixels = image.Lock())
{
this.WriteDataChunks(stream, pixels);
}
this.WriteEndChunk(stream); this.WriteEndChunk(stream);
stream.Flush(); stream.Flush();
} }
@ -292,12 +297,12 @@ namespace ImageProcessorCore.Formats
/// Writes the pixel information to the stream. /// Writes the pixel information to the stream.
/// </summary> /// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="image">The image base.</param> /// <param name="pixels">The image pixels.</param>
private void WriteDataChunks(Stream stream, ImageBase image) private void WriteDataChunks(Stream stream, PixelAccessor pixels)
{ {
byte[] data; byte[] data;
int imageWidth = image.Width; int imageWidth = pixels.Width;
int imageHeight = image.Height; int imageHeight = pixels.Height;
// Indexed image. // Indexed image.
if (this.Quality <= 256) if (this.Quality <= 256)
@ -327,7 +332,7 @@ namespace ImageProcessorCore.Formats
else else
{ {
// TrueColor image. // TrueColor image.
data = new byte[(imageWidth * imageHeight * 4) + image.Height]; data = new byte[(imageWidth * imageHeight * 4) + pixels.Height];
int rowLength = (imageWidth * 4) + 1; int rowLength = (imageWidth * 4) + 1;
@ -343,7 +348,7 @@ namespace ImageProcessorCore.Formats
for (int x = 0; x < imageWidth; x++) for (int x = 0; x < imageWidth; x++)
{ {
Bgra32 color = Color.ToNonPremultiplied(image[x, y]); Bgra32 color = Color.ToNonPremultiplied(pixels[x, y]);
// Calculate the offset for the new array. // Calculate the offset for the new array.
int dataOffset = (y * rowLength) + (x * 4) + 1; int dataOffset = (y * rowLength) + (x * 4) + 1;
@ -354,7 +359,7 @@ namespace ImageProcessorCore.Formats
if (y > 0) if (y > 0)
{ {
color = Color.ToNonPremultiplied(image[x, y - 1]); color = Color.ToNonPremultiplied(pixels[x, y - 1]);
data[dataOffset] -= color.R; data[dataOffset] -= color.R;
data[dataOffset + 1] -= color.G; data[dataOffset + 1] -= color.G;

103
src/ImageProcessorCore/IImage.cs

@ -1,103 +0,0 @@
// <copyright file="IImage.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using System;
using System.Collections.Generic;
using System.IO;
using ImageProcessorCore.Formats;
/// <summary>
/// Encapsulates an image, which consists of the pixel data for a graphics image and its attributes.
/// </summary>
public interface IImage : IImageBase
{
/// <summary>
/// Gets or sets the resolution of the image in x- direction. It is defined as
/// number of dots per inch and should be an positive value.
/// </summary>
/// <value>The density of the image in x- direction.</value>
double HorizontalResolution { get; set; }
/// <summary>
/// Gets or sets the resolution of the image in y- direction. It is defined as
/// number of dots per inch and should be an positive value.
/// </summary>
/// <value>The density of the image in y- direction.</value>
double VerticalResolution { get; set; }
/// <summary>
/// Gets the width of the image in inches. It is calculated as the width of the image
/// in pixels multiplied with the density. When the density is equals or less than zero
/// the default value is used.
/// </summary>
/// <value>The width of the image in inches.</value>
double InchWidth { get; }
/// <summary>
/// Gets the height of the image in inches. It is calculated as the height of the image
/// in pixels multiplied with the density. When the density is equals or less than zero
/// the default value is used.
/// </summary>
/// <value>The height of the image in inches.</value>
double InchHeight { get; }
/// <summary>
/// Gets a value indicating whether this image is animated.
/// </summary>
/// <value>
/// <c>True</c> if this image is animated; otherwise, <c>false</c>.
/// </value>
bool IsAnimated { get; }
/// <summary>
/// Gets or sets the number of times any animation is repeated.
/// <remarks>0 means to repeat indefinitely.</remarks>
/// </summary>
ushort RepeatCount { get; set; }
/// <summary>
/// Gets the currently loaded image format.
/// </summary>
IImageFormat CurrentImageFormat { get; }
/// <summary>
/// Gets the other frames for the animation.
/// </summary>
/// <value>The list of frame images.</value>
IList<ImageFrame> Frames { get; }
/// <summary>
/// Gets the list of properties for storing meta information about this image.
/// </summary>
/// <value>A list of image properties.</value>
IList<ImageProperty> Properties { get; }
/// <summary>
/// Saves the image to the given stream using the currently loaded image format.
/// </summary>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
void Save(Stream stream);
/// <summary>
/// Saves the image to the given stream using the given image format.
/// </summary>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="format">The format to save the image as.</param>
/// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
void Save(Stream stream, IImageFormat format);
/// <summary>
/// Saves the image to the given stream using the given image encoder.
/// </summary>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
void Save(Stream stream, IImageEncoder encoder);
}
}

106
src/ImageProcessorCore/IImageBase.cs

@ -1,106 +0,0 @@
// <copyright file="IImageBase.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using System;
using System.Runtime.CompilerServices;
/// <summary>
/// Encapsulates the basic properties and methods required to manipulate images.
/// </summary>
public interface IImageBase
{
/// <summary>
/// Gets the image pixels as byte array.
/// </summary>
/// <remarks>
/// The returned array has a length of Width * Height * 4 bytes
/// and stores the red, the green, the blue, and the alpha value for
/// each pixel in this order.
/// </remarks>
float[] Pixels { get; }
/// <summary>
/// Gets the width in pixels.
/// </summary>
int Width { get; }
/// <summary>
/// Gets the height in pixels.
/// </summary>
int Height { get; }
/// <summary>
/// Gets the pixel ratio made up of the width and height.
/// </summary>
double PixelRatio { get; }
/// <summary>
/// Gets the <see cref="Rectangle"/> representing the bounds of the image.
/// </summary>
Rectangle Bounds { get; }
/// <summary>
/// Gets or sets th quality of the image. This affects the output quality of lossy image formats.
/// </summary>
int Quality { get; set; }
/// <summary>
/// Gets or sets the frame delay for animated images.
/// If not 0, this field specifies the number of hundredths (1/100) of a second to
/// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered.
/// </summary>
int FrameDelay { get; set; }
/// <summary>
/// Gets or sets the color of a pixel at the specified position.
/// </summary>
/// <param name="x">
/// The x-coordinate of the pixel. Must be greater
/// than zero and smaller than the width of the pixel.
/// </param>
/// <param name="y">
/// The y-coordinate of the pixel. Must be greater
/// than zero and smaller than the width of the pixel.
/// </param>
/// <returns>The <see cref="Color"/> at the specified position.</returns>
Color this[int x, int y] { get; set; }
/// <summary>
/// Sets the pixel array of the image to the given value.
/// </summary>
/// <param name="width">The new width of the image. Must be greater than zero.</param>
/// <param name="height">The new height of the image. Must be greater than zero.</param>
/// <param name="pixels">
/// The array with colors. Must be a multiple of four times the width and height.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if either <paramref name="width"/> or <paramref name="height"/> are less than or equal to 0.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown if the <paramref name="pixels"/> length is not equal to Width * Height * 4.
/// </exception>
void SetPixels(int width, int height, float[] pixels);
/// <summary>
/// Sets the pixel array of the image to the given value, creating a copy of
/// the original pixels.
/// </summary>
/// <param name="width">The new width of the image. Must be greater than zero.</param>
/// <param name="height">The new height of the image. Must be greater than zero.</param>
/// <param name="pixels">
/// The array with colors. Must be a multiple of four times the width and height.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if either <paramref name="width"/> or <paramref name="height"/> are less than or equal to 0.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown if the <paramref name="pixels"/> length is not equal to Width * Height * 4.
/// </exception>
void ClonePixels(int width, int height, float[] pixels);
}
}

97
src/ImageProcessorCore/Image.cs

@ -19,10 +19,10 @@ namespace ImageProcessorCore
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The image data is always stored in RGBA format, where the red, green, blue, and /// The image data is always stored in RGBA format, where the red, green, blue, and
/// alpha values are floats. /// alpha values are floating point numbers.
/// </remarks> /// </remarks>
[DebuggerDisplay("Image: {Width}x{Height}")] [DebuggerDisplay("Image: {Width}x{Height}")]
public class Image : ImageBase, IImage public class Image : ImageBase
{ {
/// <summary> /// <summary>
/// The default horizontal resolution value (dots per inch) in x direction. /// The default horizontal resolution value (dots per inch) in x direction.
@ -97,13 +97,26 @@ namespace ImageProcessorCore
/// </summary> /// </summary>
public IReadOnlyCollection<IImageFormat> Formats { get; } = Bootstrapper.Instance.ImageFormats; public IReadOnlyCollection<IImageFormat> Formats { get; } = Bootstrapper.Instance.ImageFormats;
/// <inheritdoc/> /// <summary>
/// Gets or sets the resolution of the image in x- direction. It is defined as
/// number of dots per inch and should be an positive value.
/// </summary>
/// <value>The density of the image in x- direction.</value>
public double HorizontalResolution { get; set; } = DefaultHorizontalResolution; public double HorizontalResolution { get; set; } = DefaultHorizontalResolution;
/// <inheritdoc/> /// <summary>
/// Gets or sets the resolution of the image in y- direction. It is defined as
/// number of dots per inch and should be an positive value.
/// </summary>
/// <value>The density of the image in y- direction.</value>
public double VerticalResolution { get; set; } = DefaultVerticalResolution; public double VerticalResolution { get; set; } = DefaultVerticalResolution;
/// <inheritdoc/> /// <summary>
/// Gets the width of the image in inches. It is calculated as the width of the image
/// in pixels multiplied with the density. When the density is equals or less than zero
/// the default value is used.
/// </summary>
/// <value>The width of the image in inches.</value>
public double InchWidth public double InchWidth
{ {
get get
@ -119,7 +132,12 @@ namespace ImageProcessorCore
} }
} }
/// <inheritdoc/> /// <summary>
/// Gets the height of the image in inches. It is calculated as the height of the image
/// in pixels multiplied with the density. When the density is equals or less than zero
/// the default value is used.
/// </summary>
/// <value>The height of the image in inches.</value>
public double InchHeight public double InchHeight
{ {
get get
@ -135,36 +153,66 @@ namespace ImageProcessorCore
} }
} }
/// <inheritdoc/> /// <summary>
/// Gets a value indicating whether this image is animated.
/// </summary>
/// <value>
/// <c>True</c> if this image is animated; otherwise, <c>false</c>.
/// </value>
public bool IsAnimated => this.Frames.Count > 0; public bool IsAnimated => this.Frames.Count > 0;
/// <inheritdoc/> /// <summary>
/// Gets or sets the number of times any animation is repeated.
/// <remarks>0 means to repeat indefinitely.</remarks>
/// </summary>
public ushort RepeatCount { get; set; } public ushort RepeatCount { get; set; }
/// <inheritdoc/> /// <summary>
/// Gets the other frames for the animation.
/// </summary>
/// <value>The list of frame images.</value>
public IList<ImageFrame> Frames { get; } = new List<ImageFrame>(); public IList<ImageFrame> Frames { get; } = new List<ImageFrame>();
/// <inheritdoc/> /// <summary>
/// Gets the list of properties for storing meta information about this image.
/// </summary>
/// <value>A list of image properties.</value>
public IList<ImageProperty> Properties { get; } = new List<ImageProperty>(); public IList<ImageProperty> Properties { get; } = new List<ImageProperty>();
/// <inheritdoc/> /// <summary>
/// Gets the currently loaded image format.
/// </summary>
public IImageFormat CurrentImageFormat { get; internal set; } public IImageFormat CurrentImageFormat { get; internal set; }
/// <inheritdoc/> /// <summary>
/// Saves the image to the given stream using the currently loaded image format.
/// </summary>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
public void Save(Stream stream) public void Save(Stream stream)
{ {
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
this.CurrentImageFormat.Encoder.Encode(this, stream); this.CurrentImageFormat.Encoder.Encode(this, stream);
} }
/// <inheritdoc/> /// <summary>
/// Saves the image to the given stream using the given image format.
/// </summary>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="format">The format to save the image as.</param>
/// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
public void Save(Stream stream, IImageFormat format) public void Save(Stream stream, IImageFormat format)
{ {
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
format.Encoder.Encode(this, stream); format.Encoder.Encode(this, stream);
} }
/// <inheritdoc/> /// <summary>
/// Saves the image to the given stream using the given image encoder.
/// </summary>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
public void Save(Stream stream, IImageEncoder encoder) public void Save(Stream stream, IImageEncoder encoder)
{ {
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
@ -186,27 +234,6 @@ namespace ImageProcessorCore
} }
} }
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.IsDisposed)
{
return;
}
// Dispose of the unmanaged resources for each frame here.
if (this.Frames.Any())
{
foreach (ImageFrame frame in this.Frames)
{
frame.Dispose();
}
this.Frames.Clear();
}
base.Dispose(disposing);
}
/// <summary> /// <summary>
/// Loads the image from the given stream. /// Loads the image from the given stream.
/// </summary> /// </summary>

207
src/ImageProcessorCore/ImageBase.cs

@ -6,43 +6,18 @@
namespace ImageProcessorCore namespace ImageProcessorCore
{ {
using System; using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
/// <summary> /// <summary>
/// The base class of all images. Encapsulates the basic properties and methods /// The base class of all images. Encapsulates the basic properties and methods
/// required to manipulate images. /// required to manipulate images.
/// </summary> /// </summary>
public abstract unsafe class ImageBase : IImageBase, IDisposable public abstract class ImageBase
{ {
/// <summary>
/// The position of the first pixel in the bitmap.
/// </summary>
private float* pixelsBase;
/// <summary> /// <summary>
/// The array of pixels. /// The array of pixels.
/// </summary> /// </summary>
private float[] pixelsArray; private float[] pixelsArray;
/// <summary>
/// Provides a way to access the pixels from unmanaged memory.
/// </summary>
private GCHandle pixelsHandle;
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
internal bool IsDisposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageBase"/> class. /// Initializes a new instance of the <see cref="ImageBase"/> class.
/// </summary> /// </summary>
@ -68,8 +43,6 @@ namespace ImageProcessorCore
// Assign the pointer and pixels. // Assign the pointer and pixels.
this.pixelsArray = new float[width * height * 4]; this.pixelsArray = new float[width * height * 4];
this.pixelsHandle = GCHandle.Alloc(this.pixelsArray, GCHandleType.Pinned);
this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
} }
/// <summary> /// <summary>
@ -90,19 +63,11 @@ namespace ImageProcessorCore
this.Quality = other.Quality; this.Quality = other.Quality;
this.FrameDelay = other.FrameDelay; this.FrameDelay = other.FrameDelay;
// Assign the pointer and copy the pixels. // Copy the pixels.
this.pixelsArray = new float[this.Width * this.Height * 4]; this.pixelsArray = new float[this.Width * this.Height * 4];
this.pixelsHandle = GCHandle.Alloc(this.pixelsArray, GCHandleType.Pinned);
this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
Array.Copy(other.pixelsArray, this.pixelsArray, other.pixelsArray.Length); Array.Copy(other.pixelsArray, this.pixelsArray, other.pixelsArray.Length);
} }
/// <inheritdoc/>
~ImageBase()
{
this.Dispose(false);
}
/// <summary> /// <summary>
/// Gets or sets the maximum allowable width in pixels. /// Gets or sets the maximum allowable width in pixels.
/// </summary> /// </summary>
@ -113,67 +78,65 @@ namespace ImageProcessorCore
/// </summary> /// </summary>
public static int MaxHeight { get; set; } = int.MaxValue; public static int MaxHeight { get; set; } = int.MaxValue;
/// <inheritdoc/> /// <summary>
/// Gets the image pixels as byte array.
/// </summary>
/// <remarks>
/// The returned array has a length of Width * Height * 4 bytes
/// and stores the red, the green, the blue, and the alpha value for
/// each pixel in this order.
/// </remarks>
public float[] Pixels => this.pixelsArray; public float[] Pixels => this.pixelsArray;
/// <inheritdoc/> /// <summary>
/// Gets the width in pixels.
/// </summary>
public int Width { get; private set; } public int Width { get; private set; }
/// <inheritdoc/> /// <summary>
/// Gets the height in pixels.
/// </summary>
public int Height { get; private set; } public int Height { get; private set; }
/// <inheritdoc/> /// <summary>
/// Gets the pixel ratio made up of the width and height.
/// </summary>
public double PixelRatio => (double)this.Width / this.Height; public double PixelRatio => (double)this.Width / this.Height;
/// <inheritdoc/> /// <summary>
/// Gets the <see cref="Rectangle"/> representing the bounds of the image.
/// </summary>
public Rectangle Bounds => new Rectangle(0, 0, this.Width, this.Height); public Rectangle Bounds => new Rectangle(0, 0, this.Width, this.Height);
/// <inheritdoc/> /// <summary>
/// Gets or sets th quality of the image. This affects the output quality of lossy image formats.
/// </summary>
public int Quality { get; set; } public int Quality { get; set; }
/// <inheritdoc/> /// <summary>
/// Gets or sets the frame delay for animated images.
/// If not 0, this field specifies the number of hundredths (1/100) of a second to
/// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered.
/// </summary>
public int FrameDelay { get; set; } public int FrameDelay { get; set; }
/// <inheritdoc/> /// <summary>
public Color this[int x, int y] /// Sets the pixel array of the image to the given value.
{ /// </summary>
get /// <param name="width">The new width of the image. Must be greater than zero.</param>
{ /// <param name="height">The new height of the image. Must be greater than zero.</param>
#if DEBUG /// <param name="pixels">
if ((x < 0) || (x >= this.Width)) /// The array with colors. Must be a multiple of four times the width and height.
{ /// </param>
throw new ArgumentOutOfRangeException(nameof(x), "Value cannot be less than zero or greater than the bitmap width."); /// <exception cref="ArgumentOutOfRangeException">
} /// Thrown if either <paramref name="width"/> or <paramref name="height"/> are less than or equal to 0.
/// </exception>
if ((y < 0) || (y >= this.Height)) /// <exception cref="ArgumentException">
{ /// Thrown if the <paramref name="pixels"/> length is not equal to Width * Height * 4.
throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height."); /// </exception>
}
#endif
return *((Color*)(this.pixelsBase + ((y * this.Width) + x) * 4));
}
set
{
#if DEBUG
if ((x < 0) || (x >= this.Width))
{
throw new ArgumentOutOfRangeException(nameof(x), "Value cannot be less than zero or greater than the bitmap width.");
}
if ((y < 0) || (y >= this.Height))
{
throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height.");
}
#endif
*(Color*)(this.pixelsBase + (((y * this.Width) + x) * 4)) = value;
}
}
/// <inheritdoc/>
public void SetPixels(int width, int height, float[] pixels) public void SetPixels(int width, int height, float[] pixels)
{ {
#if DEBUG
if (width <= 0) if (width <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(width), "Width must be greater than or equals than zero."); throw new ArgumentOutOfRangeException(nameof(width), "Width must be greater than or equals than zero.");
@ -188,28 +151,30 @@ namespace ImageProcessorCore
{ {
throw new ArgumentException("Pixel array must have the length of Width * Height * 4."); throw new ArgumentException("Pixel array must have the length of Width * Height * 4.");
} }
#endif
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
// Ensure nothing is preserved if previously allocated.
if (this.pixelsHandle.IsAllocated)
{
this.pixelsArray = null;
this.pixelsHandle.Free();
this.pixelsBase = null;
}
this.pixelsArray = pixels; this.pixelsArray = pixels;
this.pixelsHandle = GCHandle.Alloc(this.pixelsArray, GCHandleType.Pinned);
this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
} }
/// <inheritdoc/> /// <summary>
/// Sets the pixel array of the image to the given value, creating a copy of
/// the original pixels.
/// </summary>
/// <param name="width">The new width of the image. Must be greater than zero.</param>
/// <param name="height">The new height of the image. Must be greater than zero.</param>
/// <param name="pixels">
/// The array with colors. Must be a multiple of four times the width and height.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if either <paramref name="width"/> or <paramref name="height"/> are less than or equal to 0.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown if the <paramref name="pixels"/> length is not equal to Width * Height * 4.
/// </exception>
public void ClonePixels(int width, int height, float[] pixels) public void ClonePixels(int width, int height, float[] pixels)
{ {
#if DEBUG if (width <= 0)
if (width <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(width), "Width must be greater than or equals than zero."); throw new ArgumentOutOfRangeException(nameof(width), "Width must be greater than or equals than zero.");
} }
@ -223,55 +188,25 @@ namespace ImageProcessorCore
{ {
throw new ArgumentException("Pixel array must have the length of Width * Height * 4."); throw new ArgumentException("Pixel array must have the length of Width * Height * 4.");
} }
#endif
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
// Assign the pointer and copy the pixels. // Copy the pixels.
this.pixelsArray = new float[pixels.Length]; this.pixelsArray = new float[pixels.Length];
this.pixelsHandle = GCHandle.Alloc(this.pixelsArray, GCHandleType.Pinned);
this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
Array.Copy(pixels, this.pixelsArray, pixels.Length); Array.Copy(pixels, this.pixelsArray, pixels.Length);
} }
/// <inheritdoc/>
public void Dispose()
{
this.Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
/// <summary> /// <summary>
/// Disposes the object and frees resources for the Garbage Collector. /// Locks the image providing access to the pixels.
/// <remarks>
/// It is imperative that the accessor is correctly disposed off after use.
/// </remarks>
/// </summary> /// </summary>
/// <param name="disposing">If true, the object gets disposed.</param> /// <returns>The <see cref="PixelAccessor"/></returns>
protected virtual void Dispose(bool disposing) public PixelAccessor Lock()
{ {
if (this.IsDisposed) return new PixelAccessor(this);
{
return;
}
if (disposing)
{
// Dispose of any managed resources here.
this.pixelsArray = null;
}
if (this.pixelsHandle.IsAllocated)
{
this.pixelsHandle.Free();
this.pixelsBase = null;
}
// Note disposing is done.
this.IsDisposed = true;
} }
} }
} }

8
src/ImageProcessorCore/ImageExtensions.cs

@ -128,11 +128,6 @@ namespace ImageProcessorCore
/// <exception cref="ObjectDisposedException">Thrown if the <paramref name="source"/> has been disposed.</exception> /// <exception cref="ObjectDisposedException">Thrown if the <paramref name="source"/> has been disposed.</exception>
private static Image PerformAction(Image source, bool clone, Action<ImageBase, ImageBase> action) private static Image PerformAction(Image source, bool clone, Action<ImageBase, ImageBase> action)
{ {
if (source.IsDisposed)
{
throw new ObjectDisposedException("Image");
}
Image transformedImage = clone Image transformedImage = clone
? new Image(source) ? new Image(source)
: new Image : new Image
@ -163,9 +158,6 @@ namespace ImageProcessorCore
} }
} }
// According to http://stackoverflow.com/questions/37921815/idisposable-unmanaged-fields-reference-types-and-assignments
// There's no need to dispose of the original image as the GC will get around to cleaning it up now there are no references to the original data.
// TODO: Investigate how long this is held onto and try to keep that to a minimum.
source = transformedImage; source = transformedImage;
return source; return source;
} }

164
src/ImageProcessorCore/PixelAccessor.cs

@ -0,0 +1,164 @@
// <copyright file="PixelAccessor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using System;
using System.Runtime.InteropServices;
/// <summary>
/// Provides per-pixel access to an images pixels.
/// </summary>
public sealed unsafe class PixelAccessor : IDisposable
{
/// <summary>
/// The position of the first pixel in the bitmap.
/// </summary>
private float* pixelsBase;
/// <summary>
/// Provides a way to access the pixels from unmanaged memory.
/// </summary>
private GCHandle pixelsHandle;
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="PixelAccessor"/> class.
/// </summary>
/// <param name="image">
/// The image to provide pixel access for.
/// </param>
public PixelAccessor(ImageBase image)
{
Guard.NotNull(image, nameof(image));
Guard.MustBeGreaterThan(image.Width, 0, "image width");
Guard.MustBeGreaterThan(image.Height, 0, "image height");
int size = image.Pixels.Length;
this.Width = image.Width;
this.Height = image.Height;
// Assign the pointer.
// If buffer is allocated on Large Object Heap, then we are going to pin it instead of making a copy.
if (size > (85 * 1024))
{
this.pixelsHandle = GCHandle.Alloc(image.Pixels, GCHandleType.Pinned);
this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
}
else
{
fixed (float* pbuffer = image.Pixels)
{
this.pixelsBase = pbuffer;
}
}
}
/// <summary>
/// Finalizes an instance of the <see cref="PixelAccessor"/> class.
/// </summary>
~PixelAccessor()
{
this.Dispose();
}
/// <summary>
/// Gets the width of the image.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of the image.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets or sets the color of a pixel at the specified position.
/// </summary>
/// <param name="x">
/// The x-coordinate of the pixel. Must be greater
/// than zero and smaller than the width of the pixel.
/// </param>
/// <param name="y">
/// The y-coordinate of the pixel. Must be greater
/// than zero and smaller than the width of the pixel.
/// </param>
/// <returns>The <see cref="Color"/> at the specified position.</returns>
public Color this[int x, int y]
{
get
{
#if DEBUG
if ((x < 0) || (x >= this.Width))
{
throw new ArgumentOutOfRangeException(nameof(x), "Value cannot be less than zero or greater than the bitmap width.");
}
if ((y < 0) || (y >= this.Height))
{
throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height.");
}
#endif
return *((Color*)(this.pixelsBase + ((y * this.Width) + x) * 4));
}
set
{
#if DEBUG
if ((x < 0) || (x >= this.Width))
{
throw new ArgumentOutOfRangeException(nameof(x), "Value cannot be less than zero or greater than the bitmap width.");
}
if ((y < 0) || (y >= this.Height))
{
throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height.");
}
#endif
*(Color*)(this.pixelsBase + (((y * this.Width) + x) * 4)) = value;
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
if (this.isDisposed)
{
return;
}
if (this.pixelsHandle.IsAllocated)
{
this.pixelsHandle.Free();
}
this.pixelsBase = null;
// Note disposing is done.
this.isDisposed = true;
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
}
}

31
src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs

@ -50,21 +50,24 @@ namespace ImageProcessorCore.Quantizers
// Get the size of the source image // Get the size of the source image
int height = image.Height; int height = image.Height;
int width = image.Width; int width = image.Width;
// Call the FirstPass function if not a single pass algorithm.
// For something like an Octree quantizer, this will run through
// all image pixels, build a data structure, and create a palette.
if (!this.singlePass)
{
this.FirstPass(image, width, height);
}
byte[] quantizedPixels = new byte[width * height]; byte[] quantizedPixels = new byte[width * height];
List<Bgra32> palette;
// Get the palette using (PixelAccessor pixels = image.Lock())
List<Bgra32> palette = this.GetPalette(); {
// Call the FirstPass function if not a single pass algorithm.
// For something like an Octree quantizer, this will run through
// all image pixels, build a data structure, and create a palette.
if (!this.singlePass)
{
this.FirstPass(pixels, width, height);
}
// Get the palette
palette = this.GetPalette();
this.SecondPass(image, quantizedPixels, width, height); this.SecondPass(pixels, quantizedPixels, width, height);
}
return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels, this.TransparentIndex); return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels, this.TransparentIndex);
} }
@ -75,7 +78,7 @@ namespace ImageProcessorCore.Quantizers
/// <param name="source">The source data</param> /// <param name="source">The source data</param>
/// <param name="width">The width in pixels of the image.</param> /// <param name="width">The width in pixels of the image.</param>
/// <param name="height">The height in pixels of the image.</param> /// <param name="height">The height in pixels of the image.</param>
protected virtual void FirstPass(ImageBase source, int width, int height) protected virtual void FirstPass(PixelAccessor source, int width, int height)
{ {
// Loop through each row // Loop through each row
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
@ -96,7 +99,7 @@ namespace ImageProcessorCore.Quantizers
/// <param name="output">The output pixel array</param> /// <param name="output">The output pixel array</param>
/// <param name="width">The width in pixels of the image</param> /// <param name="width">The width in pixels of the image</param>
/// <param name="height">The height in pixels of the image</param> /// <param name="height">The height in pixels of the image</param>
protected virtual void SecondPass(ImageBase source, byte[] output, int width, int height) protected virtual void SecondPass(PixelAccessor source, byte[] output, int width, int height)
{ {
Parallel.For( Parallel.For(
0, 0,

36
src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs

@ -123,13 +123,16 @@ namespace ImageProcessorCore.Quantizers
this.Clear(); this.Clear();
this.Build3DHistogram(image); using (PixelAccessor imagePixels = image.Lock())
this.Get3DMoments(); {
this.Build3DHistogram(imagePixels);
this.Get3DMoments();
Box[] cube; Box[] cube;
this.BuildCube(out cube, ref colorCount); this.BuildCube(out cube, ref colorCount);
return this.GenerateResult(image, colorCount, cube); return this.GenerateResult(imagePixels, colorCount, cube);
}
} }
/// <summary> /// <summary>
@ -318,7 +321,7 @@ namespace ImageProcessorCore.Quantizers
/// Builds a 3-D color histogram of <c>counts, r/g/b, c^2</c>. /// Builds a 3-D color histogram of <c>counts, r/g/b, c^2</c>.
/// </summary> /// </summary>
/// <param name="image">The image.</param> /// <param name="image">The image.</param>
private void Build3DHistogram(ImageBase image) private void Build3DHistogram(PixelAccessor image)
{ {
for (int y = 0; y < image.Height; y++) for (int y = 0; y < image.Height; y++)
{ {
@ -711,15 +714,17 @@ namespace ImageProcessorCore.Quantizers
/// <summary> /// <summary>
/// Generates the quantized result. /// Generates the quantized result.
/// </summary> /// </summary>
/// <param name="image">The image.</param> /// <param name="imagePixels">The image pixels.</param>
/// <param name="colorCount">The color count.</param> /// <param name="colorCount">The color count.</param>
/// <param name="cube">The cube.</param> /// <param name="cube">The cube.</param>
/// <returns>The result.</returns> /// <returns>The result.</returns>
private QuantizedImage GenerateResult(ImageBase image, int colorCount, Box[] cube) private QuantizedImage GenerateResult(PixelAccessor imagePixels, int colorCount, Box[] cube)
{ {
List<Bgra32> pallette = new List<Bgra32>(); List<Bgra32> pallette = new List<Bgra32>();
byte[] pixels = new byte[image.Width * image.Height]; byte[] pixels = new byte[imagePixels.Width * imagePixels.Height];
int transparentIndex = -1; int transparentIndex = -1;
int width = imagePixels.Width;
int height = imagePixels.Height;
for (int k = 0; k < colorCount; k++) for (int k = 0; k < colorCount; k++)
{ {
@ -752,12 +757,12 @@ namespace ImageProcessorCore.Quantizers
Parallel.For( Parallel.For(
0, 0,
image.Height, height,
y => y =>
{ {
for (int x = 0; x < image.Width; x++) for (int x = 0; x < width; x++)
{ {
Bgra32 color = image[x, y]; Bgra32 color = imagePixels[x, y];
int a = color.A >> (8 - IndexAlphaBits); int a = color.A >> (8 - IndexAlphaBits);
int r = color.R >> (8 - IndexBits); int r = color.R >> (8 - IndexBits);
int g = color.G >> (8 - IndexBits); int g = color.G >> (8 - IndexBits);
@ -765,16 +770,17 @@ namespace ImageProcessorCore.Quantizers
if (transparentIndex > -1 && color.A <= this.Threshold) if (transparentIndex > -1 && color.A <= this.Threshold)
{ {
pixels[(y * image.Width) + x] = (byte)transparentIndex; pixels[(y * width) + x] = (byte)transparentIndex;
continue; continue;
} }
int ind = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1); int ind = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1);
pixels[(y * image.Width) + x] = this.tag[ind]; pixels[(y * width) + x] = this.tag[ind];
} }
}); });
return new QuantizedImage(image.Width, image.Height, pallette.ToArray(), pixels, transparentIndex);
return new QuantizedImage(width, height, pallette.ToArray(), pixels, transparentIndex);
} }
} }
} }

30
src/ImageProcessorCore/Samplers/Processors/CropProcessor.cs

@ -22,21 +22,25 @@ namespace ImageProcessorCore
int sourceX = sourceRectangle.X; int sourceX = sourceRectangle.X;
int sourceY = sourceRectangle.Y; int sourceY = sourceRectangle.Y;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
startY, using (PixelAccessor targetPixels = target.Lock())
endY,
y =>
{ {
if (y >= targetY && y < targetBottom) Parallel.For(
{ startY,
for (int x = startX; x < endX; x++) endY,
{ y =>
target[x, y] = source[x + sourceX, y + sourceY]; {
} if (y >= targetY && y < targetBottom)
{
for (int x = startX; x < endX; x++)
{
targetPixels[x, y] = sourcePixels[x + sourceX, y + sourceY];
}
this.OnRowProcessed(); this.OnRowProcessed();
} }
}); });
}
} }
} }
} }

55
src/ImageProcessorCore/Samplers/Processors/EntropyCropProcessor.cs

@ -8,7 +8,7 @@ namespace ImageProcessorCore
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using ImageProcessorCore.Filters; using Filters;
/// <summary> /// <summary>
/// Provides methods to allow the cropping of an image to preserve areas of highest /// Provides methods to allow the cropping of an image to preserve areas of highest
@ -42,21 +42,20 @@ namespace ImageProcessorCore
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnApply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle) protected override void OnApply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle)
{ {
using (ImageBase temp = new Image(source.Width, source.Height)) ImageBase temp = new Image(source.Width, source.Height);
{
// Detect the edges.
new Sobel().Apply(temp, source, sourceRectangle);
// Apply threshold binarization filter. // Detect the edges.
new Threshold(.5f).Apply(temp, temp, sourceRectangle); new Sobel().Apply(temp, source, sourceRectangle);
// Search for the first white pixels // Apply threshold binarization filter.
Rectangle rectangle = ImageMaths.GetFilteredBoundingRectangle(temp, 0); new Threshold(.5f).Apply(temp, temp, sourceRectangle);
// Reset the target pixel to the correct size. // Search for the first white pixels
target.SetPixels(rectangle.Width, rectangle.Height, new float[rectangle.Width * rectangle.Height * 4]); Rectangle rectangle = ImageMaths.GetFilteredBoundingRectangle(temp, 0);
this.cropRectangle = rectangle;
} // Reset the target pixel to the correct size.
target.SetPixels(rectangle.Width, rectangle.Height, new float[rectangle.Width * rectangle.Height * 4]);
this.cropRectangle = rectangle;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -73,21 +72,25 @@ namespace ImageProcessorCore
int startX = this.cropRectangle.X; int startX = this.cropRectangle.X;
int endX = this.cropRectangle.Right; int endX = this.cropRectangle.Right;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
startY, using (PixelAccessor targetPixels = target.Lock())
endY,
y =>
{ {
if (y >= targetY && y < targetBottom) Parallel.For(
{ startY,
for (int x = startX; x < endX; x++) endY,
{ y =>
target[x - startX, y - targetY] = source[x, y]; {
} if (y >= targetY && y < targetBottom)
} {
for (int x = startX; x < endX; x++)
{
targetPixels[x - startX, y - targetY] = sourcePixels[x, y];
}
}
this.OnRowProcessed(); this.OnRowProcessed();
}); });
}
} }
/// <inheritdoc/> /// <inheritdoc/>

185
src/ImageProcessorCore/Samplers/Processors/ResizeProcessor.cs

@ -62,7 +62,13 @@ namespace ImageProcessorCore
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) protected override void Apply(
ImageBase target,
ImageBase source,
Rectangle targetRectangle,
Rectangle sourceRectangle,
int startY,
int endY)
{ {
// Jump out, we'll deal with that later. // Jump out, we'll deal with that later.
if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle) if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle)
@ -87,30 +93,34 @@ namespace ImageProcessorCore
float widthFactor = sourceRectangle.Width / (float)targetRectangle.Width; float widthFactor = sourceRectangle.Width / (float)targetRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)targetRectangle.Height; float heightFactor = sourceRectangle.Height / (float)targetRectangle.Height;
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
startY, using (PixelAccessor targetPixels = target.Lock())
endY, {
y => Parallel.For(
{ startY,
if (targetY <= y && y < targetBottom) endY,
{ y =>
// Y coordinates of source points
int originY = (int)((y - startY) * heightFactor);
for (int x = startX; x < endX; x++)
{ {
if (targetX <= x && x < targetRight) if (targetY <= y && y < targetBottom)
{ {
// X coordinates of source points // Y coordinates of source points
int originX = (int)((x - startX) * widthFactor); int originY = (int)((y - startY) * heightFactor);
target[x, y] = source[originX, originY]; for (int x = startX; x < endX; x++)
} {
} if (targetX <= x && x < targetRight)
{
// X coordinates of source points
int originX = (int)((x - startX) * widthFactor);
this.OnRowProcessed(); targetPixels[x, y] = sourcePixels[originX, originY];
} }
}); }
this.OnRowProcessed();
}
});
}
// Break out now. // Break out now.
return; return;
@ -120,78 +130,90 @@ namespace ImageProcessorCore
// A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm // A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm
// First process the columns. Since we are not using multiple threads startY and endY // First process the columns. Since we are not using multiple threads startY and endY
// are the upper and lower bounds of the source rectangle. // are the upper and lower bounds of the source rectangle.
Parallel.For( using (PixelAccessor sourcePixels = source.Lock())
0, using (PixelAccessor firstPassPixels = this.firstPass.Lock())
sourceHeight, using (PixelAccessor targetPixels = target.Lock())
y => {
{ Parallel.For(
for (int x = startX; x < endX; x++) 0,
sourceHeight,
y =>
{ {
if (x >= 0 && x < width) for (int x = startX; x < endX; x++)
{ {
// Ensure offsets are normalised for cropping and padding. if (x >= 0 && x < width)
int offsetX = x - startX;
float sum = this.HorizontalWeights[offsetX].Sum;
Weight[] horizontalValues = this.HorizontalWeights[offsetX].Values;
// Destination color components
Color destination = new Color();
for (int i = 0; i < sum; i++)
{
Weight xw = horizontalValues[i];
int originX = xw.Index;
Color sourceColor = compand ? Color.Expand(source[originX, y]) : source[originX, y];
destination += sourceColor * xw.Value;
}
if (compand)
{ {
destination = Color.Compress(destination); // Ensure offsets are normalised for cropping and padding.
int offsetX = x - startX;
float sum = this.HorizontalWeights[offsetX].Sum;
Weight[] horizontalValues = this.HorizontalWeights[offsetX].Values;
// Destination color components
Color destination = new Color();
for (int i = 0; i < sum; i++)
{
Weight xw = horizontalValues[i];
int originX = xw.Index;
Color sourceColor = compand
? Color.Expand(sourcePixels[originX, y])
: sourcePixels[originX, y];
destination += sourceColor * xw.Value;
}
if (compand)
{
destination = Color.Compress(destination);
}
firstPassPixels[x, y] = destination;
} }
this.firstPass[x, y] = destination;
} }
} });
});
// Now process the rows. // Now process the rows.
Parallel.For( Parallel.For(
startY, startY,
endY, endY,
y => y =>
{
if (y >= 0 && y < height)
{ {
// Ensure offsets are normalised for cropping and padding. if (y >= 0 && y < height)
int offsetY = y - startY;
float sum = this.VerticalWeights[offsetY].Sum;
Weight[] verticalValues = this.VerticalWeights[offsetY].Values;
for (int x = 0; x < width; x++)
{ {
// Destination color components // Ensure offsets are normalised for cropping and padding.
Color destination = new Color(); int offsetY = y - startY;
float sum = this.VerticalWeights[offsetY].Sum;
for (int i = 0; i < sum; i++) Weight[] verticalValues = this.VerticalWeights[offsetY].Values;
{
Weight yw = verticalValues[i];
int originY = yw.Index;
Color sourceColor = compand ? Color.Expand(this.firstPass[x, originY]) : this.firstPass[x, originY];
destination += sourceColor * yw.Value;
}
if (compand) for (int x = 0; x < width; x++)
{ {
destination = Color.Compress(destination); // Destination color components
Color destination = new Color();
for (int i = 0; i < sum; i++)
{
Weight yw = verticalValues[i];
int originY = yw.Index;
Color sourceColor = compand
? Color.Expand(firstPassPixels[x, originY])
: firstPassPixels[x, originY];
destination += sourceColor * yw.Value;
}
if (compand)
{
destination = Color.Compress(destination);
}
targetPixels[x, y] = destination;
} }
target[x, y] = destination;
} }
}
this.OnRowProcessed(); this.OnRowProcessed();
}); });
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -202,9 +224,6 @@ namespace ImageProcessorCore
{ {
target.ClonePixels(target.Width, target.Height, source.Pixels); target.ClonePixels(target.Width, target.Height, source.Pixels);
} }
// Clean up
this.firstPass?.Dispose();
} }
/// <summary> /// <summary>

150
src/ImageProcessorCore/Samplers/Processors/RotateFlip.cs

@ -78,20 +78,26 @@ namespace ImageProcessorCore
int height = source.Height; int height = source.Height;
Image temp = new Image(height, width); Image temp = new Image(height, width);
Parallel.For(0, height, using (PixelAccessor sourcePixels = source.Lock())
y => using (PixelAccessor tempPixels = temp.Lock())
{ {
for (int x = 0; x < width; x++) Parallel.For(
{ 0,
int newX = height - y - 1; height,
newX = height - newX - 1; y =>
int newY = width - x - 1; {
newY = width - newY - 1; for (int x = 0; x < width; x++)
temp[newX, newY] = source[x, y]; {
} int newX = height - y - 1;
newX = height - newX - 1;
this.OnRowProcessed(); int newY = width - x - 1;
}); newY = width - newY - 1;
tempPixels[newX, newY] = sourcePixels[x, y];
}
this.OnRowProcessed();
});
}
target.SetPixels(height, width, temp.Pixels); target.SetPixels(height, width, temp.Pixels);
} }
@ -106,18 +112,24 @@ namespace ImageProcessorCore
int width = source.Width; int width = source.Width;
int height = source.Height; int height = source.Height;
Parallel.For(0, height, using (PixelAccessor sourcePixels = source.Lock())
y => using (PixelAccessor targetPixels = target.Lock())
{ {
for (int x = 0; x < width; x++) Parallel.For(
{ 0,
int newX = width - x - 1; height,
int newY = height - y - 1; y =>
target[newX, newY] = source[x, y]; {
} for (int x = 0; x < width; x++)
{
this.OnRowProcessed(); int newX = width - x - 1;
}); int newY = height - y - 1;
targetPixels[newX, newY] = sourcePixels[x, y];
}
this.OnRowProcessed();
});
}
} }
/// <summary> /// <summary>
@ -131,17 +143,23 @@ namespace ImageProcessorCore
int height = source.Height; int height = source.Height;
Image temp = new Image(height, width); Image temp = new Image(height, width);
Parallel.For(0, height, using (PixelAccessor sourcePixels = source.Lock())
y => using (PixelAccessor tempPixels = temp.Lock())
{ {
for (int x = 0; x < width; x++) Parallel.For(
{ 0,
int newX = height - y - 1; height,
temp[newX, x] = source[x, y]; y =>
} {
for (int x = 0; x < width; x++)
this.OnRowProcessed(); {
}); int newX = height - y - 1;
tempPixels[newX, x] = sourcePixels[x, y];
}
this.OnRowProcessed();
});
}
target.SetPixels(height, width, temp.Pixels); target.SetPixels(height, width, temp.Pixels);
} }
@ -159,18 +177,24 @@ namespace ImageProcessorCore
ImageBase temp = new Image(width, height); ImageBase temp = new Image(width, height);
temp.ClonePixels(width, height, target.Pixels); temp.ClonePixels(width, height, target.Pixels);
Parallel.For(0, halfHeight, using (PixelAccessor targetPixels = target.Lock())
y => using (PixelAccessor tempPixels = temp.Lock())
{ {
for (int x = 0; x < width; x++) Parallel.For(
0,
halfHeight,
y =>
{ {
int newY = height - y - 1; for (int x = 0; x < width; x++)
target[x, y] = temp[x, newY]; {
target[x, newY] = temp[x, y]; int newY = height - y - 1;
} targetPixels[x, y] = tempPixels[x, newY];
targetPixels[x, newY] = tempPixels[x, y];
this.OnRowProcessed(); }
});
this.OnRowProcessed();
});
}
} }
/// <summary> /// <summary>
@ -186,18 +210,24 @@ namespace ImageProcessorCore
ImageBase temp = new Image(width, height); ImageBase temp = new Image(width, height);
temp.ClonePixels(width, height, target.Pixels); temp.ClonePixels(width, height, target.Pixels);
Parallel.For(0, height, using (PixelAccessor targetPixels = target.Lock())
y => using (PixelAccessor tempPixels = temp.Lock())
{ {
for (int x = 0; x < halfWidth; x++) Parallel.For(
{ 0,
int newX = width - x - 1; height,
target[x, y] = temp[newX, y]; y =>
target[newX, y] = temp[x, y]; {
} for (int x = 0; x < halfWidth; x++)
{
this.OnRowProcessed(); int newX = width - x - 1;
}); targetPixels[x, y] = tempPixels[newX, y];
targetPixels[newX, y] = tempPixels[x, y];
}
this.OnRowProcessed();
});
}
} }
} }
} }

43
src/ImageProcessorCore/Samplers/Processors/RotateProcessor.cs

@ -104,30 +104,27 @@ namespace ImageProcessorCore
// Since we are not working in parallel we use full height and width // Since we are not working in parallel we use full height and width
// of the first pass image. // of the first pass image.
Parallel.For( using (PixelAccessor firstPassPixels = this.firstPass.Lock())
0, using (PixelAccessor targetPixels = target.Lock())
height, {
y => Parallel.For(
{ 0,
for (int x = startX; x < endX; x++) height,
{ y =>
// Rotate at the centre point
Point rotated = Point.Rotate(new Point(x, y), rotation);
if (this.firstPass.Bounds.Contains(rotated.X, rotated.Y))
{ {
target[x, y] = this.firstPass[rotated.X, rotated.Y]; for (int x = startX; x < endX; x++)
} {
} // Rotate at the centre point
Point rotated = Point.Rotate(new Point(x, y), rotation);
this.OnRowProcessed(); if (this.firstPass.Bounds.Contains(rotated.X, rotated.Y))
}); {
} targetPixels[x, y] = firstPassPixels[rotated.X, rotated.Y];
}
/// <inheritdoc/> }
protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle)
{ this.OnRowProcessed();
// Cleanup. });
this.firstPass.Dispose(); }
} }
} }
} }

43
src/ImageProcessorCore/Samplers/Processors/SkewProcessor.cs

@ -135,30 +135,27 @@ namespace ImageProcessorCore
// Since we are not working in parallel we use full height and width // Since we are not working in parallel we use full height and width
// of the first pass image. // of the first pass image.
Parallel.For( using (PixelAccessor firstPassPixels = this.firstPass.Lock())
0, using (PixelAccessor targetPixels = target.Lock())
height, {
y => Parallel.For(
{ 0,
for (int x = startX; x < endX; x++) height,
{ y =>
// Skew at the centre point
Point skewed = Point.Skew(new Point(x, y), skew);
if (this.firstPass.Bounds.Contains(skewed.X, skewed.Y))
{ {
target[x, y] = this.firstPass[skewed.X, skewed.Y]; for (int x = startX; x < endX; x++)
} {
} // Skew at the centre point
Point skewed = Point.Skew(new Point(x, y), skew);
this.OnRowProcessed(); if (this.firstPass.Bounds.Contains(skewed.X, skewed.Y))
}); {
} targetPixels[x, y] = firstPassPixels[skewed.X, skewed.Y];
}
/// <inheritdoc/> }
protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle)
{ this.OnRowProcessed();
// Cleanup. });
this.firstPass.Dispose(); }
} }
} }
} }

7
tests/ImageProcessorCore.Benchmarks/Image/GetSetPixel.cs

@ -23,10 +23,11 @@
[Benchmark(Description = "ImageProcessorCore GetSet Pixel")] [Benchmark(Description = "ImageProcessorCore GetSet Pixel")]
public CoreColor ResizeCore() public CoreColor ResizeCore()
{ {
using (CoreImage image = new CoreImage(400, 400)) CoreImage image = new CoreImage(400, 400);
using (PixelAccessor imagePixels = image.Lock())
{ {
image[200, 200] = CoreColor.White; imagePixels[200, 200] = CoreColor.White;
return image[200, 200]; return imagePixels[200, 200];
} }
} }
} }

10
tests/ImageProcessorCore.Benchmarks/Samplers/Crop.cs

@ -4,8 +4,6 @@
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using ImageProcessorCore.Processors;
using CoreImage = ImageProcessorCore.Image; using CoreImage = ImageProcessorCore.Image;
using CoreSize = ImageProcessorCore.Size; using CoreSize = ImageProcessorCore.Size;
@ -34,11 +32,9 @@
[Benchmark(Description = "ImageProcessorCore Crop")] [Benchmark(Description = "ImageProcessorCore Crop")]
public CoreSize CropResizeCore() public CoreSize CropResizeCore()
{ {
using (CoreImage image = new CoreImage(400, 400)) CoreImage image = new CoreImage(400, 400);
{ image.Crop(100, 100);
image.Crop(100, 100); return new CoreSize(image.Width, image.Height);
return new CoreSize(image.Width, image.Height);
}
} }
} }
} }

10
tests/ImageProcessorCore.Benchmarks/Samplers/Resize.cs

@ -4,8 +4,6 @@
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using ImageProcessorCore.Processors;
using CoreImage = ImageProcessorCore.Image; using CoreImage = ImageProcessorCore.Image;
using CoreSize = ImageProcessorCore.Size; using CoreSize = ImageProcessorCore.Size;
@ -34,11 +32,9 @@
[Benchmark(Description = "ImageProcessorCore Resize")] [Benchmark(Description = "ImageProcessorCore Resize")]
public CoreSize ResizeCore() public CoreSize ResizeCore()
{ {
using (CoreImage image = new CoreImage(400, 400)) CoreImage image = new CoreImage(400, 400);
{ image.Resize(100, 100);
image.Resize(100, 100); return new CoreSize(image.Width, image.Height);
return new CoreSize(image.Width, image.Height);
}
} }
} }
} }

24
tests/ImageProcessorCore.Tests/Formats/BitmapTests.cs

@ -27,23 +27,23 @@ namespace ImageProcessorCore.Tests
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
using (Image image = new Image(stream))
{
string encodeFilename = "TestOutput/Encode/Bitmap/" + "24-" + Path.GetFileNameWithoutExtension(file) + ".bmp";
using (FileStream output = File.OpenWrite(encodeFilename)) Image image = new Image(stream);
{ string encodeFilename = "TestOutput/Encode/Bitmap/" + "24-" + Path.GetFileNameWithoutExtension(file) + ".bmp";
image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel24 });
} using (FileStream output = File.OpenWrite(encodeFilename))
{
image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel24 });
}
encodeFilename = "TestOutput/Encode/Bitmap/" + "32-" + Path.GetFileNameWithoutExtension(file) + ".bmp"; encodeFilename = "TestOutput/Encode/Bitmap/" + "32-" + Path.GetFileNameWithoutExtension(file) + ".bmp";
using (FileStream output = File.OpenWrite(encodeFilename)) using (FileStream output = File.OpenWrite(encodeFilename))
{ {
image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32 }); image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32 });
}
} }
Trace.WriteLine($"{file} : {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{file} : {watch.ElapsedMilliseconds}ms");
} }
} }

127
tests/ImageProcessorCore.Tests/Formats/EncoderDecoderTests.cs

@ -30,11 +30,10 @@ namespace ImageProcessorCore.Tests
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
using (Image image = new Image(stream))
{ Image image = new Image(stream);
string filename = "TestOutput/ToString/" + Path.GetFileNameWithoutExtension(file) + ".txt"; string filename = "TestOutput/ToString/" + Path.GetFileNameWithoutExtension(file) + ".txt";
File.WriteAllText(filename, image.ToString()); File.WriteAllText(filename, image.ToString());
}
Trace.WriteLine($"{watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{watch.ElapsedMilliseconds}ms");
} }
@ -54,14 +53,13 @@ namespace ImageProcessorCore.Tests
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
using (Image image = new Image(stream))
{
string encodeFilename = "TestOutput/Encode/" + Path.GetFileName(file);
using (FileStream output = File.OpenWrite(encodeFilename)) Image image = new Image(stream);
{ string encodeFilename = "TestOutput/Encode/" + Path.GetFileName(file);
image.Save(output);
} using (FileStream output = File.OpenWrite(encodeFilename))
{
image.Save(output);
} }
Trace.WriteLine($"{file} : {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{file} : {watch.ElapsedMilliseconds}ms");
@ -81,37 +79,32 @@ namespace ImageProcessorCore.Tests
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
using (Image image = new Image(stream)) Image image = new Image(stream);
IQuantizer quantizer = new OctreeQuantizer();
QuantizedImage quantizedImage = quantizer.Quantize(image, 256);
using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Octree-{Path.GetFileName(file)}"))
{ {
IQuantizer quantizer = new OctreeQuantizer(); Image qi = quantizedImage.ToImage();
QuantizedImage quantizedImage = quantizer.Quantize(image, 256); qi.Save(output, image.CurrentImageFormat);
using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Octree-{Path.GetFileName(file)}")) }
{
using (Image qi = quantizedImage.ToImage())
{
qi.Save(output, image.CurrentImageFormat);
}
}
quantizer = new WuQuantizer(); quantizer = new WuQuantizer();
quantizedImage = quantizer.Quantize(image, 256); quantizedImage = quantizer.Quantize(image, 256);
using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Wu-{Path.GetFileName(file)}")) using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Wu-{Path.GetFileName(file)}"))
{ {
quantizedImage.ToImage().Save(output, image.CurrentImageFormat); quantizedImage.ToImage().Save(output, image.CurrentImageFormat);
} }
quantizer = new PaletteQuantizer(); quantizer = new PaletteQuantizer();
quantizedImage = quantizer.Quantize(image, 256); quantizedImage = quantizer.Quantize(image, 256);
using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Palette-{Path.GetFileName(file)}")) using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Palette-{Path.GetFileName(file)}"))
{ {
using (Image qi = quantizedImage.ToImage()) Image qi = quantizedImage.ToImage();
{ qi.Save(output, image.CurrentImageFormat);
qi.Save(output, image.CurrentImageFormat);
}
}
} }
} }
} }
@ -129,27 +122,25 @@ namespace ImageProcessorCore.Tests
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.gif"))
{ {
using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.gif")) image.SaveAsGif(output);
{ }
image.SaveAsGif(output);
}
using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.bmp")) using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.bmp"))
{ {
image.SaveAsBmp(output); image.SaveAsBmp(output);
} }
using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.jpg")) using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.jpg"))
{ {
image.SaveAsJpeg(output); image.SaveAsJpeg(output);
} }
using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.png")) using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.png"))
{ {
image.SaveAsPng(output); image.SaveAsPng(output);
}
} }
} }
} }
@ -167,25 +158,21 @@ namespace ImageProcessorCore.Tests
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
using (Image image = new Image(stream)) Image image = new Image(stream);
byte[] serialized;
using (MemoryStream memoryStream = new MemoryStream())
{ {
byte[] serialized; image.Save(memoryStream);
using (MemoryStream memoryStream = new MemoryStream()) memoryStream.Flush();
{ serialized = memoryStream.ToArray();
image.Save(memoryStream); }
memoryStream.Flush();
serialized = memoryStream.ToArray();
}
using (MemoryStream memoryStream = new MemoryStream(serialized)) using (MemoryStream memoryStream = new MemoryStream(serialized))
{
Image image2 = new Image(memoryStream);
using (FileStream output = File.OpenWrite($"TestOutput/Serialized/{Path.GetFileName(file)}"))
{ {
using (Image image2 = new Image(memoryStream)) image2.Save(output);
{
using (FileStream output = File.OpenWrite($"TestOutput/Serialized/{Path.GetFileName(file)}"))
{
image2.Save(output);
}
}
} }
} }
} }

10
tests/ImageProcessorCore.Tests/Formats/PngTests.cs

@ -25,13 +25,11 @@ namespace ImageProcessorCore.Tests
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Encode/Png/{Path.GetFileNameWithoutExtension(file)}.png"))
{ {
using (FileStream output = File.OpenWrite($"TestOutput/Encode/Png/{Path.GetFileNameWithoutExtension(file)}.png")) image.Quality = 256;
{ image.Save(output, new PngFormat());
image.Quality = 256;
image.Save(output, new PngFormat());
}
} }
} }
} }

16
tests/ImageProcessorCore.Tests/Processors/Filters/FilterTests.cs

@ -70,16 +70,16 @@ namespace ImageProcessorCore.Tests
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
using (Image image = new Image(stream))
Image image = new Image(stream);
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Filter/{Path.GetFileName(filename)}"))
{ {
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); processor.OnProgress += this.ProgressUpdate;
using (FileStream output = File.OpenWrite($"TestOutput/Filter/{Path.GetFileName(filename)}")) image.Process(processor).Save(output);
{ processor.OnProgress -= this.ProgressUpdate;
processor.OnProgress += this.ProgressUpdate;
image.Process(processor).Save(output);
processor.OnProgress -= this.ProgressUpdate;
}
} }
Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms");
} }
} }

33
tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

@ -70,7 +70,6 @@
processor.OnProgress -= this.ProgressUpdate; processor.OnProgress -= this.ProgressUpdate;
} }
image.Dispose();
Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms");
} }
} }
@ -92,7 +91,7 @@
string filename = Path.GetFileName(file); string filename = Path.GetFileName(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Pad/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/Pad/{filename}"))
{ {
image.Pad(image.Width + 50, image.Height + 50, this.ProgressUpdate) image.Pad(image.Width + 50, image.Height + 50, this.ProgressUpdate)
@ -120,7 +119,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{ {
image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate) image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate)
@ -149,7 +148,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{ {
image.Resize(image.Width / 3, 0, new TriangleResampler(), false, this.ProgressUpdate) image.Resize(image.Width / 3, 0, new TriangleResampler(), false, this.ProgressUpdate)
@ -178,7 +177,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{ {
image.Resize(0, image.Height / 3, new TriangleResampler(), false, this.ProgressUpdate) image.Resize(0, image.Height / 3, new TriangleResampler(), false, this.ProgressUpdate)
@ -205,7 +204,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileName(file); string filename = Path.GetFileName(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/ResizeCrop/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/ResizeCrop/{filename}"))
{ {
ResizeOptions options = new ResizeOptions() ResizeOptions options = new ResizeOptions()
@ -237,7 +236,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileName(file); string filename = Path.GetFileName(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/ResizePad/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/ResizePad/{filename}"))
{ {
ResizeOptions options = new ResizeOptions() ResizeOptions options = new ResizeOptions()
@ -270,7 +269,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileName(file); string filename = Path.GetFileName(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/ResizeBoxPad/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/ResizeBoxPad/{filename}"))
{ {
ResizeOptions options = new ResizeOptions() ResizeOptions options = new ResizeOptions()
@ -303,7 +302,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileName(file); string filename = Path.GetFileName(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/ResizeMax/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/ResizeMax/{filename}"))
{ {
ResizeOptions options = new ResizeOptions() ResizeOptions options = new ResizeOptions()
@ -337,7 +336,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileName(file); string filename = Path.GetFileName(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/ResizeMin/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/ResizeMin/{filename}"))
{ {
ResizeOptions options = new ResizeOptions() ResizeOptions options = new ResizeOptions()
@ -370,7 +369,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileName(file); string filename = Path.GetFileName(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/ResizeStretch/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/ResizeStretch/{filename}"))
{ {
ResizeOptions options = new ResizeOptions() ResizeOptions options = new ResizeOptions()
@ -417,8 +416,6 @@
.Save(output); .Save(output);
} }
image.Dispose();
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
} }
} }
@ -440,7 +437,7 @@
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileNameWithoutExtension(file) + "-" + rotateType + flipType + Path.GetExtension(file); string filename = Path.GetFileNameWithoutExtension(file) + "-" + rotateType + flipType + Path.GetExtension(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/RotateFlip/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/RotateFlip/{filename}"))
{ {
image.RotateFlip(rotateType, flipType, this.ProgressUpdate) image.RotateFlip(rotateType, flipType, this.ProgressUpdate)
@ -468,7 +465,7 @@
string filename = Path.GetFileName(file); string filename = Path.GetFileName(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Rotate/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/Rotate/{filename}"))
{ {
image.Rotate(63, this.ProgressUpdate) image.Rotate(63, this.ProgressUpdate)
@ -497,7 +494,7 @@
string filename = Path.GetFileName(file); string filename = Path.GetFileName(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Skew/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/Skew/{filename}"))
{ {
image.Skew(20, 10, this.ProgressUpdate) image.Skew(20, 10, this.ProgressUpdate)
@ -523,7 +520,7 @@
{ {
string filename = Path.GetFileNameWithoutExtension(file) + "-EntropyCrop" + Path.GetExtension(file); string filename = Path.GetFileNameWithoutExtension(file) + "-EntropyCrop" + Path.GetExtension(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/EntropyCrop/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/EntropyCrop/{filename}"))
{ {
image.EntropyCrop(.5f, this.ProgressUpdate).Save(output); image.EntropyCrop(.5f, this.ProgressUpdate).Save(output);
@ -547,7 +544,7 @@
string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file); string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file);
using (Image image = new Image(stream)) Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Crop/{filename}")) using (FileStream output = File.OpenWrite($"TestOutput/Crop/{filename}"))
{ {
image.Crop(image.Width / 2, image.Height / 2, this.ProgressUpdate).Save(output); image.Crop(image.Width / 2, image.Height / 2, this.ProgressUpdate).Save(output);

Loading…
Cancel
Save