Commit 54253ef1 authored by Brandon Petty's avatar Brandon Petty
Browse files

Added Convolution and Edge Detector classes along with a color distance...

Added Convolution and Edge Detector classes along with a color distance algorithm for the RGB color space.
parent 8e48250d
// Δoxa Binarization Framework
// License: CC0 2018, "Freely you have received; freely give." - Matt 10:8
#ifndef CONVOLUTION_HPP
#define CONVOLUTION_HPP
#include <algorithm>
#include <vector>
#include "Types.hpp"
#include "Image.hpp"
#include "Region.hpp"
namespace Doxa
{
class Convolution
{
public:
typedef std::vector<int> ConvolutionImage;
/// <summary>
/// This method is used primarily for detecting veritcal edges in an image
/// </summary>
void SobelVertical(ConvolutionImage& convolutionImage, const Image& grayScaleImage)
{
const int kernelSize = 3;
const int kernelArraySize = kernelSize * kernelSize;
const int kernel[kernelArraySize] = {
-1, 0, 1,
-2, 0, 2,
-1, 0, 1
};
Convolve(convolutionImage, grayScaleImage, kernelSize, kernel);
}
/// <summary>
/// This method is used primarily for detecting horizontal edges in an image
/// </summary>
void SobelHorizontal(ConvolutionImage& convolutionImage, const Image& grayScaleImage)
{
const int kernelSize = 3;
const int kernelArraySize = kernelSize * kernelSize;
const int kernel[kernelArraySize] = {
-1, -2, -1,
0, 0, 0,
1, 2, 1
};
Convolve(convolutionImage, grayScaleImage, kernelSize, kernel);
}
/// <summary>
/// This method is used primarily for detecting edges in an image
/// </summary>
void Laplacian(ConvolutionImage& convolutionImage, const Image& grayScaleImage)
{
const int kernelSize = 3;
const int kernelArraySize = kernelSize * kernelSize;
const int kernel[kernelArraySize] = {
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
};
Convolve(convolutionImage, grayScaleImage, kernelSize, kernel);
}
/// <summary>
/// Convolve allows you to generate a convolution image from a kernel.
/// This algorithm can generically calculate varying sized kernels with very little performance tradeoff.
/// When part of a window is outside of the image, the closest pixel's value will be used for those cells.
/// </summary>
/// <param name="convolutionImage">Can contain negative numbers and numbers larger than 255</param>
/// <param name="grayScaleImage">Gray Scale input image</param>
/// <param name="windowSize">The size of the kernel. Must be odd.</param>
/// <param name="kernel">A kernel of any size</param>
void Convolve(ConvolutionImage& convolutionImage, const Image& grayScaleImage, const int windowSize, const int kernel[])
{
const int widthMinusOne = grayScaleImage.width - 1;
const int heightMinusOne = grayScaleImage.height - 1;
const int HALF_WINDOW = windowSize / 2;
Region window;
for (int yImageIdx = 0; yImageIdx < grayScaleImage.height; ++yImageIdx)
{
// Set Y Window Coordinates
window.upperLeft.y = yImageIdx - HALF_WINDOW;
window.bottomRight.y = yImageIdx + HALF_WINDOW;
for (int xImageIdx = 0; xImageIdx < grayScaleImage.width; ++xImageIdx)
{
// Set X Window Coordinates
window.upperLeft.x = xImageIdx - HALF_WINDOW;
window.bottomRight.x = xImageIdx + HALF_WINDOW;
const int rowImageIdx = yImageIdx * grayScaleImage.width;
int kernelIdx = 0;
int sum = 0;
// Iterate the Window
for (int yWindowIdx = window.upperLeft.y; yWindowIdx <= window.bottomRight.y; ++yWindowIdx)
{
// Ensure that if our window is outside the image, adjust where to get the right value.
int yWindowAdjustedIdx = std::min(heightMinusOne, yWindowIdx);
yWindowAdjustedIdx = std::max(0, yWindowAdjustedIdx);
const int rowWindowAdjustedIdx = yWindowAdjustedIdx * grayScaleImage.width;
for (int xWindowIdx = window.upperLeft.x; xWindowIdx <= window.bottomRight.x; ++xWindowIdx)
{
// Ensure that if our window is outside the image, adjust where to get the right value.
int xWindowAdjustedIdx = std::min(widthMinusOne, xWindowIdx);
xWindowAdjustedIdx = std::max(0, xWindowAdjustedIdx);
// Multiply the Kernel Cell by the Image Cell and add to Sum
sum += grayScaleImage.data[rowWindowAdjustedIdx + xWindowAdjustedIdx] * kernel[kernelIdx];
// We iterate our Window using X/Y coordinates, but the Kernel is simply incremented
kernelIdx++;
}
}
convolutionImage[rowImageIdx + xImageIdx] = sum;
}
}
}
};
}
#endif // CONVOLUTION_HPP
......@@ -83,7 +83,9 @@
<ClInclude Include="Algorithm.hpp" />
<ClInclude Include="Bernsen.hpp" />
<ClInclude Include="ContrastImage.hpp" />
<ClInclude Include="Convolution.hpp" />
<ClInclude Include="DRDM.hpp" />
<ClInclude Include="EdgeDetector.hpp" />
<ClInclude Include="Gatos.hpp" />
<ClInclude Include="Image.hpp" />
<ClInclude Include="ISauvola.hpp" />
......
......@@ -63,6 +63,12 @@
<ClInclude Include="ISauvola.hpp">
<Filter>Binarization</Filter>
</ClInclude>
<ClInclude Include="Convolution.hpp">
<Filter>Processors</Filter>
</ClInclude>
<ClInclude Include="EdgeDetector.hpp">
<Filter>Processors</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Binarization">
......
// Δoxa Binarization Framework
// License: CC0 2018, "Freely you have received; freely give." - Matt 10:8
#ifndef EDGEDETECTOR_HPP
#define EDGEDETECTOR_HPP
#include "Convolution.hpp"
////////////////////////////////////////////////////////////////////////
// This code is highly experimental and has not been unit tested yet! //
////////////////////////////////////////////////////////////////////////
namespace Doxa
{
class EdgeDetector
{
public:
static void Sobel(Image& sobelImage, const Image& grayScaleImage)
{
// Generate Sobel Convolution Images
Convolution::ConvolutionImage horizontalImage(grayScaleImage.size);
Convolution::ConvolutionImage verticalImage(grayScaleImage.size);
Convolution convolution;
convolution.SobelHorizontal(horizontalImage, grayScaleImage);
convolution.SobelVertical(verticalImage, grayScaleImage);
for (int idx = 0; idx < grayScaleImage.size; ++idx)
{
// Calculate the Magnitude of the Gradient
// TODO: Analyze estimation via absolute value to improve speed
int value = std::sqrt((horizontalImage[idx] * horizontalImage[idx]) + (verticalImage[idx] * verticalImage[idx]));
value = std::min(value, 255);
// Invert the image
sobelImage.data[idx] = 255 - value;
}
}
static void Laplacian(Image& laplacianImage, const Image& grayScaleImage)
{
// Generate Laplacian
Convolution::ConvolutionImage convolutionImage(grayScaleImage.size);
Convolution convolution;
convolution.Laplacian(convolutionImage, grayScaleImage);
// Normalize it by cutting off values out of range
for (int idx = 0; idx < grayScaleImage.size; ++idx)
{
int value = convolutionImage[idx];
// Cut off any outliers, for which there are many, leaving only the edges.
value = std::min(value, 255);
value = std::max(value, 0);
// Invert the image
laplacianImage.data[idx] = 255 - value;
}
}
};
}
#endif //EDGEDETECTOR_HPP
......@@ -39,6 +39,22 @@ namespace Doxa
return (rgba & 0x00ffffff) | (a << 24);
}
/// <summary>
/// Using the RGB color space, calculate the difference between two colors.
/// The results should be very similar to equations using the more common L*u*v* color space.
/// </summary>
/// <remarks>https://www.compuphase.com/cmetric.htm</remarks>
/// <returns></returns>
static inline int ColorDistance(Pixel32 left, Pixel32 right)
{
const int rmean = (Red(left) + Red(right)) / 2;
const int r = Red(left) - Red(right);
const int g = Green(left) - Green(right);
const int b = Blue(left) - Blue(right);
return std::sqrt( ((2 + rmean/256) * r*r) + (4 * g*g) + ((2 + (255 - rmean)/256) * b*b) );
}
// Gray Scale Helpers
static inline constexpr Pixel8 Gray(int r, int g, int b)
{
......
#include "TestUtilities.hpp"
#include "../Convolution.hpp"
namespace Doxa::UnitTests
{
TEST_CLASS(ConvolutionTests)
{
public:
TEST_METHOD(ConvolutionConvolve3x3Test)
{
// Setup
const Pixel8 data[9] = {
1, 1, 1,
1, 1, 1,
1, 1, 1
};
Image image(3, 3, data);
const int kernel[9] = {
1, 1, 1,
1, 1, 1,
1, 1, 1
};
// Test
Convolution::ConvolutionImage convolutionImage(image.size);
Convolution convolution;
convolution.Convolve(convolutionImage, image, 3, kernel);
// Ensure that all elements equal 9
bool isMiscalculated = std::any_of(std::begin(convolutionImage), std::end(convolutionImage), [&](int value)
{
return value != 9;
});
Assert::IsFalse(isMiscalculated);
}
TEST_METHOD(ConvolutionConvolve5x5Test)
{
// Setup
const Pixel8 data[9] = {
1, 1, 1,
1, 1, 1,
1, 1, 1
};
Image image(3, 3, data);
// Note: Yes, the kernel is larger than our entire image.
const int kernel[25] = {
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1
};
// Test
Convolution::ConvolutionImage convolutionImage(image.size);
Convolution convolution;
convolution.Convolve(convolutionImage, image, 5, kernel);
// Ensure that all elements equal 200
bool isMiscalculated = std::any_of(std::begin(convolutionImage), std::end(convolutionImage), [&](int value)
{
return value != 25;
});
Assert::IsFalse(isMiscalculated);
}
};
}
#include "TestUtilities.hpp"
#include "../EdgeDetector.hpp"
namespace Doxa::UnitTests
{
TEST_CLASS(EdgeDetectorTests)
{
public:
// Initialized Objects
static Image image;
static std::string projFolder;
TEST_CLASS_INITIALIZE(Initialize)
{
projFolder = TestUtilities::ProjectFolder();
// Load Color Image
const std::string filePath = projFolder + "2JohnC1V3.ppm";
image = PNM::Read(filePath);
}
TEST_METHOD(EdgeDetectorLaplacianTest)
{
Image laplacianImage(image.width, image.height);
EdgeDetector::Laplacian(laplacianImage, image);
// TODO: Assert
PNM::Write(laplacianImage, projFolder + "2JohnC1V3-LaplacianEdgeDetector.ppm");
}
TEST_METHOD(EdgeDetectorSobelTest)
{
Image sobelImage(image.width, image.height);
EdgeDetector::Sobel(sobelImage, image);
// TODO: Assert
PNM::Write(sobelImage, projFolder + "2JohnC1V3-SobelEdgeDetector.ppm");
}
};
// Static objects
Image EdgeDetectorTests::image;
std::string EdgeDetectorTests::projFolder;
}
......@@ -39,5 +39,48 @@ namespace Doxa::UnitTests
Assert::IsFalse(Palette::IsGray(rgba));
Assert::IsTrue(Palette::IsGray(gray));
}
TEST_METHOD(PaletteColorDistanceTest)
{
// When there are no differences, 0
int noDistance = Palette::ColorDistance(Palette::RGB(255, 0, 0), Palette::RGB(255, 0, 0));
Assert::AreEqual(0, noDistance);
// Changing from one color to the next should result in a distance change
int hueDistance1 = Palette::ColorDistance(Palette::RGB(0, 255, 0), Palette::RGB(0, 0, 255));
Assert::AreNotEqual(0, hueDistance1);
int hueDistance2 = Palette::ColorDistance(Palette::RGB(255, 0, 0), Palette::RGB(0, 0, 255));
Assert::AreNotEqual(0, hueDistance2);
int hueDistance3 = Palette::ColorDistance(Palette::RGB(0, 0, 255), Palette::RGB(0, 255, 0));
Assert::AreNotEqual(0, hueDistance3);
// Ensure that evenly distributed changes have an effect
int brightnessDistance1 = Palette::ColorDistance(Palette::RGB(10, 20, 30), Palette::RGB(12, 22, 32));
Assert::AreNotEqual(0, brightnessDistance1);
// Technically this is grayscale, it is questionable if this should effect things.
// TODO: Research this. It seems to comply with CIE Delta-E results.
int brightnessDistance2 = Palette::ColorDistance(Palette::RGB(10, 10, 10), Palette::RGB(12, 12, 12));
Assert::AreNotEqual(0, brightnessDistance2);
// Verify the degree of change is great enough to be detected
int colorDistance1 = Palette::ColorDistance(Palette::RGB(255, 255, 0), Palette::RGB(255, 128, 0)); // Yellow to Orange
int colorDistance2 = Palette::ColorDistance(Palette::RGB(255, 255, 0), Palette::RGB(128, 255, 0)); // Yellow to Light Green
int colorDistance3 = Palette::ColorDistance(Palette::RGB(255, 128, 0), Palette::RGB(128, 255, 0)); // Orange to Light Green
// Orange to Green should obviously be farther that the difference between Yellow to Orange, and Yellow to Light Green.
Assert::IsTrue(colorDistance3 > colorDistance1 && colorDistance3 > colorDistance2);
// Even though equally distributed, Light Green is a lot closer to Yellow than Orange.
// This is consistant with CIE Delta-E calculations.
Assert::IsTrue(colorDistance1 > colorDistance2);
// Ensure that color comparison order does not matter
int left = Palette::ColorDistance(Palette::RGB(255, 128, 50), Palette::RGB(50, 128, 128));
int right = Palette::ColorDistance(Palette::RGB(50, 128, 128), Palette::RGB(255, 128, 50));
Assert::AreEqual(left, right);
}
};
}
......@@ -98,6 +98,8 @@
<ClCompile Include="AlgorithmTests.cpp" />
<ClCompile Include="BinarizationTests.cpp" />
<ClCompile Include="ContrastImageTests.cpp" />
<ClCompile Include="ConvolutionTests.cpp" />
<ClCompile Include="EdgeDetectorTests.cpp" />
<ClCompile Include="ImageTests.cpp" />
<ClCompile Include="ISauvolaTests.cpp" />
<ClCompile Include="LocalWindowTests.cpp" />
......
......@@ -14,6 +14,8 @@
<ClCompile Include="MorphologyTests.cpp" />
<ClCompile Include="ISauvolaTests.cpp" />
<ClCompile Include="ContrastImageTests.cpp" />
<ClCompile Include="ConvolutionTests.cpp" />
<ClCompile Include="EdgeDetectorTests.cpp" />
</ItemGroup>
<ItemGroup>
<Filter Include="Utilities">
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment