Skip to content

Commit

Permalink
[Render/CannyFilterRenderProcess] Added a Canny filter/edge detector …
Browse files Browse the repository at this point in the history
…proc.
  • Loading branch information
Razakhel committed Aug 29, 2024
1 parent 6c05159 commit 40a0e4b
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 6 deletions.
1 change: 1 addition & 0 deletions include/RaZ/RaZ.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
#include "Render/BloomRenderProcess.hpp"
#include "Render/BoxBlurRenderProcess.hpp"
#include "Render/Camera.hpp"
#include "Render/CannyFilterRenderProcess.hpp"
#include "Render/ChromaticAberrationRenderProcess.hpp"
#include "Render/ConvolutionRenderProcess.hpp"
#include "Render/Cubemap.hpp"
Expand Down
32 changes: 32 additions & 0 deletions include/RaZ/Render/CannyFilterRenderProcess.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#pragma once

#ifndef RAZ_CANNYFILTERRENDERPROCESS_HPP
#define RAZ_CANNYFILTERRENDERPROCESS_HPP

#include "RaZ/Render/MonoPassRenderProcess.hpp"

namespace Raz {

/// [Canny filter/edge detector](https://en.wikipedia.org/wiki/Canny_edge_detector) render process.
/// Detects the edges within an image given its pixels' gradient information.
class CannyFilterRenderProcess final : public MonoPassRenderProcess {
public:
explicit CannyFilterRenderProcess(RenderGraph& renderGraph);

void resizeBuffers(unsigned int width, unsigned int height) override;
/// Sets the given gradient buffer as input.
/// \param gradientBuffer Buffer containing the gradient values. Obtained from another filter such as Sobel.
/// \see SobelFilterRenderProcess
void setInputGradientBuffer(Texture2DPtr gradientBuffer);
/// Sets the given gradient direction buffer as input.
/// \param gradDirBuffer Buffer containing the gradient direction values. Obtained from another filter such as Sobel.
/// \see SobelFilterRenderProcess
void setInputGradientDirectionBuffer(Texture2DPtr gradDirBuffer);
void setOutputBuffer(Texture2DPtr binaryBuffer);
void setLowerBound(float lowerBound) const;
void setUpperBound(float upperBound) const;
};

} // namespace Raz

#endif // RAZ_CANNYFILTERRENDERPROCESS_HPP
12 changes: 6 additions & 6 deletions include/RaZ/Render/SobelFilterRenderProcess.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@

namespace Raz {

/// [Sobel filter](https://en.wikipedia.org/wiki/Sobel_operator) render process.
/// [Sobel filter/operator](https://en.wikipedia.org/wiki/Sobel_operator) render process.
class SobelFilterRenderProcess final : public MonoPassRenderProcess {
public:
explicit SobelFilterRenderProcess(RenderGraph& renderGraph);

void resizeBuffers(unsigned int width, unsigned int height) override;
void setInputBuffer(Texture2DPtr colorBuffer);
/// Sets the output buffer which will contain the gradient's value.
/// \param gradientBuffer Gradient's values buffer.
/// Sets the output buffer which will contain the gradient values.
/// \param gradientBuffer Gradient buffer.
void setOutputGradientBuffer(Texture2DPtr gradientBuffer);
/// Sets the output buffer which will contain the gradient's direction.
/// Sets the output buffer which will contain the gradient direction values.
///
/// /--0.75--\
/// / \
Expand All @@ -27,10 +27,10 @@ class SobelFilterRenderProcess final : public MonoPassRenderProcess {
/// \ /
/// \--0.25--/
///
/// \note The direction's values are just like those of [atan2](https://en.wikipedia.org/wiki/Atan2) (see image below), but remapped between [0; 1].
/// \note The direction values are just like those of [atan2](https://en.wikipedia.org/wiki/Atan2) (see image below), but remapped between [0; 1].
/// \imageSize{https://upload.cppreference.com/mwiki/images/9/91/math-atan2.png, height: 20%; width: 20%;}
/// \image html https://upload.cppreference.com/mwiki/images/9/91/math-atan2.png
/// \param gradDirBuffer Gradient's direction buffer.
/// \param gradDirBuffer Gradient direction buffer.
void setOutputGradientDirectionBuffer(Texture2DPtr gradDirBuffer);
};

Expand Down
77 changes: 77 additions & 0 deletions shaders/canny_filter.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
in vec2 fragTexcoords;

uniform sampler2D uniGradients;
uniform sampler2D uniGradDirs;
uniform vec2 uniInvBufferSize;
uniform float uniLowerBound;
uniform float uniUpperBound;

layout(location = 0) out vec4 fragColor;

void main() {
vec3 midGrad = texture(uniGradients, fragTexcoords).rgb;
vec3 gradDir = texture(uniGradDirs, fragTexcoords).rgb;

vec3 rightGrad = texture(uniGradients, fragTexcoords + vec2( 1.0, 0.0) * uniInvBufferSize).rgb;
vec3 leftGrad = texture(uniGradients, fragTexcoords + vec2(-1.0, 0.0) * uniInvBufferSize).rgb;
vec3 downGrad = texture(uniGradients, fragTexcoords + vec2( 0.0, -1.0) * uniInvBufferSize).rgb;
vec3 upGrad = texture(uniGradients, fragTexcoords + vec2( 0.0, 1.0) * uniInvBufferSize).rgb;
vec3 lowerRightGrad = texture(uniGradients, fragTexcoords + vec2( 1.0, -1.0) * uniInvBufferSize).rgb;
vec3 upperLeftGrad = texture(uniGradients, fragTexcoords + vec2(-1.0, 1.0) * uniInvBufferSize).rgb;
vec3 lowerLeftGrad = texture(uniGradients, fragTexcoords + vec2(-1.0, -1.0) * uniInvBufferSize).rgb;
vec3 upperRightGrad = texture(uniGradients, fragTexcoords + vec2( 1.0, 1.0) * uniInvBufferSize).rgb;

for (int i = 0; i < 3; ++i) {
// Merging the two directions' halves together; we want to check opposite directions each time, and both will be combined that way
//
// /--0.75--\
// / \
// / \
// 0.5 0/1
// \ /
// \ /
// \--0.25--/
//
// 0.6 (upper-left) will become 0.1 (lower-right), 0.75 (up) will become 0.25 (down), etc
if (gradDir[i] > 0.5)
gradDir[i] -= 0.5;

// Gradient magnitude thresholding (edge thinning)

if (gradDir[i] <= 0.0625 || gradDir[i] > 0.4375) { // Right or left
if (midGrad[i] < rightGrad[i] || midGrad[i] < leftGrad[i])
midGrad[i] = 0.0;
} else if (gradDir[i] > 0.1875 && gradDir[i] <= 0.3125) { // Down or up
if (midGrad[i] < downGrad[i] || midGrad[i] < upGrad[i])
midGrad[i] = 0.0;
} else if (gradDir[i] > 0.0625 && gradDir[i] <= 0.1875) { // Lower-right or upper-left
if (midGrad[i] < lowerRightGrad[i] || midGrad[i] < upperLeftGrad[i])
midGrad[i] = 0.0;
} else if (gradDir[i] > 0.3125 && gradDir[i] <= 0.4375) { // Lower-left or upper-right
if (midGrad[i] < lowerLeftGrad[i] || midGrad[i] < upperRightGrad[i])
midGrad[i] = 0.0;
}

// Double thresholding + hysteresis

if (midGrad[i] <= uniLowerBound) {
midGrad[i] = 0.0;
} else if (midGrad[i] >= uniUpperBound) {
midGrad[i] = 1.0;
} else {
// If the pixel is on a weak edge (between bounds), refining it with hysteresis: if any pixel around the current one is on a strong edge
// (has a gradient value above the upper bound), consider it as part of the edge
if (rightGrad[i] >= uniUpperBound || leftGrad[i] >= uniUpperBound
|| downGrad[i] >= uniUpperBound || upGrad[i] >= uniUpperBound
|| lowerRightGrad[i] >= uniUpperBound || upperLeftGrad[i] >= uniUpperBound
|| lowerLeftGrad[i] >= uniUpperBound || upperRightGrad[i] >= uniUpperBound) {
midGrad[i] = 1.0;
} else {
midGrad[i] = 0.0;
}
}
}

float colorVal = max(midGrad.x, max(midGrad.y, midGrad.z));
fragColor = vec4(vec3(colorVal), 1.0);
}
53 changes: 53 additions & 0 deletions src/RaZ/Render/CannyFilterRenderProcess.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include "RaZ/Render/CannyFilterRenderProcess.hpp"
#include "RaZ/Render/RenderPass.hpp"

#include <string_view>

namespace Raz {

namespace {

constexpr std::string_view cannySource = {
#include "canny_filter.frag.embed"
};

} // namespace

CannyFilterRenderProcess::CannyFilterRenderProcess(RenderGraph& renderGraph)
: MonoPassRenderProcess(renderGraph, FragmentShader::loadFromSource(cannySource), "Canny filter") {
setLowerBound(0.1f);
setUpperBound(0.3f);
}

void CannyFilterRenderProcess::resizeBuffers(unsigned int width, unsigned int height) {
const Vec2f invBufferSize(1.f / static_cast<float>(width), 1.f / static_cast<float>(height));
m_pass.getProgram().setAttribute(invBufferSize, "uniInvBufferSize");
m_pass.getProgram().sendAttributes();
}

void CannyFilterRenderProcess::setInputGradientBuffer(Texture2DPtr gradientBuffer) {
assert("Error: The input gradient buffer is invalid." && gradientBuffer != nullptr);

resizeBuffers(gradientBuffer->getWidth(), gradientBuffer->getHeight());
MonoPassRenderProcess::setInputBuffer(std::move(gradientBuffer), "uniGradients");
}

void CannyFilterRenderProcess::setInputGradientDirectionBuffer(Texture2DPtr gradDirBuffer) {
MonoPassRenderProcess::setInputBuffer(std::move(gradDirBuffer), "uniGradDirs");
}

void CannyFilterRenderProcess::setOutputBuffer(Texture2DPtr binaryBuffer) {
MonoPassRenderProcess::setOutputBuffer(std::move(binaryBuffer), 0);
}

void CannyFilterRenderProcess::setLowerBound(float lowerBound) const {
m_pass.getProgram().setAttribute(lowerBound, "uniLowerBound");
m_pass.getProgram().sendAttributes();
}

void CannyFilterRenderProcess::setUpperBound(float upperBound) const {
m_pass.getProgram().setAttribute(upperBound, "uniUpperBound");
m_pass.getProgram().sendAttributes();
}

} // namespace Raz
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions tests/src/RaZ/Render/RenderProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "RaZ/Data/ImageFormat.hpp"
#include "RaZ/Math/Transform.hpp"
#include "RaZ/Render/Camera.hpp"
#include "RaZ/Render/CannyFilterRenderProcess.hpp"
#include "RaZ/Render/ChromaticAberrationRenderProcess.hpp"
#include "RaZ/Render/ConvolutionRenderProcess.hpp"
#include "RaZ/Render/FilmGrainRenderProcess.hpp"
Expand Down Expand Up @@ -31,6 +32,45 @@ Raz::Image recoverImage(const Raz::Texture2DPtr& outputTexture, const Raz::FileP

} // namespace

TEST_CASE("CannyFilterRenderProcess execution", "[render]") {
Raz::World world;

const Raz::Window& window = TestUtils::getWindow();

auto& render = world.addSystem<Raz::RenderSystem>(window.getWidth(), window.getHeight());

// RenderSystem::update() needs a Camera with a Transform component
world.addEntityWithComponents<Raz::Camera, Raz::Transform>();

const Raz::Image gradientImg = Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_sobel_grad.png", true);
REQUIRE(gradientImg.getWidth() == window.getWidth());
REQUIRE(gradientImg.getHeight() == window.getHeight());
const Raz::Image gradDirImg = Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_sobel_grad_dir.png", true);
REQUIRE(gradDirImg.getWidth() == window.getWidth());
REQUIRE(gradDirImg.getHeight() == window.getHeight());

Raz::Texture2DPtr gradientInput = Raz::Texture2D::create(gradientImg);
Raz::Texture2DPtr gradDirInput = Raz::Texture2D::create(gradDirImg);
const Raz::Texture2DPtr output = Raz::Texture2D::create(window.getWidth(), window.getHeight(), Raz::TextureColorspace::GRAY, Raz::TextureDataType::BYTE);

auto& canny = render.getRenderGraph().addRenderProcess<Raz::CannyFilterRenderProcess>();
canny.addParent(render.getGeometryPass());
canny.setInputGradientBuffer(std::move(gradientInput));
canny.setInputGradientDirectionBuffer(std::move(gradDirInput));
canny.setOutputBuffer(output);

world.update({});
CHECK_THAT(recoverImage(output),
IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_canny_default.png", true)));

canny.setLowerBound(0.f);
canny.setUpperBound(0.1f);

world.update({});
CHECK_THAT(recoverImage(output),
IsNearlyEqualToImage(Raz::ImageFormat::load(RAZ_TESTS_ROOT "assets/renders/cook-torrance_ball_canny_detailed.png", true)));
}

TEST_CASE("ChromaticAberrationRenderProcess execution", "[render]") {
Raz::World world;

Expand Down

0 comments on commit 40a0e4b

Please sign in to comment.