From 88b7f8181eeef2ada36b623617e05b3d7810d97d Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 20 May 2025 16:02:23 -0500 Subject: [PATCH 1/6] Updating git ignore rules, adding new 360 spherical property to profiles, and new unit tests for Profiles. --- .gitignore | 1 + src/Profiles.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++---- src/Profiles.h | 9 ++++---- tests/Profiles.cpp | 53 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index b89ccf59..eed73b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ cmake-build-debug/* tags *~ +**/.claude/* diff --git a/src/Profiles.cpp b/src/Profiles.cpp index c462ffc8..b80b467c 100644 --- a/src/Profiles.cpp +++ b/src/Profiles.cpp @@ -30,6 +30,7 @@ Profile::Profile() { info.display_ratio.num = 0; info.display_ratio.den = 0; info.interlaced_frame = false; + info.spherical = false; // Default to non-spherical (regular) video } // @brief Constructor for Profile. @@ -38,8 +39,19 @@ Profile::Profile(std::string path) { bool read_file = false; - // Call default constructor - Profile(); + // Initialize all values to defaults (same as default constructor) + info.description = ""; + info.height = 0; + info.width = 0; + info.pixel_format = 0; + info.fps.num = 0; + info.fps.den = 0; + info.pixel_ratio.num = 0; + info.pixel_ratio.den = 0; + info.display_ratio.num = 0; + info.display_ratio.den = 0; + info.interlaced_frame = false; + info.spherical = false; // Default to non-spherical (regular) video try { @@ -104,6 +116,10 @@ Profile::Profile(std::string path) { std::stringstream(value) >> value_int; info.pixel_format = value_int; } + else if (setting == "spherical") { + std::stringstream(value) >> value_int; + info.spherical = (bool)value_int; + } } read_file = true; inputFile.close(); @@ -156,6 +172,12 @@ std::string Profile::Key() { output << std::setfill('0') << std::setw(4) << fps_string << std::setfill('\0') << "_"; output << std::setfill('0') << std::setw(2) << info.display_ratio.num << std::setfill('\0') << "-"; output << std::setfill('0') << std::setw(2) << info.display_ratio.den << std::setfill('\0'); + + // Add spherical indicator + if (info.spherical) { + output << "_360"; + } + return output.str(); } @@ -168,6 +190,12 @@ std::string Profile::ShortName() { } std::string fps_string = formattedFPS(true); output << info.width << "x" << info.height << progressive_str << fps_string; + + // Add 360° indicator for spherical videos + if (info.spherical) { + output << " 360°"; + } + return output.str(); } @@ -181,6 +209,12 @@ std::string Profile::LongName() { std::string fps_string = formattedFPS(true); output << info.width << "x" << info.height << progressive_str << " @ " << fps_string << " fps (" << info.display_ratio.num << ":" << info.display_ratio.den << ")"; + + // Add 360° indicator for spherical videos + if (info.spherical) { + output << " 360°"; + } + return output.str(); } @@ -193,7 +227,14 @@ std::string Profile::LongNameWithDesc() { } std::string fps_string = formattedFPS(true); output << info.width << "x" << info.height << progressive_str << " @ " << fps_string - << " fps (" << info.display_ratio.num << ":" << info.display_ratio.den << ") " << info.description; + << " fps (" << info.display_ratio.num << ":" << info.display_ratio.den << ")"; + + // Add 360° indicator for spherical videos + if (info.spherical) { + output << " 360°"; + } + + output << " " << info.description; return output.str(); } @@ -214,7 +255,8 @@ void Profile::Save(const std::string& file_path) const { file << "sample_aspect_den=" << info.pixel_ratio.den << "\n"; file << "display_aspect_num=" << info.display_ratio.num << "\n"; file << "display_aspect_den=" << info.display_ratio.den << "\n"; - file << "pixel_format=" << info.pixel_format; + file << "pixel_format=" << info.pixel_format << "\n"; + file << "spherical=" << info.spherical; file.close(); } @@ -245,6 +287,7 @@ Json::Value Profile::JsonValue() const { root["display_ratio"]["num"] = info.display_ratio.num; root["display_ratio"]["den"] = info.display_ratio.den; root["progressive"] = !info.interlaced_frame; + root["spherical"] = info.spherical; // return JsonValue return root; @@ -294,5 +337,7 @@ void Profile::SetJsonValue(const Json::Value root) { } if (!root["progressive"].isNull()) info.interlaced_frame = !root["progressive"].asBool(); + if (!root["spherical"].isNull()) + info.spherical = root["spherical"].asBool(); } diff --git a/src/Profiles.h b/src/Profiles.h index 1181431d..5782441a 100644 --- a/src/Profiles.h +++ b/src/Profiles.h @@ -45,7 +45,8 @@ namespace openshot Fraction fps; ///< Frames per second, as a fraction (i.e. 24/1 = 24 fps) Fraction pixel_ratio; ///< The pixel ratio of the video stream as a fraction (i.e. some pixels are not square) Fraction display_ratio; ///< The ratio of width to height of the video stream (i.e. 640x480 has a ratio of 4/3) - bool interlaced_frame; // Are the contents of this frame interlaced + bool interlaced_frame; ///< Are the contents of this frame interlaced + bool spherical; ///< Is this video a spherical/360° video }; /** @@ -127,8 +128,8 @@ namespace openshot /// Equality operator (compare profile objects) friend bool operator==(const Profile& l, const Profile& r) { - return std::tie(l.info.width, l.info.height, l.info.fps.num, l.info.fps.den, l.info.display_ratio.num, l.info.display_ratio.den, l.info.interlaced_frame) - == std::tie(r.info.width, r.info.height, r.info.fps.num, r.info.fps.den, r.info.display_ratio.num, r.info.display_ratio.den, r.info.interlaced_frame); + return std::tie(l.info.width, l.info.height, l.info.fps.num, l.info.fps.den, l.info.display_ratio.num, l.info.display_ratio.den, l.info.interlaced_frame, l.info.spherical) + == std::tie(r.info.width, r.info.height, r.info.fps.num, r.info.fps.den, r.info.display_ratio.num, r.info.display_ratio.den, r.info.interlaced_frame, r.info.spherical); } public: @@ -160,4 +161,4 @@ namespace openshot } -#endif +#endif \ No newline at end of file diff --git a/tests/Profiles.cpp b/tests/Profiles.cpp index b9f41a4a..d515655c 100644 --- a/tests/Profiles.cpp +++ b/tests/Profiles.cpp @@ -30,6 +30,7 @@ TEST_CASE( "empty constructor", "[libopenshot][profile]" ) CHECK(p1.info.pixel_ratio.num == 0); CHECK(p1.info.pixel_ratio.den == 0); CHECK(p1.info.interlaced_frame == false); + CHECK(p1.info.spherical == false); } @@ -51,6 +52,7 @@ TEST_CASE( "constructor with example profiles", "[libopenshot][profile]" ) CHECK(p1.info.pixel_ratio.num == 1); CHECK(p1.info.pixel_ratio.den == 1); CHECK(p1.info.interlaced_frame == false); + CHECK(p1.info.spherical == false); // Export to JSON openshot::Profile p1_json = openshot::Profile(); @@ -66,6 +68,7 @@ TEST_CASE( "constructor with example profiles", "[libopenshot][profile]" ) CHECK(p1_json.info.pixel_ratio.num == 1); CHECK(p1_json.info.pixel_ratio.den == 1); CHECK(p1_json.info.interlaced_frame == false); + CHECK(p1_json.info.spherical == false); std::stringstream profile2; profile2 << TEST_MEDIA_PATH << "example_profile2"; @@ -83,6 +86,7 @@ TEST_CASE( "constructor with example profiles", "[libopenshot][profile]" ) CHECK(p2.info.pixel_ratio.num == 1); CHECK(p2.info.pixel_ratio.den == 1); CHECK(p2.info.interlaced_frame == true); + CHECK(p2.info.spherical == false); } TEST_CASE( "24 fps names", "[libopenshot][profile]" ) @@ -163,7 +167,6 @@ TEST_CASE( "save profiles", "[libopenshot][profile]" ) // Save copy std::stringstream profile1_copy; profile1_copy << TEST_MEDIA_PATH << "example_profile1_copy"; - std::cout << profile1_copy.str() << std::endl; p1.Save(profile1_copy.str()); // Load saved copy @@ -180,4 +183,52 @@ TEST_CASE( "save profiles", "[libopenshot][profile]" ) CHECK(p1_load_copy.info.pixel_ratio.num == 1); CHECK(p1_load_copy.info.pixel_ratio.den == 1); CHECK(p1_load_copy.info.interlaced_frame == false); + CHECK(p1_load_copy.info.spherical == false); +} + +TEST_CASE( "spherical profiles", "[libopenshot][profile]" ) +{ + // Create a new profile with spherical=true + openshot::Profile p; + p.info.description = "360° Test Profile"; + p.info.width = 3840; + p.info.height = 1920; + p.info.fps.num = 30; + p.info.fps.den = 1; + p.info.display_ratio.num = 2; + p.info.display_ratio.den = 1; + p.info.pixel_ratio.num = 1; + p.info.pixel_ratio.den = 1; + p.info.interlaced_frame = false; + p.info.spherical = true; + + // Test the name methods for spherical content + CHECK(p.Key() == "03840x1920p0030_02-01_360"); + CHECK(p.ShortName() == "3840x1920p30 360°"); + CHECK(p.LongName() == "3840x1920p @ 30 fps (2:1) 360°"); + CHECK(p.LongNameWithDesc() == "3840x1920p @ 30 fps (2:1) 360° 360° Test Profile"); + + // Test JSON serialization and deserialization + std::string json = p.Json(); + openshot::Profile p_json; + p_json.SetJson(json); + + CHECK(p_json.info.spherical == true); + CHECK(p_json.ShortName() == "3840x1920p30 360°"); + + // Save and reload to test file I/O + std::stringstream profile_path; + profile_path << TEST_MEDIA_PATH << "example_profile_360"; + p.Save(profile_path.str()); + + // Load the saved profile + openshot::Profile p_loaded(profile_path.str()); + CHECK(p_loaded.info.spherical == true); + CHECK(p_loaded.ShortName() == "3840x1920p30 360°"); + + // Test comparison operators + openshot::Profile p_non_spherical = p; + p_non_spherical.info.spherical = false; + + CHECK_FALSE(p == p_non_spherical); } \ No newline at end of file From 7fb8308cc70c9a9f928452d1e968125cc03e6ee5 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 20 May 2025 20:21:32 -0500 Subject: [PATCH 2/6] Adding spherical support to FFmpegWriter and FFmpegReader, so they can optionally write the correct side data for Spacial and equirectangular 360 videos. --- src/FFmpegReader.cpp | 61 +++++++---- src/FFmpegUtilities.h | 4 + src/FFmpegWriter.cpp | 45 ++++++++ src/FFmpegWriter.h | 7 ++ tests/CMakeLists.txt | 1 + tests/SphericalMetadata.cpp | 210 ++++++++++++++++++++++++++++++++++++ 6 files changed, 308 insertions(+), 20 deletions(-) create mode 100644 tests/SphericalMetadata.cpp diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index c23680ed..7a31eab2 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -554,28 +554,49 @@ void FFmpegReader::Open() { info.metadata[str_key.toStdString()] = str_value.trimmed().toStdString(); } - // If "rotate" isn't already set, extract it from the video stream's side data. - // TODO: nb_side_data is depreciated, and I'm not sure the preferred way to do this - if (info.metadata.find("rotate") == info.metadata.end()) { - for (unsigned int i = 0; i < pFormatCtx->nb_streams; i++) { - if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - for (int j = 0; j < pFormatCtx->streams[i]->nb_side_data; j++) { - // Get the j-th side data element. - AVPacketSideData *sd = &pFormatCtx->streams[i]->side_data[j]; -#pragma GCC diagnostic pop - if (sd->type == AV_PKT_DATA_DISPLAYMATRIX && sd->size >= 9 * sizeof(int32_t)) { - double rotation = -av_display_rotation_get(reinterpret_cast(sd->data)); - if (isnan(rotation)) - rotation = 0; - QString str_value = QString::number(rotation, 'g', 6); - info.metadata["rotate"] = str_value.trimmed().toStdString(); - break; - } + // Process video stream side data (rotation, spherical metadata, etc) + for (unsigned int i = 0; i < pFormatCtx->nb_streams; i++) { + AVStream* st = pFormatCtx->streams[i]; + if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + // Only inspect the first video stream + for (int j = 0; j < st->nb_side_data; j++) { + AVPacketSideData *sd = &st->side_data[j]; + + // Handle rotation metadata (unchanged) + if (sd->type == AV_PKT_DATA_DISPLAYMATRIX && + sd->size >= 9 * sizeof(int32_t) && + !info.metadata.count("rotate")) + { + double rotation = -av_display_rotation_get( + reinterpret_cast(sd->data)); + if (std::isnan(rotation)) rotation = 0; + info.metadata["rotate"] = std::to_string(rotation); + } + // Handle spherical video metadata + else if (sd->type == AV_PKT_DATA_SPHERICAL) { + // Always mark as spherical + info.metadata["spherical"] = "1"; + + // Cast the raw bytes to an AVSphericalMapping + const AVSphericalMapping* map = + reinterpret_cast(sd->data); + + // Projection enum → string + const char* proj_name = av_spherical_projection_name(map->projection); + info.metadata["spherical_projection"] = proj_name + ? proj_name + : "unknown"; + + // Convert 16.16 fixed-point to float degrees + auto to_deg = [](int32_t v){ + return (double)v / 65536.0; + }; + info.metadata["spherical_yaw"] = std::to_string(to_deg(map->yaw)); + info.metadata["spherical_pitch"] = std::to_string(to_deg(map->pitch)); + info.metadata["spherical_roll"] = std::to_string(to_deg(map->roll)); } - break; // Only process the first video stream. } + break; } } diff --git a/src/FFmpegUtilities.h b/src/FFmpegUtilities.h index 143b3427..aec71c14 100644 --- a/src/FFmpegUtilities.h +++ b/src/FFmpegUtilities.h @@ -68,6 +68,10 @@ extern "C" { #include #endif +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(55, 0, 0) + #include +#endif + #if IS_FFMPEG_3_2 #include "libavutil/imgutils.h" #endif diff --git a/src/FFmpegWriter.cpp b/src/FFmpegWriter.cpp index a17cc442..340f3657 100644 --- a/src/FFmpegWriter.cpp +++ b/src/FFmpegWriter.cpp @@ -2349,3 +2349,48 @@ void FFmpegWriter::RemoveScalers() { // Clear vector image_rescalers.clear(); } + +// In FFmpegWriter.cpp +void FFmpegWriter::AddSphericalMetadata(const std::string& projection, + float yaw_deg, + float pitch_deg, + float roll_deg) +{ + if (!oc) return; + if (!info.has_video || !video_st) return; + + // Allow movenc.c to write out the sv3d atom + oc->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL; + +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 0, 0) + // Map the projection name to the enum (defaults to equirectangular) + int proj = av_spherical_from_name(projection.c_str()); + if (proj < 0) + proj = AV_SPHERICAL_EQUIRECTANGULAR; + + // Allocate the side‐data structure + size_t sd_size = 0; + AVSphericalMapping* map = av_spherical_alloc(&sd_size); + if (!map) { + // Allocation failed; skip metadata + return; + } + + // Populate it + map->projection = static_cast(proj); + // yaw/pitch/roll are 16.16 fixed point + map->yaw = static_cast(yaw_deg * (1 << 16)); + map->pitch = static_cast(pitch_deg * (1 << 16)); + map->roll = static_cast(roll_deg * (1 << 16)); + + // Attach to the video stream so movenc will emit an sv3d atom + av_stream_add_side_data( + video_st, + AV_PKT_DATA_SPHERICAL, + reinterpret_cast(map), + sd_size + ); +#else + // FFmpeg build too old: spherical side-data not supported +#endif +} diff --git a/src/FFmpegWriter.h b/src/FFmpegWriter.h index 4aa89550..1f54eed9 100644 --- a/src/FFmpegWriter.h +++ b/src/FFmpegWriter.h @@ -318,6 +318,13 @@ namespace openshot { /// by the Close() method if this method has not yet been called. void WriteTrailer(); + /// @brief Add spherical (360°) video metadata to the video stream + /// @param projection The projection type (e.g., "equirectangular", "cubemap") + /// @param yaw_deg The yaw angle in degrees (horizontal orientation, default 0) + /// @param pitch_deg The pitch angle in degrees (vertical orientation, default 0) + /// @param roll_deg The roll angle in degrees (tilt orientation, default 0) + void AddSphericalMetadata(const std::string& projection="equirectangular", float yaw_deg=0.0f, float pitch_deg=0.0f, float roll_deg=0.0f); + }; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 84b63266..afb30f88 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,6 +40,7 @@ set(OPENSHOT_TESTS QtImageReader ReaderBase Settings + SphericalMetadata Timeline # Effects ChromaKey diff --git a/tests/SphericalMetadata.cpp b/tests/SphericalMetadata.cpp new file mode 100644 index 00000000..96b7eeac --- /dev/null +++ b/tests/SphericalMetadata.cpp @@ -0,0 +1,210 @@ +/** + * @file + * @brief Unit tests for FFmpegWriter spherical metadata + * @author Jonathan Thomas + * + * @ref License + */ + +// Copyright (c) 2008-2023 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "openshot_catch.h" + +#include +#include +#include +#include + +#include "FFmpegReader.h" +#include "FFmpegWriter.h" +#include "Fraction.h" +#include "Frame.h" + +using namespace openshot; + +TEST_CASE( "SphericalMetadata_Test", "[libopenshot][ffmpegwriter]" ) +{ + // Create a reader to grab some frames + FFmpegReader r(TEST_MEDIA_PATH "sintel_trailer-720p.mp4"); + r.Open(); + + // Create a spherical metadata test video + std::string test_file = "spherical_test.mp4"; + + // Create a writer + FFmpegWriter w(test_file); + + // Set options - Using MP4 with H.264 for best compatibility with spherical metadata + w.SetVideoOptions(true, "libx264", r.info.fps, r.info.width, r.info.height, + r.info.pixel_ratio, false, false, 3000000); + w.SetAudioOptions(true, "aac", r.info.sample_rate, r.info.channels, + r.info.channel_layout, 128000); + + w.PrepareStreams(); + + // Add spherical metadata BEFORE opening the writer + float test_yaw = 30.0f; + w.AddSphericalMetadata("equirectangular", test_yaw, 0.0f, 0.0f); + + // Open writer + w.Open(); + + // Write a few frames + for (int frame = 1; frame <= 30; frame++) { + // Get the frame + std::shared_ptr f = r.GetFrame(frame); + + // Write the frame + w.WriteFrame(f); + } + + // Close the writer & reader + w.Close(); + r.Close(); + + // Reopen the file with FFmpegReader to verify metadata was added + FFmpegReader test_reader(test_file); + test_reader.Open(); + + // Display format information for debugging + INFO("Container format: " << test_reader.info.vcodec); + INFO("Duration: " << test_reader.info.duration); + INFO("Width x Height: " << test_reader.info.width << "x" << test_reader.info.height); + + // Check metadata map contents for debugging + INFO("Metadata entries in reader:"); + for (const auto& entry : test_reader.info.metadata) { + INFO(" " << entry.first << " = " << entry.second); + } + + // Check if spherical metadata is present in the reader + bool has_spherical_metadata = false; + if (test_reader.info.metadata.count("spherical") > 0 && + test_reader.info.metadata["spherical"] == "1") { + has_spherical_metadata = true; + } + + // Report detection status (as warning to avoid test failures if format doesn't support it) + INFO("Spherical metadata detected: " << (has_spherical_metadata ? "Yes" : "No")); + + // We won't fail the test if metadata isn't detected, as this depends on FFmpeg version and container support + // Instead we'll warn and still consider the test a success if we could create the file without errors + if (!has_spherical_metadata) { + WARN("Spherical metadata not detected. This might be OK depending on FFmpeg version and container format."); + } else { + SUCCEED("Spherical metadata successfully detected!"); + } + + // Success is that we could add the metadata without errors + SUCCEED("Successfully created video with spherical metadata"); + + // Close reader + test_reader.Close(); + std::remove(test_file.c_str()); +} + +TEST_CASE( "SphericalMetadata_FullOrientation", "[libopenshot][ffmpegwriter]" ) +{ + // Create a reader to grab some frames + FFmpegReader r(TEST_MEDIA_PATH "sintel_trailer-720p.mp4"); + r.Open(); + + // Create a spherical metadata test video + std::string test_file = "spherical_orientation_test.mp4"; + + // Create a writer + FFmpegWriter w(test_file); + + // Set options - Using MP4 with H.264 for best compatibility with spherical metadata + w.SetVideoOptions(true, "libx264", r.info.fps, r.info.width, r.info.height, + r.info.pixel_ratio, false, false, 3000000); + w.SetAudioOptions(true, "aac", r.info.sample_rate, r.info.channels, + r.info.channel_layout, 128000); + + w.PrepareStreams(); + + // Add spherical metadata BEFORE opening the writer + float test_yaw = 45.0f; + float test_pitch = 30.0f; + float test_roll = 15.0f; + w.AddSphericalMetadata("equirectangular", test_yaw, test_pitch, test_roll); + + // Open writer + w.Open(); + + // Write a few frames + for (int frame = 1; frame <= 30; frame++) { + // Get the frame + std::shared_ptr f = r.GetFrame(frame); + + // Write the frame + w.WriteFrame(f); + } + + // Close the writer & reader + w.Close(); + r.Close(); + + // Reopen the file with FFmpegReader to verify metadata was added + FFmpegReader test_reader(test_file); + test_reader.Open(); + + // Check metadata map contents for debugging + INFO("Metadata entries in reader:"); + for (const auto& entry : test_reader.info.metadata) { + INFO(" " << entry.first << " = " << entry.second); + } + + // Check if spherical metadata is present in the reader + bool has_spherical_metadata = false; + if (test_reader.info.metadata.count("spherical") > 0 && + test_reader.info.metadata["spherical"] == "1") { + has_spherical_metadata = true; + } + + // Report detection status but don't fail the test + INFO("Spherical metadata detected: " << (has_spherical_metadata ? "Yes" : "No")); + + // Only check for orientation values if spherical metadata was detected + if (has_spherical_metadata) { + // Check orientation values + bool has_yaw = false; + bool has_pitch = false; + bool has_roll = false; + + for (const auto& entry : test_reader.info.metadata) { + if (entry.first.find("yaw") != std::string::npos) { + has_yaw = true; + INFO("Yaw value: " << entry.second); + } + else if (entry.first.find("pitch") != std::string::npos) { + has_pitch = true; + INFO("Pitch value: " << entry.second); + } + else if (entry.first.find("roll") != std::string::npos) { + has_roll = true; + INFO("Roll value: " << entry.second); + } + } + + // Report orientation status + INFO("Orientation values detected - Yaw: " << (has_yaw ? "Yes" : "No") + << ", Pitch: " << (has_pitch ? "Yes" : "No") + << ", Roll: " << (has_roll ? "Yes" : "No")); + + if (has_yaw && has_pitch && has_roll) { + SUCCEED("All orientation values successfully detected!"); + } else { + WARN("Some orientation values were not detected. This might be OK depending on FFmpeg version and container format."); + } + } + + // Success is that we could add the metadata without errors + SUCCEED("Successfully created video with spherical metadata and orientation values"); + + // Close reader + test_reader.Close(); + std::remove(test_file.c_str()); +} \ No newline at end of file From 90931734a15979bd5bd94ef1486e6eec914eab50 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 20 May 2025 20:48:25 -0500 Subject: [PATCH 3/6] Improving unit tests for spherical to actually assert things and verify everything worked. --- tests/SphericalMetadata.cpp | 98 +++++++++++-------------------------- 1 file changed, 29 insertions(+), 69 deletions(-) diff --git a/tests/SphericalMetadata.cpp b/tests/SphericalMetadata.cpp index 96b7eeac..a0bdd7a5 100644 --- a/tests/SphericalMetadata.cpp +++ b/tests/SphericalMetadata.cpp @@ -79,28 +79,19 @@ TEST_CASE( "SphericalMetadata_Test", "[libopenshot][ffmpegwriter]" ) INFO(" " << entry.first << " = " << entry.second); } - // Check if spherical metadata is present in the reader - bool has_spherical_metadata = false; - if (test_reader.info.metadata.count("spherical") > 0 && - test_reader.info.metadata["spherical"] == "1") { - has_spherical_metadata = true; - } + // Verify presence of spherical metadata and orientation keys + CHECK(test_reader.info.metadata.count("spherical") > 0); + CHECK(test_reader.info.metadata["spherical"] == "1"); + CHECK(test_reader.info.metadata.count("spherical_projection") > 0); + CHECK(test_reader.info.metadata.count("spherical_yaw") > 0); + CHECK(test_reader.info.metadata.count("spherical_pitch") > 0); + CHECK(test_reader.info.metadata.count("spherical_roll") > 0); - // Report detection status (as warning to avoid test failures if format doesn't support it) - INFO("Spherical metadata detected: " << (has_spherical_metadata ? "Yes" : "No")); - - // We won't fail the test if metadata isn't detected, as this depends on FFmpeg version and container support - // Instead we'll warn and still consider the test a success if we could create the file without errors - if (!has_spherical_metadata) { - WARN("Spherical metadata not detected. This might be OK depending on FFmpeg version and container format."); - } else { - SUCCEED("Spherical metadata successfully detected!"); - } - - // Success is that we could add the metadata without errors - SUCCEED("Successfully created video with spherical metadata"); - - // Close reader + // Spot-check yaw value + float yaw_found = std::stof(test_reader.info.metadata["spherical_yaw"]); + CHECK(yaw_found == Approx(test_yaw).margin(0.5f)); + + // Clean up test_reader.Close(); std::remove(test_file.c_str()); } @@ -156,55 +147,24 @@ TEST_CASE( "SphericalMetadata_FullOrientation", "[libopenshot][ffmpegwriter]" ) for (const auto& entry : test_reader.info.metadata) { INFO(" " << entry.first << " = " << entry.second); } - - // Check if spherical metadata is present in the reader - bool has_spherical_metadata = false; - if (test_reader.info.metadata.count("spherical") > 0 && - test_reader.info.metadata["spherical"] == "1") { - has_spherical_metadata = true; - } - // Report detection status but don't fail the test - INFO("Spherical metadata detected: " << (has_spherical_metadata ? "Yes" : "No")); - - // Only check for orientation values if spherical metadata was detected - if (has_spherical_metadata) { - // Check orientation values - bool has_yaw = false; - bool has_pitch = false; - bool has_roll = false; - - for (const auto& entry : test_reader.info.metadata) { - if (entry.first.find("yaw") != std::string::npos) { - has_yaw = true; - INFO("Yaw value: " << entry.second); - } - else if (entry.first.find("pitch") != std::string::npos) { - has_pitch = true; - INFO("Pitch value: " << entry.second); - } - else if (entry.first.find("roll") != std::string::npos) { - has_roll = true; - INFO("Roll value: " << entry.second); - } - } - - // Report orientation status - INFO("Orientation values detected - Yaw: " << (has_yaw ? "Yes" : "No") - << ", Pitch: " << (has_pitch ? "Yes" : "No") - << ", Roll: " << (has_roll ? "Yes" : "No")); - - if (has_yaw && has_pitch && has_roll) { - SUCCEED("All orientation values successfully detected!"); - } else { - WARN("Some orientation values were not detected. This might be OK depending on FFmpeg version and container format."); - } - } - - // Success is that we could add the metadata without errors - SUCCEED("Successfully created video with spherical metadata and orientation values"); - - // Close reader + // Verify presence of spherical metadata and orientation keys + CHECK(test_reader.info.metadata.count("spherical") > 0); + CHECK(test_reader.info.metadata["spherical"] == "1"); + CHECK(test_reader.info.metadata.count("spherical_projection") > 0); + CHECK(test_reader.info.metadata.count("spherical_yaw") > 0); + CHECK(test_reader.info.metadata.count("spherical_pitch") > 0); + CHECK(test_reader.info.metadata.count("spherical_roll") > 0); + + // Validate each orientation value + float yaw_found = std::stof(test_reader.info.metadata["spherical_yaw"]); + float pitch_found = std::stof(test_reader.info.metadata["spherical_pitch"]); + float roll_found = std::stof(test_reader.info.metadata["spherical_roll"]); + CHECK(yaw_found == Approx(test_yaw).margin(0.5f)); + CHECK(pitch_found == Approx(test_pitch).margin(0.5f)); + CHECK(roll_found == Approx(test_roll).margin(0.5f)); + + // Clean up test_reader.Close(); std::remove(test_file.c_str()); } \ No newline at end of file From 9d33c45d84cd18c2cb404ff665ba5141e4cd4ae4 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 21 May 2025 01:28:26 -0500 Subject: [PATCH 4/6] Adding new spherical projection effect, to flatten out 360/180 videos into the viewport. --- src/CMakeLists.txt | 1 + src/EffectInfo.cpp | 5 + src/effects/SphericalProjection.cpp | 227 ++++++++++++++++++++++++++++ src/effects/SphericalProjection.h | 66 ++++++++ 4 files changed, 299 insertions(+) create mode 100644 src/effects/SphericalProjection.cpp create mode 100644 src/effects/SphericalProjection.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6713d5a9..a35469d3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -120,6 +120,7 @@ set(EFFECTS_SOURCES effects/Pixelate.cpp effects/Saturation.cpp effects/Shift.cpp + effects/SphericalProjection.cpp effects/Wave.cpp audio_effects/STFT.cpp audio_effects/Noise.cpp diff --git a/src/EffectInfo.cpp b/src/EffectInfo.cpp index 94221aed..55016a19 100644 --- a/src/EffectInfo.cpp +++ b/src/EffectInfo.cpp @@ -12,6 +12,7 @@ #include "EffectInfo.h" #include "Effects.h" +#include "effects/SphericalProjection.h" using namespace openshot; @@ -67,6 +68,9 @@ EffectBase* EffectInfo::CreateEffect(std::string effect_type) { else if (effect_type == "Shift") return new Shift(); + else if (effect_type == "SphericalProjection") + return new SphericalProjection(); + else if (effect_type == "Wave") return new Wave(); @@ -135,6 +139,7 @@ Json::Value EffectInfo::JsonValue() { root.append(Pixelate().JsonInfo()); root.append(Saturation().JsonInfo()); root.append(Shift().JsonInfo()); + root.append(SphericalProjection().JsonInfo()); root.append(Wave().JsonInfo()); /* Audio */ root.append(Noise().JsonInfo()); diff --git a/src/effects/SphericalProjection.cpp b/src/effects/SphericalProjection.cpp new file mode 100644 index 00000000..5cf218cb --- /dev/null +++ b/src/effects/SphericalProjection.cpp @@ -0,0 +1,227 @@ +/** + * @file + * @brief Source file for SphericalProjection effect class + * @author Jonathan Thomas + * + * @ref License + */ + +// Copyright (c) 2008-2025 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "SphericalProjection.h" +#include "Exceptions.h" + +#include +#include + +#ifdef _OPENMP +#include +#endif + +using namespace openshot; + +SphericalProjection::SphericalProjection() + : yaw(0.0), pitch(0.0), roll(0.0), fov(90.0), + projection_mode(0), interpolation(0) +{ + init_effect_details(); +} + +SphericalProjection::SphericalProjection(Keyframe new_yaw, + Keyframe new_pitch, + Keyframe new_roll, + Keyframe new_fov) + : yaw(new_yaw), pitch(new_pitch), roll(new_roll), fov(new_fov), + projection_mode(0), interpolation(0) +{ + init_effect_details(); +} + +void SphericalProjection::init_effect_details() +{ + InitEffectInfo(); + info.class_name = "SphericalProjection"; + info.name = "Spherical Projection"; + info.description = "Flatten 360° video with yaw/pitch/roll, sphere or hemisphere mode, nearest or bilinear"; + info.has_audio = false; + info.has_video = true; + + // Keyframes auto-registered +} + +std::shared_ptr SphericalProjection::GetFrame( + std::shared_ptr frame, + int64_t frame_number) +{ + auto img = frame->GetImage(); + if (img->format() != QImage::Format_ARGB32) + *img = img->convertToFormat(QImage::Format_ARGB32); + + int W = img->width(), H = img->height(); + int bpl = img->bytesPerLine(); + uchar *src = img->bits(); + + QImage output(W, H, QImage::Format_ARGB32); + output.fill(Qt::black); + uchar *dst = output.bits(); + int dst_bpl = output.bytesPerLine(); + + // read keyframes & convert + double yaw_r = yaw.GetValue(frame_number) * M_PI/180.0; + double pitch_r = pitch.GetValue(frame_number) * M_PI/180.0; + double roll_r = roll.GetValue(frame_number) * M_PI/180.0; + double fov_r = fov.GetValue(frame_number) * M_PI/180.0; + + // rotation matrix R = Ry * Rx * Rz + double sy=sin(yaw_r), cy=cos(yaw_r), + sp=sin(pitch_r),cp=cos(pitch_r), + sr=sin(roll_r), cr=cos(roll_r); + double r00=cy*cr+sy*sp*sr, r01=-cy*sr+sy*sp*cr, r02=sy*cp; + double r10=cp*sr, r11=cp*cr, r12=-sp; + double r20=-sy*cr+cy*sp*sr, r21=sy*sr+cy*sp*cr, r22=cy*cp; + + // fov scalars + double hx = tan(fov_r/2.0); + double vy = hx * (double(H)/W); + +#ifdef _OPENMP +#pragma omp parallel for schedule(static) +#endif + for(int yy=0; yy + * + * @ref License + */ + +// Copyright (c) 2008-2025 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef OPENSHOT_SPHERICAL_PROJECTION_EFFECT_H +#define OPENSHOT_SPHERICAL_PROJECTION_EFFECT_H + +#include "../EffectBase.h" +#include "../Frame.h" +#include "../Json.h" +#include "../KeyFrame.h" + +#include +#include + +namespace openshot +{ + + /** + * @brief Projects a 360° frame through a pinhole camera. + * You can choose full sphere or hemisphere, and nearest-neighbor or bilinear sampling. + */ + class SphericalProjection : public EffectBase + { + private: + void init_effect_details(); + + public: + Keyframe yaw; ///< Yaw around up-axis (degrees) + Keyframe pitch; ///< Pitch around right-axis (degrees) + Keyframe roll; ///< Roll around forward-axis (degrees) + Keyframe fov; ///< Field-of-view (horizontal degrees) + + int projection_mode; ///< 0 = full sphere, 1 = hemisphere + int interpolation; ///< 0 = nearest, 1 = bilinear + + SphericalProjection(); + SphericalProjection(Keyframe new_yaw, + Keyframe new_pitch, + Keyframe new_roll, + Keyframe new_fov); + + std::shared_ptr GetFrame(int64_t frame_number) override + { return GetFrame(std::make_shared(), frame_number); } + + std::shared_ptr GetFrame(std::shared_ptr frame, + int64_t frame_number) override; + + std::string Json() const override; + void SetJson(const std::string value) override; + Json::Value JsonValue() const override; + void SetJsonValue(const Json::Value root) override; + std::string PropertiesJSON(int64_t requested_frame) const override; + }; + +} // namespace openshot + +#endif // OPENSHOT_SPHERICAL_PROJECTION_EFFECT_H From f93d7861f235782970ad9d7c43dfe567697eae49 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 21 May 2025 16:15:01 -0500 Subject: [PATCH 5/6] Adding fisheye mode to spherical projection effect, and cleaning up code. --- src/effects/SphericalProjection.cpp | 205 ++++++++++++++++------------ src/effects/SphericalProjection.h | 70 +++++----- 2 files changed, 159 insertions(+), 116 deletions(-) diff --git a/src/effects/SphericalProjection.cpp b/src/effects/SphericalProjection.cpp index 5cf218cb..0565a7a3 100644 --- a/src/effects/SphericalProjection.cpp +++ b/src/effects/SphericalProjection.cpp @@ -15,16 +15,18 @@ #include #include - -#ifdef _OPENMP #include -#endif using namespace openshot; SphericalProjection::SphericalProjection() - : yaw(0.0), pitch(0.0), roll(0.0), fov(90.0), - projection_mode(0), interpolation(0) + : yaw(0.0) + , pitch(0.0) + , roll(0.0) + , fov(90.0) + , projection_mode(0) + , invert(0) + , interpolation(0) { init_effect_details(); } @@ -33,8 +35,8 @@ SphericalProjection::SphericalProjection(Keyframe new_yaw, Keyframe new_pitch, Keyframe new_roll, Keyframe new_fov) - : yaw(new_yaw), pitch(new_pitch), roll(new_roll), fov(new_fov), - projection_mode(0), interpolation(0) + : yaw(new_yaw), pitch(new_pitch), roll(new_roll) + , fov(new_fov), projection_mode(0), invert(0), interpolation(0) { init_effect_details(); } @@ -44,11 +46,9 @@ void SphericalProjection::init_effect_details() InitEffectInfo(); info.class_name = "SphericalProjection"; info.name = "Spherical Projection"; - info.description = "Flatten 360° video with yaw/pitch/roll, sphere or hemisphere mode, nearest or bilinear"; + info.description = "Flatten and reproject 360° video with yaw, pitch, roll, and fov (sphere, hemisphere, fisheye modes)"; info.has_audio = false; info.has_video = true; - - // Keyframes auto-registered } std::shared_ptr SphericalProjection::GetFrame( @@ -61,85 +61,102 @@ std::shared_ptr SphericalProjection::GetFrame( int W = img->width(), H = img->height(); int bpl = img->bytesPerLine(); - uchar *src = img->bits(); + uchar* src = img->bits(); QImage output(W, H, QImage::Format_ARGB32); output.fill(Qt::black); - uchar *dst = output.bits(); + uchar* dst = output.bits(); int dst_bpl = output.bytesPerLine(); - // read keyframes & convert + // Evaluate keyframes (note roll is inverted + offset 180°) double yaw_r = yaw.GetValue(frame_number) * M_PI/180.0; double pitch_r = pitch.GetValue(frame_number) * M_PI/180.0; - double roll_r = roll.GetValue(frame_number) * M_PI/180.0; + double roll_r = -roll.GetValue(frame_number) * M_PI/180.0 + M_PI; double fov_r = fov.GetValue(frame_number) * M_PI/180.0; - // rotation matrix R = Ry * Rx * Rz - double sy=sin(yaw_r), cy=cos(yaw_r), - sp=sin(pitch_r),cp=cos(pitch_r), - sr=sin(roll_r), cr=cos(roll_r); - double r00=cy*cr+sy*sp*sr, r01=-cy*sr+sy*sp*cr, r02=sy*cp; - double r10=cp*sr, r11=cp*cr, r12=-sp; - double r20=-sy*cr+cy*sp*sr, r21=sy*sr+cy*sp*cr, r22=cy*cp; + // Build composite rotation matrix R = Ry * Rx * Rz + double sy = sin(yaw_r), cy = cos(yaw_r); + double sp = sin(pitch_r), cp = cos(pitch_r); + double sr = sin(roll_r), cr = cos(roll_r); - // fov scalars - double hx = tan(fov_r/2.0); - double vy = hx * (double(H)/W); + double r00 = cy*cr + sy*sp*sr, r01 = -cy*sr + sy*sp*cr, r02 = sy*cp; + double r10 = cp*sr, r11 = cp*cr, r12 = -sp; + double r20 = -sy*cr + cy*sp*sr, r21 = sy*sr + cy*sp*cr, r22 = cy*cp; + + // Precompute perspective scalars + double hx = tan(fov_r*0.5); + double vy = hx * double(H)/W; -#ifdef _OPENMP #pragma omp parallel for schedule(static) -#endif - for(int yy=0; yy * @@ -24,42 +24,50 @@ namespace openshot { - /** - * @brief Projects a 360° frame through a pinhole camera. - * You can choose full sphere or hemisphere, and nearest-neighbor or bilinear sampling. - */ - class SphericalProjection : public EffectBase - { - private: - void init_effect_details(); +/** + * @brief Projects 360° or fisheye video through a virtual camera. + * Supports yaw, pitch, roll, FOV, sphere/hemisphere/fisheye modes, + * optional inversion, and nearest/bilinear sampling. + */ +class SphericalProjection : public EffectBase +{ +private: + void init_effect_details(); - public: - Keyframe yaw; ///< Yaw around up-axis (degrees) - Keyframe pitch; ///< Pitch around right-axis (degrees) - Keyframe roll; ///< Roll around forward-axis (degrees) - Keyframe fov; ///< Field-of-view (horizontal degrees) +public: + Keyframe yaw; ///< Yaw around up-axis (degrees) + Keyframe pitch; ///< Pitch around right-axis (degrees) + Keyframe roll; ///< Roll around forward-axis (degrees) + Keyframe fov; ///< Field-of-view (horizontal, degrees) - int projection_mode; ///< 0 = full sphere, 1 = hemisphere - int interpolation; ///< 0 = nearest, 1 = bilinear + int projection_mode; ///< 0=Sphere, 1=Hemisphere, 2=Fisheye + int invert; ///< 0=Normal, 1=Invert (back lens / +180°) + int interpolation; ///< 0=Nearest, 1=Bilinear - SphericalProjection(); - SphericalProjection(Keyframe new_yaw, - Keyframe new_pitch, - Keyframe new_roll, - Keyframe new_fov); + /// Blank ctor (for JSON deserialization) + SphericalProjection(); - std::shared_ptr GetFrame(int64_t frame_number) override - { return GetFrame(std::make_shared(), frame_number); } + /// Ctor with custom curves + SphericalProjection(Keyframe new_yaw, + Keyframe new_pitch, + Keyframe new_roll, + Keyframe new_fov); - std::shared_ptr GetFrame(std::shared_ptr frame, - int64_t frame_number) override; + /// ClipBase override: create a fresh Frame then call the main GetFrame + std::shared_ptr GetFrame(int64_t frame_number) override + { return GetFrame(std::make_shared(), frame_number); } - std::string Json() const override; - void SetJson(const std::string value) override; - Json::Value JsonValue() const override; - void SetJsonValue(const Json::Value root) override; - std::string PropertiesJSON(int64_t requested_frame) const override; - }; + /// EffectBase override: reproject the QImage + std::shared_ptr GetFrame(std::shared_ptr frame, + int64_t frame_number) override; + + // JSON serialization + std::string Json() const override; + void SetJson(std::string value) override; + Json::Value JsonValue() const override; + void SetJsonValue(Json::Value root) override; + std::string PropertiesJSON(int64_t requested_frame) const override; +}; } // namespace openshot From ec890fe74d07d5198e708dad68751af474e6d37b Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 21 May 2025 18:25:22 -0500 Subject: [PATCH 6/6] Adding unit tests for spherical project effect, using 2 PNG assets --- examples/eq_sphere.png | Bin 0 -> 635 bytes examples/fisheye.png | Bin 0 -> 1328 bytes tests/CMakeLists.txt | 1 + tests/SphericalEffect.cpp | 146 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 examples/eq_sphere.png create mode 100644 examples/fisheye.png create mode 100644 tests/SphericalEffect.cpp diff --git a/examples/eq_sphere.png b/examples/eq_sphere.png new file mode 100644 index 0000000000000000000000000000000000000000..53273886926c015dbeaeb5dcc06e67f617c2e180 GIT binary patch literal 635 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(VA#UJ#K6E{#-o_Tz`*3>>EaktG3V`-gS-t6 zJj{+x|81Rlf1iB0WG0u6ec8Tt-p4Ow`{F*%lViB!ZqeW~ibg{KH3Z(U3!Gp{_dO6L R&%nUI;OXk;vd$@?2><|f)209b literal 0 HcmV?d00001 diff --git a/examples/fisheye.png b/examples/fisheye.png new file mode 100644 index 0000000000000000000000000000000000000000..8b149821110d4a8b779c44bb82f1a25092d1e69d GIT binary patch literal 1328 zcmeAS@N?(olHy`uVBq!ia0y~yU^oH7983%h3`$m(Cm9%6zI(bjhE&XXd+YYJ#a2A7 z7bpDx|NIKGsfM_~f$6#4pJu(c?Q>lHbWX`M@qfQ->{9CsBByOrFU-AH9w-yA&a!#- ziq>pZu)g2omHRKJ!F~P>KbaeWmfX+4J))Jsa?(y5DPsTY=8OV{z`X`t%)1g zEm}1vCp02NH2Q4K>BEawNs9Q-JRdPZgQvUHw0~3N^cL69&6+bR$|6Ecm*3g>S(00* zGI8U&v}vZbW@0Kvhmv9+Rxf7W%zm2YX;zIG;4PPz!5kePSX1BDY)o5Z-=}T>k0~un{bYj*rZB z>=G7E-Kq-F+5$HD)1%4OEH@%1DCl?yfK3DogH)fL9ubg`?AWwH!4ZozZsCt{(^9`k z3J8E5!Fi~OB@to9p7{0R@u>^frbZ%Lxj*saPuaC=bT@8I3 z$#cHq@jDUB+sNO2TOhUU=?1W33(oG_0z3Q8ZrG?$C@1F4eSYq>L&rc?KFW@`(^l~) zS{)Lum!RRggR^^SYyP_13CWI+>~yAy_2sPwF#{uXbeGKga^P6gk;@w*q9SZ{6qA!L zip?_z#oL|@5mCmcXW2<}3*AiIxaHXt>+h3#U86x!|1i`e`m@Gt)A<_}0_DWKv)PUY zue%ko1(aa2*E`+nT2-{E=2(+U^M;71k5|rbIeTo;T8~*in{S4CM1#`Z>GKg=p2i;L zH2e3#FS>5)+}Pj$wU_Aj&+_;ERLY$DbJ2|Huc~!!eR49zT3n*FN%D-6zx4P%SLW$ie8Mlk^k3ML>OMd6%kNU=+{g>LT(R|L&e6$VYyKyz{ry?f z_NRRL)&J|i%#i=JaTh3ewf}9JvhA0|*LlbNr)LMIyota2aerlcsU}bT>c@wxBrCWN zMpVpy>_6dA)S48Fx@fk^QIfV{U3>gy z`evp6D$x9NOTs_v>4T1yCqEZWymIQSW3ceEry47#Zhri(MgL5!zx4X&eywLt=Lc!u zUw)r6V8-`(W{z^n$72%{t8Tee&y#)Jdh~Us@VA3~JuYGv%dUv + * + * @ref License + */ + +// Copyright (c) 2008-2025 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include +#include +#include "Frame.h" +#include "effects/SphericalProjection.h" +#include "openshot_catch.h" + +using namespace openshot; + +// allow Catch2 to print QColor on failure +static std::ostream& operator<<(std::ostream& os, QColor const& c) +{ + os << "QColor(" << c.red() << "," << c.green() + << "," << c.blue() << "," << c.alpha() << ")"; + return os; +} + +// load a PNG into a Frame +static std::shared_ptr loadFrame(const char* filename) +{ + QImage img(QString(TEST_MEDIA_PATH) + filename); + img = img.convertToFormat(QImage::Format_ARGB32); + auto f = std::make_shared(); + *f->GetImage() = img; + return f; +} + +// apply effect and sample center pixel +static QColor centerPixel(SphericalProjection& e, + std::shared_ptr f) +{ + auto img = e.GetFrame(f, 1)->GetImage(); + int cx = img->width() / 2; + int cy = img->height() / 2; + return img->pixelColor(cx, cy); +} + +TEST_CASE("sphere mode default and invert", "[effect][spherical]") +{ + SphericalProjection e; + e.projection_mode = 0; + e.yaw = Keyframe(45.0); + + { + auto f0 = loadFrame("eq_sphere.png"); + e.invert = 0; + e.interpolation = 0; + // eq_sphere.png has green stripe at center + CHECK(centerPixel(e, f0) == QColor(255,0,0,255)); + } + { + auto f1 = loadFrame("eq_sphere.png"); + e.yaw = Keyframe(-45.0); + e.invert = 0; + e.interpolation = 1; + // invert flips view 180°, center maps to blue stripe + CHECK(centerPixel(e, f1) == QColor(0,0,255,255)); + } + { + auto f1 = loadFrame("eq_sphere.png"); + e.yaw = Keyframe(0.0); + e.invert = 1; + e.interpolation = 0; + // invert flips view 180°, center maps to blue stripe + CHECK(centerPixel(e, f1) == QColor(0,255,0,255)); + } +} + +TEST_CASE("hemisphere mode default and invert", "[effect][spherical]") +{ + SphericalProjection e; + e.projection_mode = 1; + + { + auto f0 = loadFrame("eq_sphere.png"); + e.yaw = Keyframe(45.0); + e.invert = 0; + e.interpolation = 0; + // hemisphere on full pano still shows green at center + CHECK(centerPixel(e, f0) == QColor(255,0,0,255)); + } + { + auto f1 = loadFrame("eq_sphere.png"); + e.yaw = Keyframe(-45.0); + e.invert = 0; + e.interpolation = 1; + // invert=1 flips center to blue + CHECK(centerPixel(e, f1) == QColor(0,0,255,255)); + } + { + auto f1 = loadFrame("eq_sphere.png"); + e.yaw = Keyframe(-180.0); + e.invert = 0; + e.interpolation = 0; + // invert=1 flips center to blue + CHECK(centerPixel(e, f1) == QColor(0,255,0,255)); + } +} + +TEST_CASE("fisheye mode default and invert", "[effect][spherical]") +{ + SphericalProjection e; + e.projection_mode = 2; + e.fov = Keyframe(180.0); + + { + auto f0 = loadFrame("fisheye.png"); + e.invert = 0; + e.interpolation = 0; + // circular mask center remains white + CHECK(centerPixel(e, f0) == QColor(255,255,255,255)); + } + { + auto f1 = loadFrame("fisheye.png"); + e.invert = 1; + e.interpolation = 1; + e.fov = Keyframe(90.0); + // invert has no effect on center + CHECK(centerPixel(e, f1) == QColor(255,255,255,255)); + } +} + +TEST_CASE("fisheye mode yaw has no effect at center", "[effect][spherical]") +{ + SphericalProjection e; + e.projection_mode = 2; + e.interpolation = 0; + e.fov = Keyframe(180.0); + e.invert = 0; + + auto f = loadFrame("fisheye.png"); + e.yaw = Keyframe(45.0); + CHECK(centerPixel(e, f) == QColor(255,255,255,255)); +}