Skip to content

Commit f3d34df

Browse files
authored
Merge pull request #45 from cappelletto/develop
Merge v0.8.2 - Milestone complete Expand core capabilities by adding: * Grid-based feature density normalization * Configurable enhancement pipeline * Improved configuration loader
2 parents 0a787f7 + 3173a6e commit f3d34df

19 files changed

+480
-70
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
cmake_minimum_required(VERSION 3.18)
2-
project(videostrip VERSION 0.8.0 LANGUAGES CXX)
2+
project(videostrip VERSION 0.8.2 LANGUAGES CXX)
33

44
set(CMAKE_CXX_STANDARD 17)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)

configs/sample_config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ processing:
2121
enable_logging: true
2222
# overlap_mode: FEATURE # (optional, harmless for now)
2323

24+
feature_normalization: grid # none|grid
25+
grid_normalization:
26+
cell: [32, 32]
27+
max_per_cell: 50
28+
score: response # response|size
29+
2430
enhance:
2531
enable: true
2632
sequence:

tests/CMakeLists.txt

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,58 @@ target_link_libraries(test_core_smoke
3434
${OpenCV_LIBS}
3535
)
3636

37+
# =========================
38+
# test_enhance_yaml
39+
# =========================
40+
add_executable(test_enhance_yaml
41+
${CMAKE_CURRENT_SOURCE_DIR}/test_enhance_yaml.cpp
42+
)
43+
target_include_directories(test_enhance_yaml PRIVATE ${TEST_COMMON_INCLUDES})
44+
target_link_libraries(test_enhance_yaml
45+
PRIVATE
46+
videostrip_core
47+
Catch2::Catch2WithMain
48+
yaml-cpp
49+
)
50+
catch_discover_tests(test_enhance_yaml
51+
WORKING_DIRECTORY $<TARGET_FILE_DIR:test_enhance_yaml>
52+
)
53+
54+
# =========================
55+
# test_enhance_ops
56+
# =========================
57+
add_executable(test_enhance_ops
58+
${CMAKE_CURRENT_SOURCE_DIR}/test_enhance_ops.cpp
59+
)
60+
target_include_directories(test_enhance_ops PRIVATE ${TEST_COMMON_INCLUDES})
61+
target_link_libraries(test_enhance_ops
62+
PRIVATE
63+
videostrip_core
64+
Catch2::Catch2WithMain
65+
${OpenCV_LIBS}
66+
)
67+
catch_discover_tests(test_enhance_ops
68+
WORKING_DIRECTORY $<TARGET_FILE_DIR:test_enhance_ops>
69+
)
70+
71+
# =========================
72+
# test_enhance_stage
73+
# =========================
74+
add_executable(test_enhance_stage
75+
${CMAKE_CURRENT_SOURCE_DIR}/test_enhance_stage.cpp
76+
)
77+
target_include_directories(test_enhance_stage PRIVATE ${TEST_COMMON_INCLUDES})
78+
target_link_libraries(test_enhance_stage
79+
PRIVATE
80+
videostrip_core
81+
Catch2::Catch2WithMain
82+
${OpenCV_LIBS}
83+
)
84+
catch_discover_tests(test_enhance_stage
85+
WORKING_DIRECTORY $<TARGET_FILE_DIR:test_enhance_stage>
86+
)
87+
88+
3789
catch_discover_tests(test_core_smoke
3890
WORKING_DIRECTORY $<TARGET_FILE_DIR:test_core_smoke>
3991
)

tests/test_enhance_ops.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include <opencv2/core.hpp>
3+
// imwrite
4+
#include <opencv2/imgcodecs.hpp>
5+
#include <opencv2/imgproc.hpp>
6+
7+
#include <videostrip_core/enhance/image_enhancers.hpp>
8+
9+
using namespace videostrip;
10+
11+
static cv::Mat toy2x2() {
12+
cv::Mat m(2,2,CV_8UC3);
13+
m.at<cv::Vec3b>(0,0) = {10,20,30};
14+
m.at<cv::Vec3b>(0,1) = {40,50,60};
15+
m.at<cv::Vec3b>(1,0) = {70,80,90};
16+
m.at<cv::Vec3b>(1,1) = {100,110,120};
17+
return m;
18+
}
19+
20+
TEST_CASE("ContrastOffset exactness", "[enhance][ops]") {
21+
cv::Mat m = toy2x2();
22+
Enhancer e;
23+
e.setSequence({ {EnhanceType::ContrastOffset, ContrastOffsetParams{2.0, 10.0}} });
24+
REQUIRE(e.apply(m));
25+
auto expect = [&](int r,int c){
26+
auto v = m.at<cv::Vec3b>(r,c);
27+
auto in = toy2x2().at<cv::Vec3b>(r,c);
28+
for (int k=0;k<3;++k) {
29+
int ref = std::min(255, std::max(0, int(2*in[k] + 10)));
30+
CHECK(v[k] == ref);
31+
}
32+
};
33+
expect(0,0); expect(0,1); expect(1,0); expect(1,1);
34+
}
35+
36+
TEST_CASE("GrayWorld balances means", "[enhance][ops]") {
37+
cv::Mat m(32,32,CV_8UC3, cv::Scalar(10, 50, 200)); // B,G,R
38+
Enhancer e;
39+
e.setSequence({ {EnhanceType::GrayWorldWB, GrayWorldParams{}} });
40+
REQUIRE(e.apply(m));
41+
cv::Scalar mean = cv::mean(m);
42+
CHECK(std::abs(mean[0] - mean[1]) <= 1.5);
43+
CHECK(std::abs(mean[1] - mean[2]) <= 1.5);
44+
}
45+
46+
TEST_CASE("Gamma LUT monotonic and anchors", "[enhance][ops]") {
47+
cv::Mat m(1,4,CV_8UC3);
48+
m.at<cv::Vec3b>(0,0) = {0,0,0};
49+
m.at<cv::Vec3b>(0,1) = {64,64,64};
50+
m.at<cv::Vec3b>(0,2) = {128,128,128};
51+
m.at<cv::Vec3b>(0,3) = {255,255,255};
52+
Enhancer e;
53+
e.setSequence({ {EnhanceType::Gamma, GammaParams{2.0}} });
54+
REQUIRE(e.apply(m));
55+
CHECK(m.at<cv::Vec3b>(0,0) == cv::Vec3b(0,0,0));
56+
CHECK(m.at<cv::Vec3b>(0,3) == cv::Vec3b(255,255,255));
57+
CHECK(m.at<cv::Vec3b>(0,1)[0] < m.at<cv::Vec3b>(0,2)[0]); // monotonic
58+
CHECK(m.at<cv::Vec3b>(0,2)[0] > 128); // compressed mid-tones
59+
}
60+
61+
// TEST_CASE("CLAHE increases gray stddev", "[enhance][ops]") {
62+
// // if we use uniform input image clahe does nothing
63+
// // so we use a mid-gray image and check that CLAHE increases contrast
64+
// cv::Mat m(64,64,CV_8UC3, cv::Scalar(60,60,60)); // we operate on grayscale percentiles, assuming CLAHE will increase luminance range
65+
// // Then set a darker square in the middle (16x16)
66+
// for (int r=24; r<40; ++r) {
67+
// uchar* row = m.ptr<uchar>(r);
68+
// for (int c=24; c<40; ++c) {
69+
// row[3*c+0] = 30;
70+
// row[3*c+1] = 30;
71+
// row[3*c+2] = 30;
72+
// }
73+
// }
74+
// cv::imwrite("clahe_input.png", m);
75+
// // lambda with quick histogram estimation of percentiles
76+
// auto pctl = [](const cv::Mat& img, double p)->int {
77+
// cv::Mat g; cv::cvtColor(img, g, cv::COLOR_BGR2GRAY);
78+
// int hist[256] = {0};
79+
// for (int r=0; r<g.rows; ++r) {
80+
// const uchar* row = g.ptr<uchar>(r);
81+
// for (int c=0; c<g.cols; ++c) ++hist[row[c]];
82+
// }
83+
// const int N = g.rows * g.cols;
84+
// const int target = int(std::round(p * N));
85+
// int acc = 0;
86+
// for (int v=0; v<256; ++v) { acc += hist[v]; if (acc >= target) return v; }
87+
// return 255;
88+
// };
89+
90+
// int before_p10 = pctl(m, 0.10);
91+
// int before_p90 = pctl(m, 0.90);
92+
93+
// Enhancer e;
94+
// e.setSequence({ {EnhanceType::CLAHE, ClaheParams{2.0, {8,8}, ClaheSpace::YCrCb}} });
95+
// REQUIRE(e.apply(m));
96+
97+
// int after_p10 = pctl(m, 0.10);
98+
// int after_p90 = pctl(m, 0.90);
99+
100+
// cv::imwrite("clahe_output.png", m);
101+
// // CLAHE should increase dynamic range in luminance percentiles
102+
// CHECK((after_p90 - after_p10) > (before_p90 - before_p10));
103+
// }

tests/test_enhance_stage.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include <opencv2/core.hpp>
3+
#include <opencv2/imgproc.hpp>
4+
5+
#include <videostrip_core/enhance/pipeline_enhance.hpp>
6+
#include <videostrip_core/enhance/enhance_yaml.hpp>
7+
8+
using namespace videostrip;
9+
10+
TEST_CASE("EnhanceStage: converts to 8UC3 and applies sequence", "[enhance][stage]") {
11+
// Start with 16U gray; stage should convert and then process
12+
cv::Mat src16(32,32,CV_16UC1);
13+
for (int r=0;r<src16.rows;++r)
14+
for (int c=0;c<src16.cols;++c)
15+
src16.at<uint16_t>(r,c) = uint16_t((r*32 + c) % 1024);
16+
17+
cv::Mat frame = src16; // deliberately non-8UC3 input
18+
19+
EnhanceConfig cfg;
20+
cfg.enable = true;
21+
cfg.sequence = {
22+
{ EnhanceType::GrayWorldWB, GrayWorldParams{} },
23+
{ EnhanceType::Gamma, GammaParams{1.2} }
24+
};
25+
26+
EnhanceStage stage;
27+
std::string err;
28+
REQUIRE(stage.configure(cfg, err));
29+
REQUIRE(stage.process(frame));
30+
CHECK(frame.type() == CV_8UC3);
31+
CHECK(frame.channels() == 3);
32+
}

tests/test_enhance_yaml.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include <yaml-cpp/yaml.h>
3+
#include <string>
4+
5+
#include <videostrip_core/enhance/enhance_yaml.hpp>
6+
7+
using namespace videostrip;
8+
9+
TEST_CASE("enhance yaml: valid sequence parses", "[enhance][yaml]") {
10+
const char* y = R"YAML(
11+
enhance:
12+
enable: true
13+
sequence:
14+
- type: contrast
15+
alpha: 1.1
16+
beta: -5
17+
- type: grayworld
18+
- type: gamma
19+
value: 1.05
20+
- type: clahe
21+
clip_limit: 2.0
22+
tile_grid: [8, 8]
23+
space: YCrCb
24+
)YAML";
25+
YAML::Node root = YAML::Load(y);
26+
std::string err;
27+
auto cfg = parseEnhanceConfig(root, err);
28+
REQUIRE(cfg.has_value());
29+
CHECK(cfg->enable == true);
30+
REQUIRE(cfg->sequence.size() == 4);
31+
CHECK(cfg->sequence[0].type == EnhanceType::ContrastOffset);
32+
CHECK(cfg->sequence[1].type == EnhanceType::GrayWorldWB);
33+
CHECK(cfg->sequence[2].type == EnhanceType::Gamma);
34+
CHECK(cfg->sequence[3].type == EnhanceType::CLAHE);
35+
}
36+
37+
TEST_CASE("enhance yaml: malformed op fails", "[enhance][yaml]") {
38+
const char* y = R"YAML(
39+
enhance:
40+
enable: true
41+
sequence:
42+
- type: i_do_not_exist
43+
)YAML";
44+
YAML::Node root = YAML::Load(y);
45+
std::string err;
46+
auto cfg = parseEnhanceConfig(root, err);
47+
CHECK_FALSE(cfg.has_value());
48+
CHECK(err.find("Unknown") != std::string::npos);
49+
}

videostrip_cli/config_loader.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#include <yaml-cpp/yaml.h>
55

66
#include <videostrip_cli/config_loader.hpp>
7-
#include <videostrip_core/enhance_yaml.hpp>
7+
#include <videostrip_core/enhance/enhance_yaml.hpp>
88

99
namespace fs = std::filesystem;
1010

@@ -89,6 +89,38 @@ bool load_yaml_config(const std::string& yaml_path,
8989
// e.g., out.overlap_mode = v.as<std::string>();
9090
// Ignored if not present in struct.
9191
}
92+
93+
// NEW: feature normalization, see #23
94+
// -----------------------------
95+
using videostrip::FeatureNormalizationMode;
96+
using videostrip::GridNormalizationParams;
97+
// mode: none|grid
98+
if (auto v = n["feature_normalization"]; v && v.IsScalar()) {
99+
std::string s = v.as<std::string>();
100+
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
101+
out.feature_normalization.mode =
102+
(s == "grid") ? FeatureNormalizationMode::Grid
103+
: FeatureNormalizationMode::None;
104+
}
105+
// grid_normalization: { cell:[w,h], max_per_cell:int, score:response|size }
106+
if (auto gn = n["grid_normalization"]; gn && gn.IsMap()) {
107+
if (auto cell = gn["cell"]; cell && cell.IsSequence() && cell.size() == 2) {
108+
out.feature_normalization.grid.cell_w = std::max(1, cell[0].as<int>(32));
109+
out.feature_normalization.grid.cell_h = std::max(1, cell[1].as<int>(32));
110+
}
111+
if (auto mpc = gn["max_per_cell"]; mpc && mpc.IsScalar()) {
112+
out.feature_normalization.grid.max_per_cell = std::max(1, mpc.as<int>(50));
113+
}
114+
if (auto sc = gn["score"]; sc && sc.IsScalar()) {
115+
std::string ss = sc.as<std::string>();
116+
std::transform(ss.begin(), ss.end(), ss.begin(), ::tolower);
117+
out.feature_normalization.grid.score =
118+
(ss == "size")
119+
? GridNormalizationParams::Score::Size
120+
: GridNormalizationParams::Score::Response; // default
121+
}
122+
}
123+
92124
}
93125

94126
// Normalize outputs (relative -> base_dir)
@@ -193,6 +225,13 @@ void merge_yaml_into(ExtractorConfig& dst, const ExtractorConfig& y)
193225
dst.enable_logging = y.enable_logging;
194226
dst.enhance = y.enhance;
195227

228+
// check if there is a normalization mode set in the yaml config
229+
// if so, copy the entire normalization config
230+
// if not, leave dst as-is (probably None)
231+
232+
dst.feature_normalization.mode = y.feature_normalization.mode;
233+
dst.feature_normalization.grid = y.feature_normalization.grid;
234+
196235
// Optional future field:
197236
// if (!y.overlap_mode.empty()) dst.overlap_mode = y.overlap_mode;
198237
}

videostrip_core/CMakeLists.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ message(STATUS "*******************")
55

66
add_library(videostrip_core STATIC
77
videostrip_core.cpp
8-
image_enhancers.cpp
9-
enhance_yaml.cpp
10-
pipeline_enhance.cpp
8+
enhance/image_enhancers.cpp
9+
enhance/enhance_yaml.cpp
10+
enhance/pipeline_enhance.cpp
1111
feature/feature_extractor.cpp
1212
keyframe/keyframe_selector.cpp
1313
io/metadata_writer.cpp
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#include <algorithm>
2-
#include <videostrip_core/enhance_yaml.hpp>
2+
#include <videostrip_core/enhance/enhance_yaml.hpp>
33

44
namespace videostrip {
55

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#include <optional>
44
#include <string>
55

6-
#include <videostrip_core/image_enhancers.hpp>
6+
#include <videostrip_core/enhance/image_enhancers.hpp>
77

88
namespace videostrip {
99

0 commit comments

Comments
 (0)