Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ option(FORCE_FETCH_DEPENDENCIES "Force using FetchContent for all dependencies"
# included, since the install directories are searched automatically by g++.

# Set files
SET(ncorr_src src/ncorr.cpp src/Strain2D.cpp src/Disp2D.cpp src/Data2D.cpp src/ROI2D.cpp src/Image2D.cpp src/Array2D.cpp src/session.cpp src/config.cpp)
SET(ncorr_h include/ncorr.h include/Strain2D.h include/Disp2D.h include/Data2D.h include/ROI2D.h include/Image2D.h include/Array2D.h include/ncorr/session.h include/ncorr/config.h include/ncorr/ini.h)
SET(ncorr_src src/ncorr.cpp src/Strain2D.cpp src/Disp2D.cpp src/Data2D.cpp src/ROI2D.cpp src/Image2D.cpp src/Array2D.cpp src/session.cpp src/config.cpp src/log.cpp)
SET(ncorr_h include/ncorr.h include/Strain2D.h include/Disp2D.h include/Data2D.h include/ROI2D.h include/Image2D.h include/Array2D.h include/ncorr/session.h include/ncorr/config.h include/ncorr/ini.h include/ncorr/log.h)

# Set include directory
INCLUDE_DIRECTORIES(include)
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@ cd examples/ohtcfrp

Check the video directory in the `examples/ohtcfrp` directory for the result video.

## Logging

CppNCorr uses a small, dependency-free leveled logger (`include/ncorr/log.h`)
instead of ad-hoc `std::cout`/`std::cerr`. Messages carry a severity — **TRACE,
DEBUG, INFO, WARN, ERROR** — and go to the console (INFO/DEBUG/TRACE to `stdout`,
WARN/ERROR to `stderr`) and, optionally, a full-detail log file. The stream-style
macros (`NLOG_INFO << ...`) short-circuit message construction when the level is
disabled, so they are cheap inside the per-frame / per-iteration DIC loops.

Because the library has no command line of its own, configuration is driven by
environment variables (applied lazily on first use) and by the engine's existing
`debug` flag:

```bash
export NCORR_LOG_LEVEL=info # trace|debug|info|warn|error|off (console threshold)
export NCORR_LOG_FILE=ncorr.log # also write a full debug-level log here (empty = none)
export NCORR_LOG_CONSOLE=0 # disable console output entirely
```

Setting `debug = true` in the DIC analysis input lowers the console threshold to
DEBUG so the engine's debug diagnostics appear. A parent application can also
configure the logger programmatically via `ncorr::log::set_level()`,
`ncorr::log::set_file()`, and `ncorr::log::Logger::instance()`. When consumed by
CPPxDIC, that project propagates its own logging settings here via the
`NCORR_LOG_*` environment variables so engine logs share the same verbosity and
destination.


8 changes: 4 additions & 4 deletions include/Array2D.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include <limits>
#include <string>

#include "ncorr/log.h"

// Non standard libraries
extern "C" { // Blas - for matrix multiplication
void dgemm_(char*, char*, int*, int*, int*, double*, double*, int*, double*, int*, double*, double*, int*);
Expand Down Expand Up @@ -2613,7 +2615,7 @@ namespace details {
pointer allocate(difference_type s, const void * = 0) {
pointer ptr = static_cast<pointer>(malloc_function(s * sizeof(value_type)));
if (!ptr) {
std::cerr << "Failed to allocate memory using allocate in fftw_allocator." << std::endl;
NLOG_ERROR << "Failed to allocate memory using allocate in fftw_allocator.";
throw std::bad_alloc();
}

Expand Down Expand Up @@ -3003,9 +3005,7 @@ typename Array2D<T,T_alloc>::pointer Array2D<T,T_alloc>::allocate(difference_typ
ptr_new = alloc.allocate(s_init);
} catch (std::bad_alloc &e) {
// Bad alloc doesn't have a what() message, so just add one to terminal
std::cerr << "---------------------------------------------" << std::endl
<< "Error: failed to allocate memory for Array2D." << std::endl
<< "---------------------------------------------" << std::endl;
NLOG_ERROR << "Error: failed to allocate memory for Array2D.";
throw;
}

Expand Down
3 changes: 2 additions & 1 deletion include/ROI2D.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <stack>

#include "Array2D.h"
#include "ncorr/log.h"


namespace ncorr {
Expand Down Expand Up @@ -442,7 +443,7 @@ T_container& fill(T_container &A, const Array2D<double>&boundary, const T &val)
// Check for empty boundary first - this is not an error, just nothing to fill
if (boundary.empty() || boundary.height() == 0) {
// If boundary is empty just return
std::cout << "Warning: The boundary is empty or the height is equal 0 - just nothing to fill" << std::endl;
NLOG_WARN << "Warning: The boundary is empty or the height is equal 0 - just nothing to fill";
return A;
}

Expand Down
164 changes: 164 additions & 0 deletions include/ncorr/log.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#pragma once
/**
* @file log.h
* @brief Lightweight, dependency-free logging facility for CppNCorr.
*
* CppNCorr is a static library consumed by other projects (e.g. CPPxDIC), so
* the logger is intentionally tiny and brings in no third-party dependency. It
* provides:
* - Five severity levels (Trace, Debug, Info, Warn, Error) plus Off.
* - Independent thresholds for the console and an optional log file, so a file
* sink can capture full Debug detail while the console stays quiet.
* - Thread-safe emission (an internal mutex), safe to call from OpenMP
* parallel regions used by the DIC engine.
* - Stream-style macros (NLOG_INFO << ...) that short-circuit message
* construction when the level is disabled — important for the per-frame /
* per-iteration hot loops in ncorr.cpp.
*
* Because the library has no command line of its own, configuration is driven by
* environment variables (applied lazily on first use) and by the engine's
* existing @c debug flag:
* - @c NCORR_LOG_LEVEL trace|debug|info|warn|error|off (console threshold)
* - @c NCORR_LOG_FILE path to a log file (full Debug detail is written)
* - @c NCORR_LOG_CONSOLE 0|1|true|false (disable/enable console output)
*
* A parent application can also configure the logger programmatically via the
* helpers below before invoking the engine.
*/

#include <fstream>
#include <mutex>
#include <ostream>
#include <sstream>
#include <string>

namespace ncorr {
namespace log {

/// Severity levels, ordered from most to least verbose.
enum class Level { Trace = 0, Debug = 1, Info = 2, Warn = 3, Error = 4, Off = 5 };

/// Parse a level name (case-insensitive): trace, debug, info, warn|warning,
/// error, off. Returns @p fallback if @p s is empty or unrecognized.
Level level_from_string(const std::string& s, Level fallback = Level::Info);

/// Short, fixed-width display name for a level (e.g. "INFO ").
const char* level_name(Level l);

/**
* @brief Process-wide singleton logger. All members are thread-safe.
*/
class Logger {
public:
static Logger& instance();

void set_console_level(Level l);
void set_file_level(Level l);
Level console_level() const;
Level file_level() const;

/// Open (or replace) the file sink. An empty path closes the file sink.
/// @return false if @p path could not be opened (file sink left closed).
bool set_file(const std::string& path);

/// Enable or disable console output entirely.
void set_console_enabled(bool on);

/// Include timestamp + source location in console output too (the file sink
/// always includes them). Off by default to keep the console readable.
void set_verbose_format(bool on);

/// Apply NCORR_LOG_LEVEL / NCORR_LOG_FILE / NCORR_LOG_CONSOLE. Runs once
/// automatically on first use; calling again re-applies them.
void configure_from_env();

/// True if a message at @p l would reach any sink. Use to guard expensive
/// message construction in hot loops.
bool enabled(Level l);

/// Emit a fully-formed message (trailing newlines are trimmed) at @p l.
void write(Level l, const char* file, int line, const std::string& msg);

private:
Logger();
void ensure_env();

mutable std::mutex mtx_;
Level console_level_ = Level::Info;
Level file_level_ = Level::Debug;
bool console_enabled_ = true;
bool verbose_format_ = false;
bool color_ = false;
bool env_done_ = false;
std::ofstream file_;
};

/// Set the console threshold (convenience wrapper).
void set_level(Level l);

/// Lower the console threshold to Debug when @p on is true (used to honour the
/// engine's existing @c debug flag). A no-op when @p on is false.
void set_debug(bool on);

/// Convenience: open a log file sink (full Debug detail).
bool set_file(const std::string& path);

/// True if a message at @p l would be emitted by any sink.
inline bool enabled(Level l) {
return Logger::instance().enabled(l);
}

/**
* @brief RAII stream builder; flushes its accumulated text to the logger when it
* is destroyed at the end of the full expression.
*/
class Stream {
public:
Stream(Level level, const char* file, int line) : level_(level), file_(file), line_(line) {}
~Stream() { Logger::instance().write(level_, file_, line_, oss_.str()); }

Stream(const Stream&) = delete;
Stream& operator=(const Stream&) = delete;

template <typename T>
Stream& operator<<(const T& v) {
oss_ << v;
return *this;
}
/// Support stream manipulators such as std::endl / std::setprecision.
Stream& operator<<(std::ostream& (*manip)(std::ostream&)) {
oss_ << manip;
return *this;
}

private:
Level level_;
const char* file_;
int line_;
std::ostringstream oss_;
};

/// Helper that turns the conditional logging expression back into a void
/// statement (glog idiom). @c operator& has lower precedence than @c << but
/// higher than @c ?:, so the macro is safe inside an unbraced if/else.
class Voidify {
public:
Voidify() = default;
void operator&(Stream&) {}
};

} // namespace log
} // namespace ncorr

// Stream-style logging macros. When the level is disabled, the right-hand side
// (message construction) is never evaluated.
#define NLOG_AT(lvl) \
!::ncorr::log::enabled(lvl) \
? (void)0 \
: ::ncorr::log::Voidify() & ::ncorr::log::Stream((lvl), __FILE__, __LINE__)

#define NLOG_TRACE NLOG_AT(::ncorr::log::Level::Trace)
#define NLOG_DEBUG NLOG_AT(::ncorr::log::Level::Debug)
#define NLOG_INFO NLOG_AT(::ncorr::log::Level::Info)
#define NLOG_WARN NLOG_AT(::ncorr::log::Level::Warn)
#define NLOG_ERROR NLOG_AT(::ncorr::log::Level::Error)
31 changes: 16 additions & 15 deletions src/Image2D.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

#include "Image2D.h"
#include "ncorr/log.h"
#include <filesystem>
#include <algorithm>
#include <numeric>
Expand Down Expand Up @@ -364,7 +365,7 @@ ImageProcessor::filter_like_ben(const std::vector<cv::Mat>& input,
// Compute boundaries from FIRST FILTERED image using percentiles (5th, 95th) if not provided
if (gs_boundaries == nullptr) {
boundaries = compute_percentile_boundaries(filtered_images[0], mask, 5.0, 95.0);
std::cout << " Computed filter boundaries: [" << boundaries.first << ", " << boundaries.second << "]" << std::endl;
NLOG_INFO << " Computed filter boundaries: [" << boundaries.first << ", " << boundaries.second << "]";
} else {
boundaries = *gs_boundaries;
}
Expand Down Expand Up @@ -431,7 +432,7 @@ std::pair<double, double> ImageProcessor::compute_percentile_boundaries(const cv
}

if (values.empty()) {
std::cerr << "Warning: No pixels in mask for percentile computation" << std::endl;
NLOG_WARN << "Warning: No pixels in mask for percentile computation";
return {0.0, 255.0};
}

Expand Down Expand Up @@ -570,7 +571,7 @@ std::vector<Image2D> VideoImporter::import_video(

cv::VideoCapture cap(video_path);
if (!cap.isOpened()) {
std::cerr << "Failed to open video file: " << video_path << std::endl;
NLOG_ERROR << "Failed to open video file: " << video_path;
return images;
}

Expand All @@ -579,17 +580,17 @@ std::vector<Image2D> VideoImporter::import_video(
int frame_end = (params.frame_end < 0) ? total_frames : std::min(params.frame_end, total_frames);
int frame_jump = std::max(1, params.frame_jump);

std::cout << "Importing video: " << video_path << std::endl;
std::cout << " Total frames: " << total_frames << std::endl;
std::cout << " Import range: " << frame_start << " to " << frame_end << " (step " << frame_jump << ")" << std::endl;
NLOG_INFO << "Importing video: " << video_path;
NLOG_INFO << " Total frames: " << total_frames;
NLOG_INFO << " Import range: " << frame_start << " to " << frame_end << " (step " << frame_jump << ")";

int imported_count = 0;
for (int f = frame_start; f <= frame_end; f += frame_jump) {
cap.set(cv::CAP_PROP_POS_FRAMES, f - 1); // 0-indexed

cv::Mat frame;
if (!cap.read(frame)) {
std::cerr << " Warning: Failed to read frame " << f << std::endl;
NLOG_WARN << " Warning: Failed to read frame " << f;
break;
}

Expand All @@ -610,7 +611,7 @@ std::vector<Image2D> VideoImporter::import_video(
}

cap.release();
std::cout << " Imported " << imported_count << " frames" << std::endl;
NLOG_INFO << " Imported " << imported_count << " frames";

return images;
}
Expand All @@ -629,7 +630,7 @@ std::vector<Image2D> VideoImporter::import_video_to_files(

cv::VideoCapture cap(video_path);
if (!cap.isOpened()) {
std::cerr << "Failed to open video file: " << video_path << std::endl;
NLOG_ERROR << "Failed to open video file: " << video_path;
return images;
}

Expand All @@ -638,18 +639,18 @@ std::vector<Image2D> VideoImporter::import_video_to_files(
int frame_end = (params.frame_end < 0) ? total_frames : std::min(params.frame_end, total_frames);
int frame_jump = std::max(1, params.frame_jump);

std::cout << "Importing video to files: " << video_path << std::endl;
std::cout << " Output directory: " << output_dir << std::endl;
std::cout << " Total frames: " << total_frames << std::endl;
std::cout << " Import range: " << frame_start << " to " << frame_end << " (step " << frame_jump << ")" << std::endl;
NLOG_INFO << "Importing video to files: " << video_path;
NLOG_INFO << " Output directory: " << output_dir;
NLOG_INFO << " Total frames: " << total_frames;
NLOG_INFO << " Import range: " << frame_start << " to " << frame_end << " (step " << frame_jump << ")";

int imported_count = 0;
for (int f = frame_start; f <= frame_end; f += frame_jump) {
cap.set(cv::CAP_PROP_POS_FRAMES, f - 1); // 0-indexed

cv::Mat frame;
if (!cap.read(frame)) {
std::cerr << " Warning: Failed to read frame " << f << std::endl;
NLOG_WARN << " Warning: Failed to read frame " << f;
break;
}

Expand All @@ -674,7 +675,7 @@ std::vector<Image2D> VideoImporter::import_video_to_files(
}

cap.release();
std::cout << " Imported and saved " << imported_count << " frames" << std::endl;
NLOG_INFO << " Imported and saved " << imported_count << " frames";

return images;
}
Expand Down
Loading
Loading