diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
new file mode 100644
index 0000000000..af6d32b216
--- /dev/null
+++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Processing
+{
+ ///
+ /// Defines extensions that allow the computation of image integrals on an
+ ///
+ public static partial class ProcessingExtensions
+ {
+ ///
+ /// Apply an image integral.
+ ///
+ /// The image on which to apply the integral.
+ /// The type of the pixel.
+ /// The containing all the sums.
+ public static Buffer2D CalculateIntegralImage(this Image source)
+ where TPixel : unmanaged, IPixel
+ {
+ Configuration configuration = source.GetConfiguration();
+
+ int endY = source.Height;
+ int endX = source.Width;
+
+ Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height);
+ ulong sumX0 = 0;
+
+ using (IMemoryOwner tempRow = configuration.MemoryAllocator.Allocate(source.Width))
+ {
+ Span tempSpan = tempRow.GetSpan();
+ Span sourceRow = source.GetPixelRowSpan(0);
+ Span destRow = intImage.GetRowSpan(0);
+
+ PixelOperations.Instance.ToL8(configuration, sourceRow, tempSpan);
+
+ // First row
+ for (int x = 0; x < endX; x++)
+ {
+ sumX0 += tempSpan[x].PackedValue;
+ destRow[x] = sumX0;
+ }
+
+ Span previousDestRow = destRow;
+
+ // All other rows
+ for (int y = 1; y < endY; y++)
+ {
+ sourceRow = source.GetPixelRowSpan(y);
+ destRow = intImage.GetRowSpan(y);
+
+ PixelOperations.Instance.ToL8(configuration, sourceRow, tempSpan);
+
+ // Process first column
+ sumX0 = tempSpan[0].PackedValue;
+ destRow[0] = sumX0 + previousDestRow[0];
+
+ // Process all other colmns
+ for (int x = 1; x < endX; x++)
+ {
+ sumX0 += tempSpan[x].PackedValue;
+ destRow[x] = sumX0 + previousDestRow[x];
+ }
+
+ previousDestRow = destRow;
+ }
+ }
+
+ return intImage;
+ }
+ }
+}
diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs
index 0bf83812d0..ac14d84231 100644
--- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs
+++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs
@@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing
///
/// Adds extensions that allow the processing of images to the type.
///
- public static class ProcessingExtensions
+ public static partial class ProcessingExtensions
{
///
/// Mutates the source image by applying the image operation to it.
diff --git a/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs b/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs
new file mode 100644
index 0000000000..481463f47e
--- /dev/null
+++ b/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs
@@ -0,0 +1,110 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests.Processing.Transforms
+{
+ public class IntegralImageTests : BaseImageOperationsExtensionTest
+ {
+ [Theory]
+ [WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
+ public void CalculateIntegralImage_Rgba32Works(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage();
+
+ // Act:
+ Buffer2D integralBuffer = image.CalculateIntegralImage();
+
+ // Assert:
+ VerifySumValues(provider, integralBuffer, (Rgba32 pixel) =>
+ {
+ L8 outputPixel = default;
+
+ outputPixel.FromRgba32(pixel);
+
+ return outputPixel.PackedValue;
+ });
+ }
+
+ [Theory]
+ [WithFile(TestImages.Png.Bradley01, PixelTypes.L8)]
+ [WithFile(TestImages.Png.Bradley02, PixelTypes.L8)]
+ public void CalculateIntegralImage_L8Works(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage();
+
+ // Act:
+ Buffer2D integralBuffer = image.CalculateIntegralImage();
+
+ // Assert:
+ VerifySumValues(provider, integralBuffer, (L8 pixel) => { return pixel.PackedValue; });
+ }
+
+ private static void VerifySumValues(
+ TestImageProvider provider,
+ Buffer2D integralBuffer,
+ System.Func getPixel)
+ where TPixel : unmanaged, IPixel
+ {
+ Image image = provider.GetImage();
+
+ // Check top-left corner
+ Assert.Equal(getPixel(image[0, 0]), integralBuffer[0, 0]);
+
+ ulong pixelValues = 0;
+
+ pixelValues += getPixel(image[0, 0]);
+ pixelValues += getPixel(image[1, 0]);
+ pixelValues += getPixel(image[0, 1]);
+ pixelValues += getPixel(image[1, 1]);
+
+ // Check top-left 2x2 pixels
+ Assert.Equal(pixelValues, integralBuffer[1, 1]);
+
+ pixelValues = 0;
+
+ pixelValues += getPixel(image[image.Width - 3, 0]);
+ pixelValues += getPixel(image[image.Width - 2, 0]);
+ pixelValues += getPixel(image[image.Width - 1, 0]);
+ pixelValues += getPixel(image[image.Width - 3, 1]);
+ pixelValues += getPixel(image[image.Width - 2, 1]);
+ pixelValues += getPixel(image[image.Width - 1, 1]);
+
+ // Check top-right 3x2 pixels
+ Assert.Equal(pixelValues, integralBuffer[image.Width - 1, 1] + 0 - 0 - integralBuffer[image.Width - 4, 1]);
+
+ pixelValues = 0;
+
+ pixelValues += getPixel(image[0, image.Height - 3]);
+ pixelValues += getPixel(image[0, image.Height - 2]);
+ pixelValues += getPixel(image[0, image.Height - 1]);
+ pixelValues += getPixel(image[1, image.Height - 3]);
+ pixelValues += getPixel(image[1, image.Height - 2]);
+ pixelValues += getPixel(image[1, image.Height - 1]);
+
+ // Check bottom-left 2x3 pixels
+ Assert.Equal(pixelValues, integralBuffer[1, image.Height - 1] + 0 - integralBuffer[1, image.Height - 4] - 0);
+
+ pixelValues = 0;
+
+ pixelValues += getPixel(image[image.Width - 3, image.Height - 3]);
+ pixelValues += getPixel(image[image.Width - 2, image.Height - 3]);
+ pixelValues += getPixel(image[image.Width - 1, image.Height - 3]);
+ pixelValues += getPixel(image[image.Width - 3, image.Height - 2]);
+ pixelValues += getPixel(image[image.Width - 2, image.Height - 2]);
+ pixelValues += getPixel(image[image.Width - 1, image.Height - 2]);
+ pixelValues += getPixel(image[image.Width - 3, image.Height - 1]);
+ pixelValues += getPixel(image[image.Width - 2, image.Height - 1]);
+ pixelValues += getPixel(image[image.Width - 1, image.Height - 1]);
+
+ // Check bottom-right 3x3 pixels
+ Assert.Equal(pixelValues, integralBuffer[image.Width - 1, image.Height - 1] + integralBuffer[image.Width - 4, image.Height - 4] - integralBuffer[image.Width - 1, image.Height - 4] - integralBuffer[image.Width - 4, image.Height - 1]);
+ }
+ }
+}