Compare commits

...

16 Commits

8 changed files with 446 additions and 1 deletions

19
.gitignore vendored
View File

@@ -27,8 +27,27 @@
*.a
*.lib
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
out/
build/
# Executables
*.exe
*.out
*.app
.vscode/*

72
CMakeLists.txt Normal file
View File

@@ -0,0 +1,72 @@
cmake_minimum_required(VERSION 3.18)
project(TV_Denoising_CUDA)
set(CMAKE_BUILD_TYPE "Release")
list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR})
# Find the Qt5 package
find_package(Qt5 COMPONENTS Widgets REQUIRED)
# Find OpenCV
if(NOT DEFINED OpenCV_DIR)
set(OpenCV_DIR "C:/Users/Culis/.conan2/p/b/openc720995090ff52/b/")
# message(FATAL_ERROR "OpenCV_DIR is set to ${OpenCV_DIR}. Please set it to the appropriate directory.")
endif()
find_package(OpenCV REQUIRED)
if(NOT OpenCV_FOUND)
message(FATAL_ERROR "OpenCV package not found. Please make sure OpenCV is installed and OpenCV_DIR is set correctly.")
endif()
find_package(OpenCV REQUIRED)
# Find CUDA
enable_language(CUDA)
# Set CUDA flags and properties
set(CUDA_SEPARABLE_COMPILATION ON)
set(CUDA_PROPAGATE_HOST_FLAGS OFF)
# Add the CUDA source files
file(GLOB CUDA_SOURCE_FILES "src/*.cu")
set_source_files_properties(${CUDA_SOURCE_FILES} PROPERTIES CUDA_SOURCE_PROPERTY_FORMAT OBJ)
# Add the C++ source files
file(GLOB CPP_SOURCE_FILES "src/*.cpp")
# Set the include directories
include_directories(${OpenCV_DIR} ${CUDA_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS} "include")
# Create the executable
add_executable(TV_Denoising_CUDA ${CPP_SOURCE_FILES} ${CUDA_SOURCE_FILES})
# Link CUDA libraries
target_link_libraries(TV_Denoising_CUDA ${CUDA_LIBRARIES} ${OpenCV_LIBS} Qt5::Widgets)
set_property(TARGET TV_Denoising_CUDA PROPERTY ENVIRONMENT "PATH=${CMAKE_CURRENT_BINARY_DIR}/Debug;${CMAKE_CURRENT_BINARY_DIR}/Release")
if(WIN32)
set_target_properties(TV_Denoising_CUDA PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" WIN32_EXECUTABLE TRUE)
endif()
# Copy necessary DLLs and runtime files to the release directory
install(
DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Release/"
FILES_MATCHING PATTERN "*.dll"
)
install(
TARGETS TV_Denoising_CUDA
RUNTIME DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/"
LIBRARY DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/"
ARCHIVE DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/"
)
install(
DIRECTORY "${OpenCV_INSTALL_PATH}/"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/"
FILES_MATCHING PATTERN "*.dll"
)

9
CMakeUserPresets.json Normal file
View File

@@ -0,0 +1,9 @@
{
"version": 4,
"vendor": {
"conan": {}
},
"include": [
"E:\\programming\\C++\\tv-image-denoising\\build\\CMakePresets.json"
]
}

View File

@@ -1,3 +1,31 @@
# tv-image-denoising
Using the TV algorithm to denoise images with c++ and cuda.
Using the TV algorithm to denoise images with c++ and cuda.
## Requirements
g++
vcpkg
cmake
gtk
opencv2
cuda
## Build
```bash
# Generate build files
cmake -B build .
# Build
cmake --build build
```
## Run
```bash
./TV_Denoising_CUDA <input_image> <output_image> <lambda> <iterations>
```
## Original Exercise:
7. Image denoising (up to 50+10+20=80%+)
- [x] Use and implement a total variation image denoising method for the GPU. The input should be a noisy image, processed on the GPU by solving the energy minimization problem, then the output image should be displayed or saved to the disk. (50%)
- [ ] You can gain extra points for providing a simple GUI that allows the user to tune the parameters of the denoising algorithm, and immediately display the results! (+10%)
- [ ] Additional extra points can be achieved by also submitting a comparative evaluation of yours and other denoising techniques, applied to various inputs. I.e., use metrics such as SSIM, NCC, MAD, etc. to compare results to ground truth images. (+20%)
(Plus points: The more techniques you compare or the better quality that your figures,charts & diagrams are, you may be rewarded by further extra points.)

11
conanfile.txt Normal file
View File

@@ -0,0 +1,11 @@
[requires]
opencv/4.5.5
qt/5.15.8
[options]
qt/5.15.8:qttools=True
qt/5.15.8:shared=True
[generators]
CMakeDeps
CMakeToolchain

12
include/tv_denoising.hpp Normal file
View File

@@ -0,0 +1,12 @@
/*
Author: Vargha Csongor Csaba
Created: 2023-06-25 10:23:33
*/
#ifndef TV_DENOISING_H
#define TV_DENOISING_H
#include <opencv2/opencv.hpp>
extern "C" void TVDenoising(cv::Mat& image, float lambda, int maxIterations);
#endif // TV_DENOISING_H

217
src/main.cpp Normal file
View File

@@ -0,0 +1,217 @@
/*
Author: Vargha Csongor Csaba
Created: 2023-06-25 10:10:13
Description:
This file contains the main function for the TV image denoising cli tool.
It reads an image file, denoises it using the TV denoising algorithm,
and saves the denoised image to a file.
You can run it with the following command:
./TV_Denoising_CUDA <input_image> <output_image> <lambda> <iterations>
where:
- <input_image> is the path to the input image file you want to denoise.
- <output_image> is the path to the output denoised image file.
- <lambda> is the regularization parameter for TV denoising (optional, default: 0.02).
- <iterations> is the number of iterations for TV denoising (optional, default: 10).
*/
#include <iostream>
#include <QApplication>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QDebug>
#include <QLineEdit>
#include <QPushButton>
#include <QSlider>
#include <QFileDialog>
#include <QMessageBox>
#include <QImageReader>
#include <QImage>
#include <opencv2/opencv.hpp>
#include "tv_denoising.hpp"
// Global variables for GUI elements
QLabel *originalImageLabel;
QLabel *resultImageLabel;
QLineEdit *inputImagePathEdit;
QLineEdit *outputImagePathEdit;
QSlider *lambdaSlider;
QSlider *iterationsSlider;
void updateDenoisedImage()
{
std::string inputImagePath = inputImagePathEdit->text().toStdString();
std::string outputImagePath = outputImagePathEdit->text().toStdString();
float lambda = lambdaSlider->value() / 100.0;
int iterations = iterationsSlider->value();
// Read the input image
cv::Mat image = cv::imread(inputImagePath, cv::IMREAD_GRAYSCALE);
// Check if the image was successfully loaded
if (image.empty())
{
QMessageBox::critical(nullptr, "Error", "Failed to read the input image.");
return;
}
// Perform TV denoising
TVDenoising(image, lambda, iterations);
// Display the denoised image
QImage resultImage(image.data, image.cols, image.rows, QImage::Format_Grayscale8);
resultImageLabel->setPixmap(QPixmap::fromImage(resultImage));
// Save the denoised image
cv::imwrite(outputImagePath, image);
}
void openInputImage()
{
QString filePath = QFileDialog::getOpenFileName(nullptr, "Select Input Image");
if (!filePath.isEmpty())
inputImagePathEdit->setText(filePath);
}
void openOutputImage()
{
QString filePath = QFileDialog::getSaveFileName(nullptr, "Select Output Image");
if (!filePath.isEmpty())
outputImagePathEdit->setText(filePath);
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
// Create the main window
QWidget window;
window.setWindowTitle("TV Image Denoising");
window.resize(800, 600);
// Create the layout
QGridLayout *layout = new QGridLayout(&window);
// Create the original image label
QLabel *originalImageLabel = new QLabel("Original Image");
layout->addWidget(originalImageLabel, 0, 0, 1, 2); // Row 0, spanning 1 row, 2 columns
// Create the original image display widget
QLabel *originalImageDisplay = new QLabel();
originalImageDisplay->setAlignment(Qt::AlignCenter);
layout->addWidget(originalImageDisplay, 1, 0, 1, 2); // Row 1, spanning 1 row, 2 columns
// Create the result image label
QLabel *resultImageLabel = new QLabel("Result Image");
layout->addWidget(resultImageLabel, 0, 2, 1, 2); // Row 0, spanning 1 row, 2 columns
// Create the result image display widget
QLabel *resultImageDisplay = new QLabel();
resultImageDisplay->setAlignment(Qt::AlignCenter);
layout->addWidget(resultImageDisplay, 1, 2, 1, 2); // Row 1, spanning 1 row, 2 columns
// Create the input image path input field
QHBoxLayout *inputLayout = new QHBoxLayout;
QLabel *inputLabel = new QLabel("Input Image:");
QLineEdit *inputImagePathEdit = new QLineEdit;
QPushButton *inputBrowseButton = new QPushButton("Browse");
QObject::connect(inputBrowseButton, &QPushButton::clicked, openInputImage);
inputLayout->addWidget(inputLabel);
inputLayout->addWidget(inputImagePathEdit);
inputLayout->addWidget(inputBrowseButton);
layout->addLayout(inputLayout, 2, 0, 1, 4); // Row 2, spanning 1 row, 4 columns
// Create the output image path input field
QHBoxLayout *outputLayout = new QHBoxLayout;
QLabel *outputLabel = new QLabel("Output Image:");
QLineEdit *outputImagePathEdit = new QLineEdit;
QPushButton *outputBrowseButton = new QPushButton("Browse");
QObject::connect(outputBrowseButton, &QPushButton::clicked, openOutputImage);
outputLayout->addWidget(outputLabel);
outputLayout->addWidget(outputImagePathEdit);
outputLayout->addWidget(outputBrowseButton);
layout->addLayout(outputLayout, 3, 0, 1, 4); // Row 3, spanning 1 row, 4 columns
// Create the lambda slider
QLabel *lambdaLabel = new QLabel("Lambda:");
QSlider *lambdaSlider = new QSlider(Qt::Horizontal);
lambdaSlider->setObjectName("LambdaSlider"); // Set object name for styling
lambdaSlider->setMinimum(0);
lambdaSlider->setMaximum(100);
// Create the iterations slider
QLabel *iterationsLabel = new QLabel("Iterations:");
QSlider *iterationsSlider = new QSlider(Qt::Horizontal);
iterationsSlider->setObjectName("IterationsSlider"); // Set object name for styling
iterationsSlider->setMinimum(1);
iterationsSlider->setMaximum(100);
// Create labels to display slider values
QLabel *lambdaValueLabel = new QLabel();
QLabel *iterationsValueLabel = new QLabel();
// Set stylesheet for dark design
QString styleSheet = "QLabel { color: white; }" // Set label text color to white
"QSlider::groove:horizontal { background-color: #555555; height: 6px; }" // Set groove color and height
"QSlider::handle:horizontal { background-color: #ffffff; width: 10px; margin: -6px 0; }"; // Set handle color and size
lambdaSlider->setStyleSheet(styleSheet);
iterationsSlider->setStyleSheet(styleSheet);
// Set size policies for sliders
lambdaSlider->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
iterationsSlider->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
// Create the denoise button
QPushButton *denoiseButton = new QPushButton("Denoise");
QObject::connect(denoiseButton, &QPushButton::clicked, updateDenoisedImage);
// Add the lambda slider and its label
layout->addWidget(lambdaLabel, 4, 0); // Row 4, column 0
layout->addWidget(lambdaSlider, 4, 1); // Row 4, column 1
layout->addWidget(lambdaValueLabel, 5, 0); // Row 5, column 0
// Add the iterations slider and its label
layout->addWidget(iterationsLabel, 4, 2); // Row 4, column 2
layout->addWidget(iterationsSlider, 4, 3); // Row 4, column 3
layout->addWidget(iterationsValueLabel, 5, 2); // Row 5, column 2
// Add the denoise button
layout->addWidget(denoiseButton, 6, 0, 1, 4); // Row 6, spanning 1 row, 4 columns
QList<QByteArray> supportedFormats = QImageReader::supportedImageFormats();
qDebug() << "Supported image formats:" << supportedFormats;
// Load and display the input image
QString inputImagePath = inputImagePathEdit->text();
QImage inputImage(inputImagePath);
if (inputImage.isNull())
{
qDebug() << "Failed to load input image";
}
else
{
qDebug() << "Input image loaded successfully";
}
QPixmap inputPixmap = QPixmap::fromImage(inputImage);
originalImageDisplay->setPixmap(inputPixmap.scaled(originalImageDisplay->size(), Qt::KeepAspectRatio));
// Load and display the output image
QString outputImagePath = outputImagePathEdit->text();
QImage outputImage(outputImagePath);
if (outputImage.isNull())
{
qDebug() << "Failed to load input image";
}
else
{
qDebug() << "Input image loaded successfully";
}
QPixmap outputPixmap = QPixmap::fromImage(outputImage);
resultImageDisplay->setPixmap(outputPixmap.scaled(resultImageDisplay->size(), Qt::KeepAspectRatio));
// Show the main window
window.show();
return app.exec();
}

77
src/tv_denoising.cu Normal file
View File

@@ -0,0 +1,77 @@
#include <cuda.h>
#include <cuda_runtime.h>
#include <device_launch_parameters.h>
#include <opencv2/opencv.hpp>
#define BLOCK_SIZE_X 16
#define BLOCK_SIZE_Y 16
__global__ void tvDenoisingKernel(float* image, int width, int height, float lambda, int maxIterations)
{
// Calculate the global thread index
int col = blockIdx.x * blockDim.x + threadIdx.x;
int row = blockIdx.y * blockDim.y + threadIdx.y;
int index = row * width + col;
// Declare shared memory arrays
__shared__ float gradientX[BLOCK_SIZE_X][BLOCK_SIZE_Y];
__shared__ float gradientY[BLOCK_SIZE_X][BLOCK_SIZE_Y];
__shared__ float updatedImage[BLOCK_SIZE_X][BLOCK_SIZE_Y];
// Perform TV denoising iteratively
for (int iteration = 0; iteration < maxIterations; ++iteration)
{
// Calculate the gradients using central differences
gradientX[threadIdx.x][threadIdx.y] = image[index + 1] - image[index - 1];
gradientY[threadIdx.x][threadIdx.y] = image[index + width] - image[index - width];
// Synchronize threads to ensure all gradient calculations are complete
__syncthreads();
// Apply TV denoising update rule
updatedImage[threadIdx.x][threadIdx.y] = image[index] + lambda * (
gradientX[threadIdx.x][threadIdx.y] - gradientX[threadIdx.x - 1][threadIdx.y] +
gradientY[threadIdx.x][threadIdx.y] - gradientY[threadIdx.x][threadIdx.y - 1]
);
// Update the global image array with the updated pixel values
image[index] = updatedImage[threadIdx.x][threadIdx.y];
// Synchronize threads to ensure all image updates are complete
__syncthreads();
}
}
extern "C" void TVDenoising(cv::Mat& image, float lambda, int maxIterations)
{
// Convert the image to float precision
cv::Mat floatImage;
image.convertTo(floatImage, CV_32F);
// Get image dimensions
int width = image.cols;
int height = image.rows;
// Calculate the number of blocks and threads per block
dim3 blockSize(16, 16);
dim3 gridSize((width + blockSize.x - 1) / blockSize.x, (height + blockSize.y - 1) / blockSize.y);
// Allocate GPU memory for the image
float* d_image;
cudaMalloc(&d_image, width * height * sizeof(float));
// Copy the image data from host to device
cudaMemcpy(d_image, floatImage.ptr<float>(0), width * height * sizeof(float), cudaMemcpyHostToDevice);
// Invoke the TV denoising kernel
tvDenoisingKernel<<<gridSize, blockSize>>>(d_image, width, height, lambda, maxIterations);
// Copy the denoised image data back from device to host
cudaMemcpy(floatImage.ptr<float>(0), d_image, width * height * sizeof(float), cudaMemcpyDeviceToHost);
// Convert the denoised image back to the original data type
floatImage.convertTo(image, image.type());
// Free the GPU memory
cudaFree(d_image);
}