A cutting-edge C++23 application that demonstrates advanced Fourier transform concepts through interactive image analysis. This tool performs 2D Discrete Fourier Transform (DFT) decomposition on RGB images, allowing users to visualize how complex images can be reconstructed from sinusoidal frequency components.
Here's a quick demo of the app:
When you load an image into CppFourier, the application performs the following mathematical operations:
- Image Decomposition: Each RGB channel is independently transformed from the spatial domain to the frequency domain using a custom Cooley-Tukey FFT implementation
- Frequency Analysis: The complex-valued frequency coefficients are sorted by magnitude, identifying the most significant sinusoidal components
- Selective Reconstruction: Using the logarithmic slider, you control how many of the top frequency components are used to reconstruct the image
- Real-time Visualization: The application displays both the original image and the reconstructed version side-by-side, demonstrating how frequency-domain filtering affects image quality
The mathematical foundation relies on the principle that any 2D signal (image) can be represented as a sum of sinusoidal waves with different frequencies, phases, and amplitudes. By limiting the number of frequency components, you observe the fundamental concept behind image compression and frequency-domain filtering.
- RGB Image Processing: Load images from Resources folder with automatic format detection
- Custom FFT Implementation: High-performance Cooley-Tukey algorithm for 2D transforms
- Interactive Controls: Logarithmic frequency slider (1-50,000 frequencies) for real-time reconstruction
- Automatic Optimization: Smart image resizing (max 512x512) for optimal performance
- Dual Visualization: Side-by-side comparison of original and reconstructed images
- RGB Spectrum Analysis: Real-time frequency spectrum visualization for all color channels with transparency
- Animation Mode: Automatic frequency sweep with smooth cubic ease-in-out damping
- Welcome Screen: Startup popup introducing the application with custom branding
- Ranges and Views: Leverages
std::ranges
for expressive, functional-style code - Parallel Processing: Multi-core execution using
std::execution::par_unseq
with Intel TBB - Event-Driven Architecture: Observer pattern for efficient updates via custom event system
- Type Safety: Unified
Scalar
type system for consistent precision throughout - Memory Efficiency: Lazy evaluation with range views
- Cross-platform: Supports Linux, Windows, and macOS
This project showcases advanced C++23 features that dramatically improve code readability, performance, and maintainability. Below are key transformations demonstrating the power of modern C++.
Before (Traditional C++17):
// Old approach: manual loops and intermediate containers
std::vector<std::string> imageFiles;
for (const auto& entry : std::filesystem::directory_iterator(resourcesPath)) {
if (entry.is_regular_file()) {
std::string extension = entry.path().extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
for (const auto& supportedExt : supportedExtensions) {
if (extension == supportedExt) {
imageFiles.push_back(entry.path().string());
break;
}
}
}
}
std::sort(imageFiles.begin(), imageFiles.end());
After (Modern C++23):
// New approach: declarative pipeline with ranges
auto imageFiles = std::filesystem::directory_iterator(resourcesPath)
| std::views::filter([](const auto& entry) { return entry.is_regular_file(); })
| std::views::transform([](const auto& entry) { return entry.path(); })
| std::views::filter([&supportedExtensions](const auto& path) {
auto extension = path.extension().string();
std::ranges::transform(extension, extension.begin(), ::tolower);
return std::ranges::find(supportedExtensions, extension) != supportedExtensions.end();
})
| std::views::transform([](const auto& path) { return path.string(); })
| std::ranges::to<std::vector>();
std::ranges::sort(imageFiles);
Benefits:
- 50% fewer lines of code
- Zero intermediate allocations with lazy views
- Self-documenting functional pipeline
- Composable operations that can be easily modified
Before (Traditional C++17):
// Old approach: nested loops for 2D coordinate iteration
for (size_t y = 0; y < height; ++y) {
for (size_t x = 0; x < width; ++x) {
double fx = (x < width / 2) ? static_cast<double>(x) : static_cast<double>(x) - static_cast<double>(width);
double fy = (y < height / 2) ? static_cast<double>(y) : static_cast<double>(y) - static_cast<double>(height);
double freq = std::sqrt(fx * fx + fy * fy);
if ((low_pass && freq > frequency_cutoff) || (!low_pass && freq < frequency_cutoff)) {
result.at(x, y) = ComplexImage::Complex(0, 0);
}
}
}
After (Modern C++23):
// New approach: ranges with structured bindings and filters
auto coordinates = std::views::cartesian_product(
std::views::iota(0uz, height),
std::views::iota(0uz, width)
);
auto mask_coords = coordinates | std::views::filter([=](const auto& coord) {
auto [y, x] = coord;
double fx = (x < width / 2) ? double(x) : double(x - width);
double fy = (y < height / 2) ? double(y) : double(y - height);
double freq = std::sqrt(fx * fx + fy * fy);
return (low_pass && freq > frequency_cutoff) || (!low_pass && freq < frequency_cutoff);
});
std::ranges::for_each(mask_coords, [&result](const auto& coord) {
auto [y, x] = coord;
result.at(x, y) = ComplexImage::Complex(0, 0);
});
Benefits:
- Eliminated nested loops for better readability
- Structured bindings
auto [y, x] = coord
improve clarity - Reduced static_cast usage by 60%
- Separates filtering logic from iteration mechanics
Before (Traditional C++17):
// Old approach: sequential processing
RGBComplexImage result(input.getWidth(), input.getHeight());
for (int channel = 0; channel < 3; ++channel) {
ComplexImage transformed = transform2D(input.getChannel(channel), direction);
result.getChannel(channel) = transformed;
}
After (Modern C++23):
// New approach: parallel futures with ranges
auto channel_range = std::views::iota(0, 3);
std::vector<std::future<ComplexImage>> channel_futures;
// Launch parallel transforms for each RGB channel
std::ranges::transform(channel_range, std::back_inserter(channel_futures),
[&input, direction](int channel) {
return std::async(std::launch::async, [&input, direction, channel]() {
return FourierTransform{}.transform2D(input.getChannel(channel), direction);
});
});
// Collect results using zip view
for (auto [channel, future] : std::views::zip(channel_range, channel_futures)) {
result.getChannel(channel) = future.get();
}
Benefits:
- 3x performance improvement on multi-core systems
- Automatic load balancing across CPU cores
- Exception safety with RAII futures
- Elegant result collection with zip views
Before (Traditional C++17):
// Old approach: mixed float/double types causing casting noise
for (size_t y = 0; y < height; ++y) {
for (size_t x = 0; x < width; ++x) {
double fx = static_cast<double>(x);
double fy = static_cast<double>(y);
float scaleX = static_cast<float>(width) / static_cast<float>(freq_width);
float screenX = static_cast<float>(fx * scaleX);
screenX = std::clamp(screenX, 0.0f, static_cast<float>(width));
// Inconsistent float/double mixing throughout
}
}
After (Modern C++23):
// New approach: unified Scalar typedef with ranges
using Scalar = double; // Single line controls all floating-point precision
auto coordinates = std::views::cartesian_product(
std::views::iota(0uz, height),
std::views::iota(0uz, width)
);
Scalar fx = static_cast<Scalar>(x);
Scalar fy = static_cast<Scalar>(y);
Scalar scaleX = width / freq_width; // No casting needed
Scalar screenX = fx * scaleX; // Direct calculation
screenX = std::clamp(screenX, Scalar(0.0), width); // Type-consistent
Benefits:
- Unified type system: Single
Scalar
typedef controls all floating-point precision - Easy reconfiguration: Change
double
tofloat
in one line to switch precision globally - Eliminated type noise: Mathematical operations work with consistent types
- Reduced static_cast usage by 10% through better type flow
- Future-proof: Can easily switch to custom precision types (e.g.,
long double
, fixed-point)
Before (Traditional C++17):
// Old approach: positional initialization
VisualizationLine line;
line.x1 = width * 0.5f;
line.y1 = height * 0.5f;
line.x2 = screenX;
line.y2 = screenY;
line.magnitude = magnitude;
line.phase = phase;
line.frequency = std::sqrt(float(u * u + v * v));
After (Modern C++23):
// New approach: designated initializers
return VisualizationLine{
.x1 = width * 0.5f,
.y1 = height * 0.5f,
.x2 = screenX,
.y2 = screenY,
.magnitude = magnitude,
.phase = phase,
.frequency = std::sqrt(float(u * u + v * v))
};
Benefits:
- Self-documenting field assignments
- Compile-time validation of field names
- Reduced initialization errors
- Better IDE support with autocomplete
- C++23 compatible compiler (GCC 12+, Clang 15+, or MSVC 2022+)
- CMake 3.20 or higher
- OpenGL 3.3+
- PNG and JPEG libraries (for image format support)
- Intel TBB (Threading Building Blocks) for parallel execution
- FFTW3 library for Fourier transforms
sudo apt-get update
sudo apt-get install build-essential cmake git
sudo apt-get install libpng-dev libjpeg-dev
sudo apt-get install libfftw3-dev libtbb-dev
sudo apt-get install libgl1-mesa-dev libglu1-mesa-dev
sudo apt-get install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev
sudo dnf install gcc-c++ cmake git
sudo dnf install libpng-devel libjpeg-devel
sudo dnf install fftw3-devel tbb-devel
sudo dnf install mesa-libGL-devel mesa-libGLU-devel
sudo dnf install libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel
For WSL2 users, follow the Ubuntu/Debian instructions above. Make sure you have WSLg enabled for GUI support.
# Clone the repository
git clone https://github.com/cschladetsch/CppFourier.git
cd CppFourier
# Run the run script
./r
##### OR
# Create build directory
mkdir build && cd build
# Configure
cmake ..
# Build
make -j$(nproc)
# Run tests (optional)
ctest
./Bin/fourier_viewer
You can add your own images to analyze by placing them in the Resources/
folder. The application will automatically detect and list all supported image files in this directory.
Supported formats:
- JPEG (.jpg, .jpeg)
- PNG (.png)
- BMP (.bmp)
- TIFF (.tiff, .tif)
Simply copy your images to the Resources/
folder and restart the application. Your images will appear in the image selector within the application's control panel.
Included Test Images:
- RGB.jpg: Vertical RGB color blocks for testing horizontal frequency components
- RGB_Circles.jpg: Concentric RGB circles for radial frequency patterns
- RGB_Diagonal.jpg: 45-degree diagonal RGB stripes for angular frequency analysis
-
Welcome Screen
- Displays "Christian's Visual Thing" branding on startup
- Informs users that animation will start automatically
- Reminds users they can interact with the frequency slider at any time
-
Image Selection
- Radio buttons to select from available images in the Resources/ folder
- Supports PNG, JPEG, BMP, TIFF formats
- Auto-loads first available image on startup
-
Frequency Control
- Logarithmic slider to adjust frequency components (1 to max available)
- Maximum frequencies calculated as min(50,000, image_width × image_height ÷ 4)
- Real-time reconstruction updates
- Animate button for automatic frequency sweep (13 seconds per direction)
- Animation starts automatically by default
- Features smooth cubic ease-in-out damping at endpoints
- User interaction with slider automatically stops animation
-
Status Display
- Shows current image dimensions
- Displays total available frequencies
- Shows active frequency count and reconstruction quality percentage
- Animation progress and direction indicator
-
RGB Frequency Spectrum
- Real-time frequency magnitude visualization with semi-transparent window
- All three color channels (R, G, B) overlaid on same plot with smoothed curves
- Logarithmic scale for better dynamic range visibility
- Updates automatically with frequency changes via event system
- 80% opacity window for subtle transparency effect
-
Window Management
- All windows can be freely moved by dragging their title bars
- Windows can be resized by dragging their corners or edges
- Close individual windows using the X button (they can be reopened)
- Window positions and sizes are remembered between sessions
- Spectrum window features adjustable transparency
The application performs a 2D Discrete Fourier Transform (DFT) on RGB images using a custom Cooley-Tukey FFT implementation, processing each color channel separately. Images are converted from the spatial domain to the frequency domain, then reconstructed by selecting the top N frequency components based on magnitude.
By limiting the number of frequencies used in reconstruction, you can see how images can be approximated using fewer components, demonstrating the principle of frequency-based compression. Large images are automatically resized to 512×512 pixels for optimal performance.
- Types.hpp: Unified scalar type system with configurable precision (
using Scalar = double
) - EventSystem.hpp: Observer pattern implementation for loose coupling between components
- ImageLoader: Loads RGB images using CImg library with multi-format support
- ComplexImage: Represents grayscale images in complex number format with Scalar precision
- RGBComplexImage: Represents RGB images with separate complex channels
- FourierTransform: Performs forward and inverse FFT using custom Cooley-Tukey implementation
- FourierVisualizer: Manages frequency filtering and image reconstruction
- ImageProcessor: Parallel image processing with normalization and filtering
- Renderer: OpenGL-based rendering with event-driven texture updates
- UIManager: ImGui-based interface with spectrum visualization, animation, and transparency effects
- Parallel Execution: Multi-core processing using
std::execution::par_unseq
for:- Image downsampling across RGB channels
- Spectrum computation and normalization with moving average smoothing
- Frequency magnitude calculations
- Coordinate generation and transformations
- Event-Driven Updates: Components subscribe to events, updating only when needed
- Intel TBB Integration: Automatic work distribution across available CPU cores
- Zero-Copy Architecture: Shared pointers minimize data copying between components
The unified Scalar
type system allows easy switching between float and double precision. Benchmark results reveal interesting performance characteristics:
Metric | Float | Double | Difference |
---|---|---|---|
Average Time | 14.61 μs | 13.79 μs | Double is 5.6% faster |
Throughput | 70.08 M samples/s | 74.28 M samples/s | Double is 6% faster |
Memory Usage | 8 KB | 16 KB | Double uses 2x memory |
-
Counter-intuitive Performance: Double precision is actually faster than float for FFT operations
- Modern x86-64 CPUs are optimized for 64-bit operations
- SSE/AVX vector instructions work efficiently with doubles
- Better numerical stability reduces computational corrections
-
Memory Trade-off: Double uses exactly 2x the memory
- Critical for large datasets or memory-constrained systems
- Float advantage increases with dataset size due to cache effects
-
Precision Benefits: Double provides significantly better numerical accuracy
- Important for scientific computing and signal processing
- Reduces accumulation of rounding errors in iterative algorithms
To change precision throughout the entire codebase, simply edit one line in Include/Types.hpp
:
// For double precision (default - recommended for accuracy)
using Scalar = double;
// For float precision (memory-efficient)
using Scalar = float;
-
Use Double (default) for:
- Scientific computing requiring high accuracy
- Audio processing (better dynamic range)
- General-purpose image analysis
-
Use Float for:
- Real-time graphics applications
- Embedded systems with memory constraints
- Large-scale batch processing
The project includes comprehensive unit tests using Google Test framework:
cd build
ctest --verbose
This project is licensed under the MIT License - see the LICENSE file for details.
- Dear ImGui - Immediate mode GUI library
- CImg - C++ image processing library
- GLFW - OpenGL window management
- Google Test - Unit testing framework
Contributions are welcome! Please feel free to submit a Pull Request.
- C++23 Standard (ISO/IEC 14882:2023) - Official C++23 language specification
- cppreference.com C++23 - Comprehensive C++23 feature documentation
- std::ranges Library - Standard library ranges and views documentation
- std::views Documentation - Range adaptor objects and view types
- Cooley, J. W., & Tukey, J. W. (1965). "An algorithm for the machine calculation of complex Fourier series." Mathematics of Computation, 19(90), 297-301.
- Brigham, E. O. (1988). The Fast Fourier Transform and Its Applications. Prentice Hall.
- Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
- Press, W. H., Teukolsky, S. A., Vetterling, W. T., & Flannery, B. P. (2007). Numerical Recipes: The Art of Scientific Computing (3rd ed.). Cambridge University Press.
- Gonzalez, R. C., & Woods, R. E. (2017). Digital Image Processing (4th ed.). Pearson.
- Pratt, W. K. (2007). Digital Image Processing: PIKS Scientific Inside (4th ed.). Wiley-Interscience.
- Jähne, B. (2005). Digital Image Processing (6th ed.). Springer.
- OpenGL 3.3 Core Profile Specification - Official OpenGL specification
- Dear ImGui Documentation - Immediate mode GUI library
- GLFW Documentation - Cross-platform window and input handling
- Shreiner, D., Sellers, G., Kessenich, J., & Licea-Kane, B. (2013). OpenGL Programming Guide (8th ed.). Addison-Wesley.
- Rudin, W. (1987). Real and Complex Analysis (3rd ed.). McGraw-Hill.
- Stein, E. M., & Shakarchi, R. (2003). Fourier Analysis: An Introduction. Princeton University Press.
- Bracewell, R. N. (1999). The Fourier Transform and Its Applications (3rd ed.). McGraw-Hill.
- Meyers, S. (2014). Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14. O'Reilly Media.
- Stroustrup, B. (2013). The C++ Programming Language (4th ed.). Addison-Wesley.
- C++ Core Guidelines - Modern C++ best practices
- Awesome Modern C++ - Curated list of modern C++ resources