Skip to content

Commit d1b7caf

Browse files
committed
1D plotting in C++ post
1 parent 9f6052f commit d1b7caf

File tree

5 files changed

+170
-0
lines changed

5 files changed

+170
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
title: 1D visualization
3+
tags: [programming, C++]
4+
style: fill
5+
color: danger
6+
description: Easy visualizing 1D signals in C++
7+
---
8+
9+
# Introduction
10+
11+
Today, I'd like to share and document what I consider the best library for quickly plotting 1D signals in C++.
12+
13+
Compared to other alternatives like QWT or Qt's own libraries, the header-only library matplotlibcpp is extremely simple, free of errors, and as transparent to use as its Python counterpart.
14+
15+
# Matplotlibcpp
16+
17+
Available on [GitHub](https://github.com/lava/matplotlib-cpp), this plotting library mirrors the plotting API used by Matlab and matplotlib. Let's give it a try in Qt.
18+
19+
Specify directories where the compiler should look for header files during compilation:
20+
```pro
21+
INCLUDEPATH += /opt/matplotlib-cpp
22+
INCLUDEPATH += /usr/include/python3.10
23+
```
24+
Specify additional libraries to link against:
25+
```pro
26+
LIBS += -lpython3.10
27+
```
28+
29+
Now, let's move to our ```main.cpp```. First, include our encapsulated library:
30+
```cpp
31+
#include <matplotlibcpp.h>
32+
```
33+
34+
For convenience, use the plt namespace, just like in Python:
35+
```cpp
36+
namespace plt = matplotlibcpp;
37+
```
38+
39+
Create a structure to make the code modular and more understandable:
40+
```cpp
41+
struct PlotData {
42+
std::vector<double> u;
43+
std::vector<double> f;
44+
std::mutex mtx;
45+
double timestep_s = 0.001;
46+
bool update_plot = false;
47+
};
48+
```
49+
50+
Since we don't want our visualization process to interfere with other tasks in the main thread, let's create a function that will run in a secondary thread:.
51+
```cpp
52+
void plotThread(PlotData& plot_data) {
53+
plt::figure_size(1080, 720);
54+
while (true)
55+
{
56+
std::lock_guard<std::mutex> lock(plot_data.mtx);
57+
if (plot_data.update_plot)
58+
{
59+
std::vector<double> x_samples(plot_data.u.size());
60+
for (int i = 0; i < (int)plot_data.u.size(); ++i)
61+
x_samples[i] = static_cast<double>(i);
62+
plt::clf();
63+
plt::plot(x_samples, plot_data.f, "r-");
64+
plt::plot(x_samples, plot_data.u, "b-");
65+
plt::title("TV denoising evolution");
66+
plt::xlabel("Punto");
67+
plt::ylabel("Valor");
68+
plt::legend();
69+
plt::draw();
70+
plt::pause(plot_data.timestep_s);
71+
plot_data.update_plot = false;
72+
}
73+
}
74+
}
75+
```
76+
77+
Later, in any function or context where we want to display the figure, we can simply use multithreading libraries like ```std::thread``` to plot our data:
78+
```cpp
79+
size_t N = u.size();
80+
PlotData plot_data;
81+
plot_data.f = f;
82+
plot_data.timestep_s = 0.001;
83+
84+
std::thread plot_thread;
85+
if (showResults)
86+
plot_thread = std::thread(plotThread, std::ref(plot_data));
87+
88+
```
89+
90+
To test the library, we'll synthesize a 1D signal and filter it to show the results before and after. For curiosity's sake, we'll use a moving average filter, but we won't detail how it works in this post.
91+
92+
We'll need a one-dimensional grid for the abscissas and another for the ordinates:
93+
```cpp
94+
std::vector<double> xValues(noisy_signal.size());
95+
for (size_t i = 0; i < xValues.size(); ++i)
96+
xValues[i] = static_cast<double>(i);
97+
plt::figure_size(1080, 720);
98+
plt::plot(xValues, noisy_signal, "r-");
99+
plt::plot(xValues, smooth_signal, "b-");
100+
plt::title("Comparison: Original Profile (red) vs Filtered (blue) " + std::to_string(profileNumber));
101+
plt::xlabel("Point");
102+
plt::ylabel("Value");
103+
plt::legend();
104+
plt::show();
105+
```
106+
107+
Our noisy signal looks like this:
108+
```cpp
109+
for (int i = 0; i < profile.cols; ++i)
110+
noisy_signal.push_back(profile.at<double>(0, i));
111+
112+
std::vector<double> xValues(noisy_signal.size());
113+
for (size_t i = 0; i < xValues.size(); ++i)
114+
xValues[i] = static_cast<double>(i);
115+
plt::figure_size(1080, 720);
116+
plt::plot(xValues, noisy_signal, "r-");
117+
plt::title("Original profile");
118+
plt::xlabel("Point");
119+
plt::ylabel("Value");
120+
plt::legend();
121+
plt::show();
122+
```
123+
124+
<img src="../assets/blog_images/2025-01-30-cpp-1d-visualization/original.png" alt="Original signal" width="800" height="600" style="display: block; margin-left: auto; margin-right: auto;">
125+
126+
<p>The library even integrates buttons for zooming, detrending, or translating the graph!</p>
127+
128+
<img src="../assets/blog_images/2025-01-30-cpp-1d-visualization/modified.png" alt="Modified signal" width="800" height="600" style="display: block; margin-left: auto; margin-right: auto;">
129+
130+
<p>After applying our filter, we may want to graph both on the same scale to compare them. We can do it simply and easily:</p>
131+
132+
<img src="../assets/blog_images/2025-01-30-cpp-1d-visualization/comp.png" alt="Comparison of signals" width="800" height="600" style="display: block; margin-left: auto; margin-right: auto;">
133+
134+
Of course, by moving the functions to other threads, we can see the evolution of the filtering within the processing loop:
135+
```cpp
136+
void filterSignal(std::vector<double>& u, const std::vector<double>& f, int iterations, bool showResults) {
137+
size_t N = u.size();
138+
PlotData plot_data;
139+
plot_data.f = f;
140+
plot_data.timestep_s = 0.001;
141+
142+
std::thread plot_thread;
143+
if (showResults)
144+
plot_thread = std::thread(plotThread, std::ref(plot_data));
145+
146+
for (int iter = 0; iter < iterations; ++iter) {
147+
std::vector<double> u_new(N);
148+
149+
u_new = processMySignal();
150+
151+
u = u_new;
152+
153+
if (showResults)
154+
{
155+
std::lock_guard<std::mutex> lock(plot_data.mtx);
156+
plot_data.u = u;
157+
plot_data.update_plot = true;
158+
std::this_thread::sleep_for(std::chrono::milliseconds((int)(plot_data.timestep_s * 1000) * 2)); // Applying Nyquist Theorem, sending data at double the display rate
159+
}
160+
}
161+
162+
if (showResults)
163+
plot_thread.join();
164+
}
165+
```
166+
167+
The resolution is very high, and the widget's response is dynamic. There's no risk of the application freezing, as per my experiments with it.
168+
169+
<img src="../assets/blog_images/2025-01-30-cpp-1d-visualization/comp2.png" alt="Comparison of signals after filtering" width="800" height="600" style="display: block; margin-left: auto; margin-right: auto;">
170+
42.9 KB
Loading
87.5 KB
Loading
49.4 KB
Loading
35.6 KB
Loading

0 commit comments

Comments
 (0)