diff -ruN '--exclude=build*' a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt	2026-08-18 03:00:44.000000000 -0300
+++ b/CMakeLists.txt	2026-09-01 11:36:51.709113651 -0300
@@ -99,7 +99,7 @@
 endif()
 
 #### Set C++ standard level
-set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD 20)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 set(CMAKE_CXX_EXTENSIONS OFF)
 
diff -ruN '--exclude=build*' a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt	2026-08-18 03:00:44.000000000 -0300
+++ b/src/CMakeLists.txt	2026-09-01 11:38:19.261702052 -0300
@@ -553,7 +553,7 @@
 
 ################## OPENCV ###################
 if(ENABLE_OPENCV)
-  find_package(OpenCV 4)
+  find_package(OpenCV)
   if(NOT OpenCV_FOUND)
     set(ENABLE_OPENCV FALSE CACHE BOOL
       "Build with OpenCV algorithms (requires Protobuf 3)" FORCE)
@@ -605,6 +605,7 @@
       opencv_highgui
       opencv_dnn
       opencv_tracking
+    $<$<VERSION_GREATER_EQUAL:${OpenCV_VERSION},5>:opencv_geometry>
     )
     set(HAVE_OPENCV TRUE CACHE BOOL "Building with OpenCV effects" FORCE)
     mark_as_advanced(HAVE_OPENCV)
diff -ruN '--exclude=build*' a/src/CVObjectMask.h b/src/CVObjectMask.h
--- a/src/CVObjectMask.h	2026-08-18 03:00:44.000000000 -0300
+++ b/src/CVObjectMask.h	2026-09-01 11:38:19.259688212 -0300
@@ -17,6 +17,14 @@
 #include <opencv2/core.hpp>
 #include <opencv2/dnn.hpp>
 #include <opencv2/opencv.hpp>
+#include <opencv2/core/version.hpp>
+#if CV_VERSION_MAJOR >= 5
+// OpenCV 5 split calib3d: boundingRect/getRotationMatrix2D moved to
+// geometry/2d.hpp, estimateAffinePartial2D to geometry/3d.hpp, and
+// opencv.hpp no longer pulls them in.
+#include <opencv2/geometry/2d.hpp>
+#include <opencv2/geometry/3d.hpp>
+#endif
 #undef uint64
 #undef int64
 
diff -ruN '--exclude=build*' a/src/CVStabilization.h b/src/CVStabilization.h
--- a/src/CVStabilization.h	2026-08-18 03:00:44.000000000 -0300
+++ b/src/CVStabilization.h	2026-09-01 11:38:19.259660250 -0300
@@ -17,6 +17,14 @@
 #define int64 opencv_broken_int
 #define uint64 opencv_broken_uint
 #include <opencv2/opencv.hpp>
+#include <opencv2/core/version.hpp>
+#if CV_VERSION_MAJOR >= 5
+// OpenCV 5 split calib3d: boundingRect/getRotationMatrix2D moved to
+// geometry/2d.hpp, estimateAffinePartial2D to geometry/3d.hpp, and
+// opencv.hpp no longer pulls them in.
+#include <opencv2/geometry/2d.hpp>
+#include <opencv2/geometry/3d.hpp>
+#endif
 #include <opencv2/core.hpp>
 #undef uint64
 #undef int64
diff -ruN '--exclude=build*' a/src/effects/AnalogTape.cpp b/src/effects/AnalogTape.cpp
--- a/src/effects/AnalogTape.cpp	2026-08-18 03:00:44.000000000 -0300
+++ b/src/effects/AnalogTape.cpp	2026-09-01 11:37:42.135781496 -0300
@@ -43,7 +43,7 @@
 	info.has_audio = false;
 }
 
-static inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
+static inline float os_lerp(float a, float b, float t) { return a + (b - a) * t; }
 
 std::shared_ptr<Frame> AnalogTape::GetFrame(std::shared_ptr<Frame> frame,
 																						int64_t frame_number) {
@@ -137,7 +137,7 @@
 	const float k_stripe = stripe.GetValue(frame_number);
 	const float k_bands = staticBands.GetValue(frame_number);
 
-	int r_y = std::round(lerp(0.0f, 2.0f, k_soft) * inverse_scale_x);
+	int r_y = std::round(os_lerp(0.0f, 2.0f, k_soft) * inverse_scale_x);
 	if (k_noise > 0.6f)
 		r_y = std::min(r_y, 1);
 	if (r_y > 0) {
@@ -149,8 +149,8 @@
 		Y.swap(tmpY);
 	}
 
-	float shift = lerp(0.0f, 2.5f, k_bleed) * inverse_scale_x;
-	int r_c = std::round(lerp(0.0f, 3.0f, k_bleed) * inverse_scale_x);
+	float shift = os_lerp(0.0f, 2.5f, k_bleed) * inverse_scale_x;
+	int r_c = std::round(os_lerp(0.0f, 3.0f, k_bleed) * inverse_scale_x);
 	float sat = 1.0f - 0.30f * k_bleed;
 	float shift_h = shift * 0.5f;
 #ifdef _OPENMP
@@ -195,20 +195,20 @@
 	uint32_t SCHED_SEED = SEED ^ fnv1a_32(schedSalt, 0x9e3779b9u);
 	const float PI = 3.14159265358979323846f;
 
-	float sigmaY = lerp(0.0f, 0.08f, k_noise);
+	float sigmaY = os_lerp(0.0f, 0.08f, k_noise);
 	const float decay = 0.88f + 0.08f * k_noise;
 	const float amp = 0.18f * k_noise;
 	const float baseP = 0.0025f + 0.02f * k_noise;
 
-	float Hfixed = lerp(0.0f, 0.12f * h, k_stripe);
+	float Hfixed = os_lerp(0.0f, 0.12f * h, k_stripe);
 	float Gfixed = 0.10f * k_stripe;
 	float Nfixed = 1.0f + 1.5f * k_stripe;
 
 	float rate = 0.4f * k_bands;
-	int dur_frames = std::round(lerp(1.0f, 6.0f, k_bands));
-	float Hburst = lerp(0.06f * h, 0.25f * h, k_bands);
-	float Gburst = lerp(0.10f, 0.25f, k_bands);
-	float sat_band = lerp(0.8f, 0.5f, k_bands);
+	int dur_frames = std::round(os_lerp(1.0f, 6.0f, k_bands));
+	float Hburst = os_lerp(0.06f * h, 0.25f * h, k_bands);
+	float Gburst = os_lerp(0.10f, 0.25f, k_bands);
+	float sat_band = os_lerp(0.8f, 0.5f, k_bands);
 	float Nburst = 1.0f + 2.0f * k_bands;
 
 	struct Band { float center; double t0; };
@@ -314,10 +314,10 @@
 		}
 	}
 
-	float A = lerp(0.0f, 3.0f, k_track) * inverse_scale_x; // pixels
-	float f = lerp(0.25f, 1.2f, k_track); // Hz
-	float Hsk = lerp(0.0f, 0.10f * h, k_track); // pixels
-	float S = lerp(0.0f, 5.0f, k_track) * inverse_scale_x; // pixels
+	float A = os_lerp(0.0f, 3.0f, k_track) * inverse_scale_x; // pixels
+	float f = os_lerp(0.25f, 1.2f, k_track); // Hz
+	float Hsk = os_lerp(0.0f, 0.10f * h, k_track); // pixels
+	float S = os_lerp(0.0f, 5.0f, k_track) * inverse_scale_x; // pixels
 	float phase = 2 * PI * (f * t) + 0.7f * (SEED * 0.001f);
 	for (int y = 0; y < h; ++y) {
 		const float y_ref = static_cast<float>(y) * reference_scale_y;
diff -ruN '--exclude=build*' a/src/effects/DenoiseImage.cpp b/src/effects/DenoiseImage.cpp
--- a/src/effects/DenoiseImage.cpp	2026-08-18 03:00:44.000000000 -0300
+++ b/src/effects/DenoiseImage.cpp	2026-09-01 11:37:42.138602824 -0300
@@ -46,7 +46,7 @@
 	return t * t * (3.0f - (2.0f * t));
 }
 
-static inline float lerp(float a, float b, float amount) {
+static inline float os_lerp(float a, float b, float amount) {
 	return a + ((b - a) * amount);
 }
 
@@ -510,10 +510,10 @@
 				const float out_chroma_r = out_r - out_y;
 				const float out_chroma_g = out_g - out_y;
 				const float out_chroma_b = out_b - out_y;
-				out_y = lerp(out_y, prev_y, temporal_luma_mix);
-				out_r = out_y + lerp(out_chroma_r, prev_r - prev_y, temporal_chroma_mix);
-				out_g = out_y + lerp(out_chroma_g, prev_g - prev_y, temporal_chroma_mix);
-				out_b = out_y + lerp(out_chroma_b, prev_b - prev_y, temporal_chroma_mix);
+				out_y = os_lerp(out_y, prev_y, temporal_luma_mix);
+				out_r = out_y + os_lerp(out_chroma_r, prev_r - prev_y, temporal_chroma_mix);
+				out_g = out_y + os_lerp(out_chroma_g, prev_g - prev_y, temporal_chroma_mix);
+				out_b = out_y + os_lerp(out_chroma_b, prev_b - prev_y, temporal_chroma_mix);
 			}
 
 			uchar* output_pixel = output_pixels + (y * output_stride) + (x * 4);
diff -ruN '--exclude=build*' a/src/effects/FilmGrain.cpp b/src/effects/FilmGrain.cpp
--- a/src/effects/FilmGrain.cpp	2026-08-18 03:00:44.000000000 -0300
+++ b/src/effects/FilmGrain.cpp	2026-09-01 11:37:42.141134452 -0300
@@ -46,7 +46,7 @@
 	return static_cast<int>(value + 0.5f);
 }
 
-static float lerp(float a, float b, float t) {
+static float os_lerp(float a, float b, float t) {
 	return a + ((b - a) * t);
 }
 
@@ -135,11 +135,11 @@
 	const float evolution_value   = clamp01(static_cast<float>(evolution.GetValue(frame_number)));
 	const float coherence_value   = clamp01(static_cast<float>(coherence.GetValue(frame_number)));
 
-	const float cell_size       = lerp(1.0f, 9.0f, size_value);
+	const float cell_size       = os_lerp(1.0f, 9.0f, size_value);
 	const float fine_frequency  = 1.0f / cell_size;
-	const float soft_frequency  = fine_frequency * lerp(0.45f, 0.12f, softness_value);
-	const float clump_frequency = fine_frequency * lerp(0.35f, 0.08f, clump_value);
-	const float temporal_rate   = lerp(0.0f, 1.0f, evolution_value) * lerp(1.0f, 8.0f, 1.0f - coherence_value);
+	const float soft_frequency  = fine_frequency * os_lerp(0.45f, 0.12f, softness_value);
+	const float clump_frequency = fine_frequency * os_lerp(0.35f, 0.08f, clump_value);
+	const float temporal_rate   = os_lerp(0.0f, 1.0f, evolution_value) * os_lerp(1.0f, 8.0f, 1.0f - coherence_value);
 	const float temporal_position   = static_cast<float>(frame_number) * temporal_rate;
 	const int   time0               = static_cast<int>(std::floor(temporal_position));
 	const int   time1               = time0 + 1;
@@ -227,7 +227,7 @@
 		float fine_cache[2][4] = {};
 
 		// soft_col0/col1[time_idx][salt_idx]: value_noise y-axis columns pre-lerped.
-		// Per pixel we only need one lerp across x instead of 4 hash calls + 3 lerps.
+		// Per pixel we only need one os_lerp across x instead of 4 hash calls + 3 lerps.
 		float soft_col0[2][4] = {}, soft_col1[2][4] = {};
 
 		// clump_col0/col1[salt_idx]: clump only uses time0
@@ -269,7 +269,7 @@
 
 			// Soft noise: bilinear value_noise, but y-axis columns are pre-lerped into
 			// soft_col0/col1. Only 4 hashes needed when the x-cell changes (~every 7 px),
-			// then 1 lerp per pixel instead of 4 hashes + 3 lerps every pixel.
+			// then 1 os_lerp per pixel instead of 4 hashes + 3 lerps every pixel.
 			float soft_sx = 0.0f;
 			if (use_softness) {
 				const float srx    = reference_x * soft_frequency;
@@ -338,7 +338,7 @@
 				}
 				if (use_clump) {
 					const float cluster = clump_col0[s] + (clump_col1[s] - clump_col0[s]) * clump_sx;
-					grain *= lerp(1.0f, 0.45f + (std::abs(cluster) * 1.35f), clump_value);
+					grain *= os_lerp(1.0f, 0.45f + (std::abs(cluster) * 1.35f), clump_value);
 				}
 				grains[s] = grain;
 			}
diff -ruN '--exclude=build*' a/src/effects/Stabilizer.cpp b/src/effects/Stabilizer.cpp
--- a/src/effects/Stabilizer.cpp	2026-08-18 03:00:44.000000000 -0300
+++ b/src/effects/Stabilizer.cpp	2026-09-01 11:38:49.136249250 -0300
@@ -16,6 +16,10 @@
 #include <iostream>
 
 #include "effects/Stabilizer.h"
+#include <opencv2/core/version.hpp>
+#if CV_VERSION_MAJOR >= 5
+#include <opencv2/geometry/2d.hpp>
+#endif
 #include "Exceptions.h"
 #include "stabilizedata.pb.h"
 
