From f60d6659e19e06ec49fae4a3c521ff1b192382dd Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 12 Jun 2025 17:30:17 -0500 Subject: [PATCH 01/41] Bumping version to 0.5.0, SO 28, this is a major new release of libopenshot. --- CMakeLists.txt | 4 ++-- src/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bb5a5e92d..c8a559ad3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,8 +24,8 @@ For more information, please visit . set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules") ################ PROJECT VERSION #################### -set(PROJECT_VERSION_FULL "0.4.0") -set(PROJECT_SO_VERSION 27) +set(PROJECT_VERSION_FULL "0.5.0") +set(PROJECT_SO_VERSION 28) # Remove the dash and anything following, to get the #.#.# version for project() STRING(REGEX REPLACE "\-.*$" "" VERSION_NUM "${PROJECT_VERSION_FULL}") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 153b2c1ec..26334783e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -181,7 +181,7 @@ target_include_directories(openshot # Find JUCE-based openshot Audio libraries if(NOT TARGET OpenShot::Audio) # Only load if necessary (not for integrated builds) - find_package(OpenShotAudio 0.4.0 REQUIRED) + find_package(OpenShotAudio 0.5.0 REQUIRED) endif() target_link_libraries(openshot PUBLIC OpenShot::Audio) From c6720bb59b398478ef015812bf8086228e59508f Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 12 Jun 2025 17:34:36 -0500 Subject: [PATCH 02/41] Lowering version required for libopenshot-audio, since technically it's fine to link against the previous library. --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 26334783e..153b2c1ec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -181,7 +181,7 @@ target_include_directories(openshot # Find JUCE-based openshot Audio libraries if(NOT TARGET OpenShot::Audio) # Only load if necessary (not for integrated builds) - find_package(OpenShotAudio 0.5.0 REQUIRED) + find_package(OpenShotAudio 0.4.0 REQUIRED) endif() target_link_libraries(openshot PUBLIC OpenShot::Audio) From 515c4ff5099e516cb485c117a1927464c7a96d56 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Sun, 15 Jun 2025 16:01:17 -0500 Subject: [PATCH 03/41] Improving Tracker effect to better track occluded objects, follow objects offscreen and back onscreen without getting lost, and improved unit tests. --- src/CVTracker.cpp | 299 +++++++++++++++++++++++++++++--------------- src/CVTracker.h | 31 +++-- tests/CVTracker.cpp | 37 ++++++ 3 files changed, 257 insertions(+), 110 deletions(-) diff --git a/src/CVTracker.cpp b/src/CVTracker.cpp index 0690f8f14..fb8f92092 100644 --- a/src/CVTracker.cpp +++ b/src/CVTracker.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include @@ -31,6 +32,7 @@ CVTracker::CVTracker(std::string processInfoJson, ProcessingController &processi SetJson(processInfoJson); start = 1; end = 1; + lostCount = 0; } // Set desirable tracker method @@ -54,152 +56,243 @@ cv::Ptr CVTracker::selectTracker(std::string trackerType){ return nullptr; } -// Track object in the hole clip or in a given interval -void CVTracker::trackClip(openshot::Clip& video, size_t _start, size_t _end, bool process_interval){ - +// Track object in the whole clip or in a given interval +void CVTracker::trackClip(openshot::Clip& video, + size_t _start, + size_t _end, + bool process_interval) +{ video.Open(); - if(!json_interval){ + if (!json_interval) { start = _start; end = _end; - - if(!process_interval || end <= 1 || end-start == 0){ - // Get total number of frames in video - start = (int)(video.Start() * video.Reader()->info.fps.ToFloat()) + 1; - end = (int)(video.End() * video.Reader()->info.fps.ToFloat()) + 1; + if (!process_interval || end <= 1 || end - start == 0) { + start = int(video.Start() * video.Reader()->info.fps.ToFloat()) + 1; + end = int(video.End() * video.Reader()->info.fps.ToFloat()) + 1; } + } else { + start = int(start + video.Start() * video.Reader()->info.fps.ToFloat()) + 1; + end = int(video.End() * video.Reader()->info.fps.ToFloat()) + 1; } - else{ - start = (int)(start + video.Start() * video.Reader()->info.fps.ToFloat()) + 1; - end = (int)(video.End() * video.Reader()->info.fps.ToFloat()) + 1; - } - - if(error){ - return; - } - + if (error) return; processingController->SetError(false, ""); - bool trackerInit = false; - size_t frame; - // Loop through video - for (frame = start; frame <= end; frame++) - { - - // Stop the feature tracker process - if(processingController->ShouldStop()){ - return; - } + bool trackerInit = false; + lostCount = 0; // reset lost counter once at the start - size_t frame_number = frame; - // Get current frame - std::shared_ptr f = video.GetFrame(frame_number); + for (size_t frame = start; frame <= end; ++frame) { + if (processingController->ShouldStop()) return; - // Grab OpenCV Mat image - cv::Mat cvimage = f->GetImageCV(); + auto f = video.GetFrame(frame); + cv::Mat img = f->GetImageCV(); - if(frame == start){ - // Take the normalized inital bounding box and multiply to the current video shape - bbox = cv::Rect2d(int(bbox.x*cvimage.cols), int(bbox.y*cvimage.rows), - int(bbox.width*cvimage.cols), int(bbox.height*cvimage.rows)); + if (frame == start) { + bbox = cv::Rect2d( + int(bbox.x * img.cols), + int(bbox.y * img.rows), + int(bbox.width * img.cols), + int(bbox.height * img.rows) + ); } - // Pass the first frame to initialize the tracker - if(!trackerInit){ - - // Initialize the tracker - initTracker(cvimage, frame_number); - + if (!trackerInit) { + initTracker(img, frame); trackerInit = true; + lostCount = 0; } - else{ - // Update the object tracker according to frame - trackerInit = trackFrame(cvimage, frame_number); - - // Draw box on image - FrameData fd = GetTrackedData(frame_number); + else { + // trackFrame now manages lostCount and will re-init internally + trackFrame(img, frame); + // record whatever bbox we have now + FrameData fd = GetTrackedData(frame); } - // Update progress - processingController->SetProgress(uint(100*(frame_number-start)/(end-start))); + + processingController->SetProgress( + uint(100 * (frame - start) / (end - start)) + ); } } // Initialize the tracker -bool CVTracker::initTracker(cv::Mat &frame, size_t frameId){ - +bool CVTracker::initTracker(cv::Mat &frame, size_t frameId) +{ // Create new tracker object tracker = selectTracker(trackerType); - // Correct if bounding box contains negative proportions (width and/or height < 0) - if(bbox.width < 0){ - bbox.x = bbox.x - abs(bbox.width); - bbox.width = abs(bbox.width); + // Correct negative width/height + if (bbox.width < 0) { + bbox.x -= bbox.width; + bbox.width = -bbox.width; } - if(bbox.height < 0){ - bbox.y = bbox.y - abs(bbox.height); - bbox.height = abs(bbox.height); + if (bbox.height < 0) { + bbox.y -= bbox.height; + bbox.height = -bbox.height; } + // Clamp to frame bounds + bbox &= cv::Rect2d(0, 0, frame.cols, frame.rows); + if (bbox.width <= 0) bbox.width = 1; + if (bbox.height <= 0) bbox.height = 1; + // Initialize tracker tracker->init(frame, bbox); - float fw = frame.size().width; - float fh = frame.size().height; + float fw = float(frame.cols), fh = float(frame.rows); + + // record original pixel size + origWidth = bbox.width; + origHeight = bbox.height; + + // initialize sub-pixel smoother at true center + smoothC_x = bbox.x + bbox.width * 0.5; + smoothC_y = bbox.y + bbox.height * 0.5; // Add new frame data - trackedDataById[frameId] = FrameData(frameId, 0, (bbox.x)/fw, - (bbox.y)/fh, - (bbox.x+bbox.width)/fw, - (bbox.y+bbox.height)/fh); + trackedDataById[frameId] = FrameData( + frameId, 0, + bbox.x / fw, + bbox.y / fh, + (bbox.x + bbox.width) / fw, + (bbox.y + bbox.height) / fh + ); return true; } // Update the object tracker according to frame -bool CVTracker::trackFrame(cv::Mat &frame, size_t frameId){ - // Update the tracking result - bool ok = tracker->update(frame, bbox); +// returns true if KLT succeeded, false otherwise +bool CVTracker::trackFrame(cv::Mat &frame, size_t frameId) +{ + const int W = frame.cols, H = frame.rows; + const auto& prev = trackedDataById[frameId - 1]; + + // Reconstruct last-known box in pixel coords + cv::Rect2d lastBox( + prev.x1 * W, prev.y1 * H, + (prev.x2 - prev.x1) * W, + (prev.y2 - prev.y1) * H + ); + + // Convert to grayscale + cv::Mat gray; + cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); + + cv::Rect2d cand; + bool didKLT = false; + + // Try KLT-based drift + if (!prevGray.empty() && !prevPts.empty()) { + std::vector currPts; + std::vector status; + std::vector err; + cv::calcOpticalFlowPyrLK( + prevGray, gray, + prevPts, currPts, + status, err, + cv::Size(21,21), 3, + cv::TermCriteria{cv::TermCriteria::COUNT|cv::TermCriteria::EPS,30,0.01}, + cv::OPTFLOW_LK_GET_MIN_EIGENVALS, 1e-4 + ); + + // collect per-point displacements + std::vector dx, dy; + for (size_t i = 0; i < status.size(); ++i) { + if (status[i] && err[i] < 12.0) { + dx.push_back(currPts[i].x - prevPts[i].x); + dy.push_back(currPts[i].y - prevPts[i].y); + } + } - // Add frame number and box coords if tracker finds the object - // Otherwise add only frame number - if (ok) - { - float fw = frame.size().width; - float fh = frame.size().height; - - cv::Rect2d filtered_box = filter_box_jitter(frameId); - // Add new frame data - trackedDataById[frameId] = FrameData(frameId, 0, (filtered_box.x)/fw, - (filtered_box.y)/fh, - (filtered_box.x+filtered_box.width)/fw, - (filtered_box.y+filtered_box.height)/fh); - } - else - { - // Copy the last frame data if the tracker get lost - trackedDataById[frameId] = trackedDataById[frameId-1]; + if ((int)dx.size() >= minKltPts) { + auto median = [&](auto &v){ + std::nth_element(v.begin(), v.begin()+v.size()/2, v.end()); + return v[v.size()/2]; + }; + double mdx = median(dx), mdy = median(dy); + + cand = lastBox; + cand.x += mdx; + cand.y += mdy; + cand.width = origWidth; + cand.height = origHeight; + + lostCount = 0; + didKLT = true; + } } - return ok; -} + // Fallback to whole-frame flow if KLT failed + if (!didKLT) { + ++lostCount; + cand = lastBox; + if (!fullPrevGray.empty()) { + cv::Mat flow; + cv::calcOpticalFlowFarneback( + fullPrevGray, gray, flow, + 0.5,3,15,3,5,1.2,0 + ); + cv::Scalar avg = cv::mean(flow); + cand.x += avg[0]; + cand.y += avg[1]; + } + cand.width = origWidth; + cand.height = origHeight; -cv::Rect2d CVTracker::filter_box_jitter(size_t frameId){ - // get tracked data for the previous frame - float last_box_width = trackedDataById[frameId-1].x2 - trackedDataById[frameId-1].x1; - float last_box_height = trackedDataById[frameId-1].y2 - trackedDataById[frameId-1].y1; + if (lostCount >= 10) { + initTracker(frame, frameId); + cand = bbox; + lostCount = 0; + } + } - float curr_box_width = bbox.width; - float curr_box_height = bbox.height; - // keep the last width and height if the difference is less than 1% - float threshold = 0.01; + // Dead-zone sub-pixel smoothing + { + constexpr double JITTER_THRESH = 1.0; + double measCx = cand.x + cand.width * 0.5; + double measCy = cand.y + cand.height * 0.5; + double dx = measCx - smoothC_x; + double dy = measCy - smoothC_y; + + if (std::abs(dx) > JITTER_THRESH || std::abs(dy) > JITTER_THRESH) { + smoothC_x = measCx; + smoothC_y = measCy; + } - cv::Rect2d filtered_box = bbox; - if(std::abs(1-(curr_box_width/last_box_width)) <= threshold){ - filtered_box.width = last_box_width; + cand.x = smoothC_x - cand.width * 0.5; + cand.y = smoothC_y - cand.height * 0.5; } - if(std::abs(1-(curr_box_height/last_box_height)) <= threshold){ - filtered_box.height = last_box_height; + + // Re-seed KLT features + { + cv::Rect roi( + int(std::max(0., cand.x)), + int(std::max(0., cand.y)), + int(std::min(cand.width, double(W - cand.x))), + int(std::min(cand.height, double(H - cand.y))) + ); + cv::goodFeaturesToTrack( + gray(roi), prevPts, + kltMaxCorners, kltQualityLevel, + kltMinDist, cv::Mat(), kltBlockSize + ); + for (auto &pt : prevPts) + pt += cv::Point2f(float(roi.x), float(roi.y)); } - return filtered_box; + + // Commit state + fullPrevGray = gray.clone(); + prevGray = gray.clone(); + bbox = cand; + float fw = float(W), fh = float(H); + trackedDataById[frameId] = FrameData( + frameId, 0, + cand.x / fw, + cand.y / fh, + (cand.x + cand.width) / fw, + (cand.y + cand.height) / fh + ); + + return didKLT; } bool CVTracker::SaveTrackedData(){ diff --git a/src/CVTracker.h b/src/CVTracker.h index eff8b50a8..023d9297b 100644 --- a/src/CVTracker.h +++ b/src/CVTracker.h @@ -94,11 +94,25 @@ namespace openshot bool error = false; - // Initialize the tracker - bool initTracker(cv::Mat &frame, size_t frameId); - - // Update the object tracker according to frame - bool trackFrame(cv::Mat &frame, size_t frameId); + // count of consecutive “missed” frames + int lostCount{0}; + + // KLT parameters and state + cv::Mat prevGray; // last frame in gray + std::vector prevPts; // tracked feature points + const int kltMaxCorners = 100; // max features to keep + const double kltQualityLevel = 0.01; // goodFeatures threshold + const double kltMinDist = 5.0; // min separation + const int kltBlockSize = 3; // window for feature detect + const int minKltPts = 10; // below this, we assume occluded + double smoothC_x = 0, smoothC_y = 0; ///< running, sub-pixel center + const double smoothAlpha = 0.8; ///< [0..1], higher → tighter but more jitter + + // full-frame fall-back + cv::Mat fullPrevGray; + + // last known good box size + double origWidth{0}, origHeight{0}; public: @@ -113,8 +127,11 @@ namespace openshot /// If start, end and process_interval are passed as argument, clip will be processed in [start,end) void trackClip(openshot::Clip& video, size_t _start=0, size_t _end=0, bool process_interval=false); - /// Filter current bounding box jitter - cv::Rect2d filter_box_jitter(size_t frameId); + // Update the object tracker according to frame + bool trackFrame(cv::Mat &frame, size_t frameId); + + // Initialize the tracker + bool initTracker(cv::Mat &frame, size_t frameId); /// Get tracked data for a given frame FrameData GetTrackedData(size_t frameId); diff --git a/tests/CVTracker.cpp b/tests/CVTracker.cpp index 95bcc6c8d..0ac1116c5 100644 --- a/tests/CVTracker.cpp +++ b/tests/CVTracker.cpp @@ -107,6 +107,43 @@ TEST_CASE( "Track_Video", "[libopenshot][opencv][tracker]" ) CHECK(height == Approx(166).margin(2)); } +TEST_CASE( "Track_BoundingBoxClipping", "[libopenshot][opencv][tracker]" ) +{ + // Create a video clip + std::stringstream path; + path << TEST_MEDIA_PATH << "test.avi"; + + // Open clip + openshot::Clip c1(path.str()); + c1.Open(); + + std::string json_data = R"proto( + { + "tracker-type": "KCF", + "region": { + "normalized_x": -0.2, + "normalized_y": -0.2, + "normalized_width": 1.5, + "normalized_height": 1.5, + "first-frame": 1 + } + } )proto"; + + ProcessingController tracker_pc; + CVTracker tracker(json_data, tracker_pc); + tracker_pc.SetError(false, ""); + + // Grab first frame and run tracker directly + std::shared_ptr f = c1.GetFrame(1); + cv::Mat image = f->GetImageCV(); + + tracker.initTracker(image, 1); + tracker.trackFrame(image, 2); + + INFO(tracker_pc.GetErrorMessage()); + CHECK(tracker_pc.GetError() == false); +} + TEST_CASE( "SaveLoad_Protobuf", "[libopenshot][opencv][tracker]" ) { From 055975a21257eec30c5123b9647989f7a0ec8ec4 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 17 Jun 2025 18:27:52 -0500 Subject: [PATCH 04/41] Improving Tracker and Object Detector to include effect ID in the tracked Object IDs they return - to allow for multiple effects on the same clip, and to not accidentally clobber trackedObject IDs (i.e. "0" as the default tracked object ID) --- src/effects/ObjectDetection.cpp | 303 ++++++++++++++------------------ src/effects/ObjectDetection.h | 2 - src/effects/Tracker.cpp | 224 ++++++++++++----------- src/effects/Tracker.h | 2 - 4 files changed, 242 insertions(+), 289 deletions(-) diff --git a/src/effects/ObjectDetection.cpp b/src/effects/ObjectDetection.cpp index 6bc019552..9b3a5b984 100644 --- a/src/effects/ObjectDetection.cpp +++ b/src/effects/ObjectDetection.cpp @@ -29,29 +29,16 @@ using namespace std; using namespace openshot; -/// Blank constructor, useful when using Json to load the effect properties -ObjectDetection::ObjectDetection(std::string clipObDetectDataPath) : -display_box_text(1.0), display_boxes(1.0) -{ - // Init effect properties - init_effect_details(); - - // Tries to load the tracker data from protobuf - LoadObjDetectdData(clipObDetectDataPath); - - // Initialize the selected object index as the first object index - selectedObjectIndex = trackedObjects.begin()->first; -} - // Default constructor -ObjectDetection::ObjectDetection() : - display_box_text(1.0), display_boxes(1.0) +ObjectDetection::ObjectDetection() + : display_box_text(1.0) + , display_boxes(1.0) { - // Init effect properties + // Init effect metadata init_effect_details(); - // Initialize the selected object index as the first object index - selectedObjectIndex = trackedObjects.begin()->first; + // We haven’t loaded any protobuf yet, so there's nothing to pick. + selectedObjectIndex = -1; } // Init effect settings @@ -167,108 +154,96 @@ std::shared_ptr ObjectDetection::GetFrame(std::shared_ptr frame, i } // Load protobuf data file -bool ObjectDetection::LoadObjDetectdData(std::string inputFilePath){ - // Create tracker message - pb_objdetect::ObjDetect objMessage; - - // Read the existing tracker message. - std::fstream input(inputFilePath, std::ios::in | std::ios::binary); - if (!objMessage.ParseFromIstream(&input)) { - std::cerr << "Failed to parse protobuf message." << std::endl; - return false; - } +bool ObjectDetection::LoadObjDetectdData(std::string inputFilePath) +{ + // Parse the file + pb_objdetect::ObjDetect objMessage; + std::fstream input(inputFilePath, std::ios::in | std::ios::binary); + if (!objMessage.ParseFromIstream(&input)) { + std::cerr << "Failed to parse protobuf message." << std::endl; + return false; + } - // Make sure classNames, detectionsData and trackedObjects are empty - classNames.clear(); - detectionsData.clear(); - trackedObjects.clear(); + // Clear out any old state + classNames.clear(); + detectionsData.clear(); + trackedObjects.clear(); + + // Seed colors for each class + std::srand(1); + for (int i = 0; i < objMessage.classnames_size(); ++i) { + classNames.push_back(objMessage.classnames(i)); + classesColor.push_back(cv::Scalar( + std::rand() % 205 + 50, + std::rand() % 205 + 50, + std::rand() % 205 + 50 + )); + } - // Seed to generate same random numbers - std::srand(1); - // Get all classes names and assign a color to them - for(int i = 0; i < objMessage.classnames_size(); i++) - { - classNames.push_back(objMessage.classnames(i)); - classesColor.push_back(cv::Scalar(std::rand()%205 + 50, std::rand()%205 + 50, std::rand()%205 + 50)); - } + // Walk every frame in the protobuf + for (size_t fi = 0; fi < objMessage.frame_size(); ++fi) { + const auto &pbFrame = objMessage.frame(fi); + size_t frameId = pbFrame.id(); + + // Buffers for DetectionData + std::vector classIds; + std::vector confidences; + std::vector> boxes; + std::vector objectIds; + + // For each bounding box in this frame + for (int di = 0; di < pbFrame.bounding_box_size(); ++di) { + const auto &b = pbFrame.bounding_box(di); + float x = b.x(), y = b.y(), w = b.w(), h = b.h(); + int classId = b.classid(); + float confidence= b.confidence(); + int objectId = b.objectid(); + + // Record for DetectionData + classIds.push_back(classId); + confidences.push_back(confidence); + boxes.emplace_back(x, y, w, h); + objectIds.push_back(objectId); + + // Either append to an existing TrackedObjectBBox… + auto it = trackedObjects.find(objectId); + if (it != trackedObjects.end()) { + it->second->AddBox(frameId, x + w/2, y + h/2, w, h, 0.0); + } + else { + // …or create a brand-new one + TrackedObjectBBox tmpObj( + (int)classesColor[classId][0], + (int)classesColor[classId][1], + (int)classesColor[classId][2], + /*alpha=*/0 + ); + tmpObj.stroke_alpha = Keyframe(1.0); + tmpObj.AddBox(frameId, x + w/2, y + h/2, w, h, 0.0); + + auto ptr = std::make_shared(tmpObj); + ptr->ParentClip(this->ParentClip()); + + // Prefix with effect UUID for a unique string ID + ptr->Id(this->Id() + "-" + std::to_string(objectId)); + trackedObjects.emplace(objectId, ptr); + } + } - // Iterate over all frames of the saved message - for (size_t i = 0; i < objMessage.frame_size(); i++) - { - // Create protobuf message reader - const pb_objdetect::Frame& pbFrameData = objMessage.frame(i); - - // Get frame Id - size_t id = pbFrameData.id(); - - // Load bounding box data - const google::protobuf::RepeatedPtrField &pBox = pbFrameData.bounding_box(); - - // Construct data vectors related to detections in the current frame - std::vector classIds; - std::vector confidences; - std::vector> boxes; - std::vector objectIds; - - // Iterate through the detected objects - for(int i = 0; i < pbFrameData.bounding_box_size(); i++) - { - // Get bounding box coordinates - float x = pBox.Get(i).x(); - float y = pBox.Get(i).y(); - float w = pBox.Get(i).w(); - float h = pBox.Get(i).h(); - // Get class Id (which will be assign to a class name) - int classId = pBox.Get(i).classid(); - // Get prediction confidence - float confidence = pBox.Get(i).confidence(); - - // Get the object Id - int objectId = pBox.Get(i).objectid(); - - // Search for the object id on trackedObjects map - auto trackedObject = trackedObjects.find(objectId); - // Check if object already exists on the map - if (trackedObject != trackedObjects.end()) - { - // Add a new BBox to it - trackedObject->second->AddBox(id, x+(w/2), y+(h/2), w, h, 0.0); - } - else - { - // There is no tracked object with that id, so insert a new one - TrackedObjectBBox trackedObj((int)classesColor[classId](0), (int)classesColor[classId](1), (int)classesColor[classId](2), (int)0); - trackedObj.stroke_alpha = Keyframe(1.0); - trackedObj.AddBox(id, x+(w/2), y+(h/2), w, h, 0.0); - - std::shared_ptr trackedObjPtr = std::make_shared(trackedObj); - ClipBase* parentClip = this->ParentClip(); - trackedObjPtr->ParentClip(parentClip); - - // Create a temp ID. This ID is necessary to initialize the object_id Json list - // this Id will be replaced by the one created in the UI - trackedObjPtr->Id(std::to_string(objectId)); - trackedObjects.insert({objectId, trackedObjPtr}); - } - - // Create OpenCV rectangle with the bouding box info - cv::Rect_ box(x, y, w, h); - - // Push back data into vectors - boxes.push_back(box); - classIds.push_back(classId); - confidences.push_back(confidence); - objectIds.push_back(objectId); - } + // Save the DetectionData for this frame + detectionsData[frameId] = DetectionData( + classIds, confidences, boxes, frameId, objectIds + ); + } - // Assign data to object detector map - detectionsData[id] = DetectionData(classIds, confidences, boxes, id, objectIds); - } + google::protobuf::ShutdownProtobufLibrary(); - // Delete all global objects allocated by libprotobuf. - google::protobuf::ShutdownProtobufLibrary(); + // Finally, pick a default selectedObjectIndex if we have any + if (!trackedObjects.empty()) { + selectedObjectIndex = trackedObjects.begin()->first; + } - return true; + return true; } // Get the indexes and IDs of all visible objects in the given frame @@ -377,70 +352,60 @@ void ObjectDetection::SetJson(const std::string value) { } // Load Json::Value into this object -void ObjectDetection::SetJsonValue(const Json::Value root) { - // Set parent data - EffectBase::SetJsonValue(root); - - // Set data from Json (if key is found) - if (!root["protobuf_data_path"].isNull() && protobuf_data_path.size() <= 1){ - protobuf_data_path = root["protobuf_data_path"].asString(); - - if(!LoadObjDetectdData(protobuf_data_path)){ - throw InvalidFile("Invalid protobuf data path", ""); - protobuf_data_path = ""; - } - } - - // Set the selected object index - if (!root["selected_object_index"].isNull()) - selectedObjectIndex = root["selected_object_index"].asInt(); - - if (!root["confidence_threshold"].isNull()) - confidence_threshold = root["confidence_threshold"].asFloat(); - - if (!root["display_box_text"].isNull()) - display_box_text.SetJsonValue(root["display_box_text"]); +void ObjectDetection::SetJsonValue(const Json::Value root) +{ + // Parent properties + EffectBase::SetJsonValue(root); + + // If a protobuf path is provided, load & prefix IDs + if (!root["protobuf_data_path"].isNull() && protobuf_data_path.empty()) { + protobuf_data_path = root["protobuf_data_path"].asString(); + if (!LoadObjDetectdData(protobuf_data_path)) { + throw InvalidFile("Invalid protobuf data path", ""); + } + } + // Selected index, thresholds, UI flags, filters, etc. + if (!root["selected_object_index"].isNull()) + selectedObjectIndex = root["selected_object_index"].asInt(); + if (!root["confidence_threshold"].isNull()) + confidence_threshold = root["confidence_threshold"].asFloat(); + if (!root["display_box_text"].isNull()) + display_box_text.SetJsonValue(root["display_box_text"]); if (!root["display_boxes"].isNull()) display_boxes.SetJsonValue(root["display_boxes"]); if (!root["class_filter"].isNull()) { class_filter = root["class_filter"].asString(); - - // Convert the class_filter to a QString - QString qClassFilter = QString::fromStdString(root["class_filter"].asString()); - - // Split the QString by commas and automatically trim each resulting string - QStringList classList = qClassFilter.split(','); - classList.removeAll(""); // Skip empty parts + QStringList parts = + QString::fromStdString(class_filter) + .split(',', Qt::SkipEmptyParts); display_classes.clear(); - - // Iterate over the QStringList and add each trimmed, non-empty string - for (const QString &classItem : classList) { - QString trimmedItem = classItem.trimmed().toLower(); - if (!trimmedItem.isEmpty()) { - display_classes.push_back(trimmedItem.toStdString()); - } + for (auto &p : parts) { + auto s = p.trimmed().toLower(); + if (!s.isEmpty()) display_classes.push_back(s.toStdString()); } } - if (!root["objects"].isNull()){ - for (auto const& trackedObject : trackedObjects){ - std::string obj_id = std::to_string(trackedObject.first); - if(!root["objects"][obj_id].isNull()){ - trackedObject.second->SetJsonValue(root["objects"][obj_id]); - } - } - } - - // Set the tracked object's ids - if (!root["objects_id"].isNull()){ - for (auto const& trackedObject : trackedObjects){ - Json::Value trackedObjectJSON; - trackedObjectJSON["box_id"] = root["objects_id"][trackedObject.first].asString(); - trackedObject.second->SetJsonValue(trackedObjectJSON); - } - } + // Apply any per-object overrides + if (!root["objects"].isNull()) { + for (auto &kv : trackedObjects) { + auto &idx = kv.first; + auto &obj = kv.second; + std::string key = std::to_string(idx); + if (!root["objects"][key].isNull()) + obj->SetJsonValue(root["objects"][key]); + } + } + if (!root["objects_id"].isNull()) { + for (auto &kv : trackedObjects) { + auto &idx = kv.first; + auto &obj = kv.second; + Json::Value tmp; + tmp["box_id"] = root["objects_id"][idx].asString(); + obj->SetJsonValue(tmp); + } + } } // Get all properties for a specific frame diff --git a/src/effects/ObjectDetection.h b/src/effects/ObjectDetection.h index d56eca721..1675bfca6 100644 --- a/src/effects/ObjectDetection.h +++ b/src/effects/ObjectDetection.h @@ -82,8 +82,6 @@ namespace openshot /// Index of the Tracked Object that was selected to modify it's properties int selectedObjectIndex; - ObjectDetection(std::string clipTrackerDataPath); - /// Default constructor ObjectDetection(); diff --git a/src/effects/Tracker.cpp b/src/effects/Tracker.cpp index c4e023e82..c9aaea210 100644 --- a/src/effects/Tracker.cpp +++ b/src/effects/Tracker.cpp @@ -32,38 +32,21 @@ using namespace std; using namespace openshot; using google::protobuf::util::TimeUtil; -/// Blank constructor, useful when using Json to load the effect properties -Tracker::Tracker(std::string clipTrackerDataPath) -{ - // Init effect properties - init_effect_details(); - // Instantiate a TrackedObjectBBox object and point to it - TrackedObjectBBox trackedDataObject; - trackedData = std::make_shared(trackedDataObject); - // Tries to load the tracked object's data from protobuf file - trackedData->LoadBoxData(clipTrackerDataPath); - ClipBase* parentClip = this->ParentClip(); - trackedData->ParentClip(parentClip); - trackedData->Id(std::to_string(0)); - // Insert TrackedObject with index 0 to the trackedObjects map - trackedObjects.insert({0, trackedData}); -} // Default constructor Tracker::Tracker() { - // Init effect properties + // Initialize effect metadata init_effect_details(); - // Instantiate a TrackedObjectBBox object and point to it - TrackedObjectBBox trackedDataObject; - trackedData = std::make_shared(trackedDataObject); - ClipBase* parentClip = this->ParentClip(); - trackedData->ParentClip(parentClip); - trackedData->Id(std::to_string(0)); - // Insert TrackedObject with index 0 to the trackedObjects map - trackedObjects.insert({0, trackedData}); -} + // Create a placeholder object so we always have index 0 available + trackedData = std::make_shared(); + trackedData->ParentClip(this->ParentClip()); + + // Seed our map with a single entry at index 0 + trackedObjects.clear(); + trackedObjects.emplace(0, trackedData); +} // Init effect settings void Tracker::init_effect_details() @@ -84,73 +67,80 @@ void Tracker::init_effect_details() // This method is required for all derived classes of EffectBase, and returns a // modified openshot::Frame object -std::shared_ptr Tracker::GetFrame(std::shared_ptr frame, int64_t frame_number) { - // Get the frame's QImage - std::shared_ptr frame_image = frame->GetImage(); - - // Check if frame isn't NULL - if(frame_image && !frame_image->isNull() && - trackedData->Contains(frame_number) && - trackedData->visible.GetValue(frame_number) == 1) { - QPainter painter(frame_image.get()); - - // Get the bounding-box of the given frame - BBox fd = trackedData->GetBox(frame_number); - - // Create a QRectF for the bounding box - QRectF boxRect((fd.cx - fd.width / 2) * frame_image->width(), - (fd.cy - fd.height / 2) * frame_image->height(), - fd.width * frame_image->width(), - fd.height * frame_image->height()); - - // Check if track data exists for the requested frame - if (trackedData->draw_box.GetValue(frame_number) == 1) { - painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); - - // Get trackedObjectBox keyframes - std::vector stroke_rgba = trackedData->stroke.GetColorRGBA(frame_number); - int stroke_width = trackedData->stroke_width.GetValue(frame_number); - float stroke_alpha = trackedData->stroke_alpha.GetValue(frame_number); - std::vector bg_rgba = trackedData->background.GetColorRGBA(frame_number); - float bg_alpha = trackedData->background_alpha.GetValue(frame_number); - float bg_corner = trackedData->background_corner.GetValue(frame_number); - - // Set the pen for the border - QPen pen(QColor(stroke_rgba[0], stroke_rgba[1], stroke_rgba[2], 255 * stroke_alpha)); - pen.setWidth(stroke_width); - painter.setPen(pen); - - // Set the brush for the background - QBrush brush(QColor(bg_rgba[0], bg_rgba[1], bg_rgba[2], 255 * bg_alpha)); - painter.setBrush(brush); - - // Draw the rounded rectangle - painter.drawRoundedRect(boxRect, bg_corner, bg_corner); - } - - painter.end(); - } - - // No need to set the image back to the frame, as we directly modified the frame's QImage - return frame; +std::shared_ptr Tracker::GetFrame(std::shared_ptr frame, int64_t frame_number) +{ + // Sanity‐check + if (!frame) return frame; + auto frame_image = frame->GetImage(); + if (!frame_image || frame_image->isNull()) return frame; + if (!trackedData) return frame; + + // 2) Only proceed if we actually have a box and it's visible + if (!trackedData->Contains(frame_number) || + trackedData->visible.GetValue(frame_number) != 1) + return frame; + + QPainter painter(frame_image.get()); + painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); + + // Draw the box + BBox fd = trackedData->GetBox(frame_number); + QRectF boxRect( + (fd.cx - fd.width/2) * frame_image->width(), + (fd.cy - fd.height/2) * frame_image->height(), + fd.width * frame_image->width(), + fd.height * frame_image->height() + ); + + if (trackedData->draw_box.GetValue(frame_number) == 1) + { + auto stroke_rgba = trackedData->stroke.GetColorRGBA(frame_number); + int stroke_width = trackedData->stroke_width.GetValue(frame_number); + float stroke_alpha = trackedData->stroke_alpha.GetValue(frame_number); + auto bg_rgba = trackedData->background.GetColorRGBA(frame_number); + float bg_alpha = trackedData->background_alpha.GetValue(frame_number); + float bg_corner = trackedData->background_corner.GetValue(frame_number); + + QPen pen(QColor( + stroke_rgba[0], stroke_rgba[1], stroke_rgba[2], + int(255 * stroke_alpha) + )); + pen.setWidth(stroke_width); + painter.setPen(pen); + + QBrush brush(QColor( + bg_rgba[0], bg_rgba[1], bg_rgba[2], + int(255 * bg_alpha) + )); + painter.setBrush(brush); + + painter.drawRoundedRect(boxRect, bg_corner, bg_corner); + } + + painter.end(); + return frame; } // Get the indexes and IDs of all visible objects in the given frame -std::string Tracker::GetVisibleObjects(int64_t frame_number) const{ - - // Initialize the JSON objects +std::string Tracker::GetVisibleObjects(int64_t frame_number) const +{ Json::Value root; root["visible_objects_index"] = Json::Value(Json::arrayValue); - root["visible_objects_id"] = Json::Value(Json::arrayValue); - - // Iterate through the tracked objects - for (const auto& trackedObject : trackedObjects){ - // Get the tracked object JSON properties for this frame - Json::Value trackedObjectJSON = trackedObject.second->PropertiesJSON(frame_number); - if (trackedObjectJSON["visible"]["value"].asBool()){ - // Save the object's index and ID if it's visible in this frame - root["visible_objects_index"].append(trackedObject.first); - root["visible_objects_id"].append(trackedObject.second->Id()); + root["visible_objects_id"] = Json::Value(Json::arrayValue); + + if (trackedObjects.empty()) + return root.toStyledString(); + + for (auto const& kv : trackedObjects) { + auto ptr = kv.second; + if (!ptr) continue; + + // Directly get the Json::Value for this object's properties + Json::Value propsJson = ptr->PropertiesJSON(frame_number); + + if (propsJson["visible"]["value"].asBool()) { + root["visible_objects_index"].append(kv.first); + root["visible_objects_id"].append(ptr->Id()); } } @@ -214,51 +204,53 @@ void Tracker::SetJsonValue(const Json::Value root) { // Set parent data EffectBase::SetJsonValue(root); - if (!root["BaseFPS"].isNull() && root["BaseFPS"].isObject()) - { + if (!root["BaseFPS"].isNull()) { if (!root["BaseFPS"]["num"].isNull()) - { - BaseFPS.num = (int) root["BaseFPS"]["num"].asInt(); - } + BaseFPS.num = root["BaseFPS"]["num"].asInt(); if (!root["BaseFPS"]["den"].isNull()) - { - BaseFPS.den = (int) root["BaseFPS"]["den"].asInt(); - } + BaseFPS.den = root["BaseFPS"]["den"].asInt(); } - if (!root["TimeScale"].isNull()) - TimeScale = (double) root["TimeScale"].asDouble(); + if (!root["TimeScale"].isNull()) { + TimeScale = root["TimeScale"].asDouble(); + } - // Set data from Json (if key is found) - if (!root["protobuf_data_path"].isNull() && protobuf_data_path.size() <= 1) - { + if (!root["protobuf_data_path"].isNull() && protobuf_data_path.empty()) { protobuf_data_path = root["protobuf_data_path"].asString(); - if(!trackedData->LoadBoxData(protobuf_data_path)) - { + if (!trackedData->LoadBoxData(protobuf_data_path)) { std::clog << "Invalid protobuf data path " << protobuf_data_path << '\n'; - protobuf_data_path = ""; + protobuf_data_path.clear(); + } + else { + // prefix “-” for each entry + for (auto& kv : trackedObjects) { + auto idx = kv.first; + auto ptr = kv.second; + if (ptr) { + ptr->Id(this->Id() + "-" + std::to_string(idx)); + } + } } } - if (!root["objects"].isNull()){ - for (auto const& trackedObject : trackedObjects){ - std::string obj_id = std::to_string(trackedObject.first); - if(!root["objects"][obj_id].isNull()){ - trackedObject.second->SetJsonValue(root["objects"][obj_id]); + // then any per-object JSON overrides… + if (!root["objects"].isNull()) { + for (auto& kv : trackedObjects) { + std::string key = std::to_string(kv.first); + if (!root["objects"][key].isNull()) { + kv.second->SetJsonValue(root["objects"][key]); } } } // Set the tracked object's ids - if (!root["objects_id"].isNull()){ - for (auto const& trackedObject : trackedObjects){ - Json::Value trackedObjectJSON; - trackedObjectJSON["box_id"] = root["objects_id"][trackedObject.first].asString(); - trackedObject.second->SetJsonValue(trackedObjectJSON); + if (!root["objects_id"].isNull()) { + for (auto& kv : trackedObjects) { + Json::Value tmp; + tmp["box_id"] = root["objects_id"][kv.first].asString(); + kv.second->SetJsonValue(tmp); } } - - return; } // Get all properties for a specific frame diff --git a/src/effects/Tracker.h b/src/effects/Tracker.h index d05b72a1f..b34c376f6 100644 --- a/src/effects/Tracker.h +++ b/src/effects/Tracker.h @@ -54,8 +54,6 @@ namespace openshot /// Default constructor Tracker(); - Tracker(std::string clipTrackerDataPath); - /// @brief Apply this effect to an openshot::Frame /// /// @returns The modified openshot::Frame object From 22cd56331f2328edf522868db57cdfc7dd38f56e Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 17 Jun 2025 19:06:48 -0500 Subject: [PATCH 05/41] Removing SkipEmptyParts from modified ObjectDetection.cpp code (old QT build server doesn't have this) --- src/effects/ObjectDetection.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/effects/ObjectDetection.cpp b/src/effects/ObjectDetection.cpp index 9b3a5b984..7e6c95aa0 100644 --- a/src/effects/ObjectDetection.cpp +++ b/src/effects/ObjectDetection.cpp @@ -375,17 +375,17 @@ void ObjectDetection::SetJsonValue(const Json::Value root) if (!root["display_boxes"].isNull()) display_boxes.SetJsonValue(root["display_boxes"]); - if (!root["class_filter"].isNull()) { - class_filter = root["class_filter"].asString(); - QStringList parts = - QString::fromStdString(class_filter) - .split(',', Qt::SkipEmptyParts); - display_classes.clear(); - for (auto &p : parts) { - auto s = p.trimmed().toLower(); - if (!s.isEmpty()) display_classes.push_back(s.toStdString()); - } - } + if (!root["class_filter"].isNull()) { + class_filter = root["class_filter"].asString(); + QStringList parts = QString::fromStdString(class_filter).split(','); + display_classes.clear(); + for (auto &p : parts) { + auto s = p.trimmed().toLower(); + if (!s.isEmpty()) { + display_classes.push_back(s.toStdString()); + } + } + } // Apply any per-object overrides if (!root["objects"].isNull()) { From 9cbfc80e2c3e5f5e24f831016df6c21df8c12ed7 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 8 Jul 2025 00:21:57 -0500 Subject: [PATCH 06/41] Fixing Tracker and Object Detection effect to not crash when camera quickly makes tracked object go offscreen (go pro mp4 from raffi) --- src/CVTracker.cpp | 49 +++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/CVTracker.cpp b/src/CVTracker.cpp index fb8f92092..f243fcfb5 100644 --- a/src/CVTracker.cpp +++ b/src/CVTracker.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include @@ -26,6 +27,15 @@ using namespace openshot; using google::protobuf::util::TimeUtil; +// Clamp a rectangle to image bounds and ensure a minimal size +static inline void clampRect(cv::Rect2d &r, int width, int height) +{ + r.x = std::clamp(r.x, 0.0, double(width - 1)); + r.y = std::clamp(r.y, 0.0, double(height - 1)); + r.width = std::clamp(r.width, 1.0, double(width - r.x)); + r.height = std::clamp(r.height, 1.0, double(height - r.y)); +} + // Constructor CVTracker::CVTracker(std::string processInfoJson, ProcessingController &processingController) : processingController(&processingController), json_interval(false){ @@ -130,9 +140,7 @@ bool CVTracker::initTracker(cv::Mat &frame, size_t frameId) } // Clamp to frame bounds - bbox &= cv::Rect2d(0, 0, frame.cols, frame.rows); - if (bbox.width <= 0) bbox.width = 1; - if (bbox.height <= 0) bbox.height = 1; + clampRect(bbox, frame.cols, frame.rows); // Initialize tracker tracker->init(frame, bbox); @@ -262,21 +270,30 @@ bool CVTracker::trackFrame(cv::Mat &frame, size_t frameId) cand.y = smoothC_y - cand.height * 0.5; } + + // Candidate box may now lie outside frame; ROI for KLT is clamped below // Re-seed KLT features { - cv::Rect roi( - int(std::max(0., cand.x)), - int(std::max(0., cand.y)), - int(std::min(cand.width, double(W - cand.x))), - int(std::min(cand.height, double(H - cand.y))) - ); - cv::goodFeaturesToTrack( - gray(roi), prevPts, - kltMaxCorners, kltQualityLevel, - kltMinDist, cv::Mat(), kltBlockSize - ); - for (auto &pt : prevPts) - pt += cv::Point2f(float(roi.x), float(roi.y)); + // Clamp ROI to frame bounds and avoid negative width/height + int roiX = int(std::clamp(cand.x, 0.0, double(W - 1))); + int roiY = int(std::clamp(cand.y, 0.0, double(H - 1))); + int roiW = int(std::min(cand.width, double(W - roiX))); + int roiH = int(std::min(cand.height, double(H - roiY))); + roiW = std::max(0, roiW); + roiH = std::max(0, roiH); + + if (roiW > 0 && roiH > 0) { + cv::Rect roi(roiX, roiY, roiW, roiH); + cv::goodFeaturesToTrack( + gray(roi), prevPts, + kltMaxCorners, kltQualityLevel, + kltMinDist, cv::Mat(), kltBlockSize + ); + for (auto &pt : prevPts) + pt += cv::Point2f(float(roi.x), float(roi.y)); + } else { + prevPts.clear(); + } } // Commit state From 713cf39c4fd44ff9d81e5abd23198185b52d5bd1 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 8 Jul 2025 15:15:32 -0500 Subject: [PATCH 07/41] Updating godot git ref --- external/godot-cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/godot-cpp b/external/godot-cpp index 6388e26dd..d502d8e8a 160000 --- a/external/godot-cpp +++ b/external/godot-cpp @@ -1 +1 @@ -Subproject commit 6388e26dd8a42071f65f764a3ef3d9523dda3d6e +Subproject commit d502d8e8aae35248bad69b9f40b98150ab694774 From 6cea273b77d788b9de5f4c4b04e656c25a4bb13d Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 8 Jul 2025 15:19:17 -0500 Subject: [PATCH 08/41] Fix timeline cache when updating Clips with ApplyJsonDiff (old and new position) --- src/Timeline.cpp | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Timeline.cpp b/src/Timeline.cpp index 9f8efb58b..ec6d33b2d 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -1431,17 +1431,26 @@ void Timeline::apply_json_to_clips(Json::Value change) { // Update existing clip if (existing_clip) { + // Calculate start and end frames prior to the update + int64_t old_starting_frame = (existing_clip->Position() * info.fps.ToDouble()) + 1; + int64_t old_ending_frame = ((existing_clip->Position() + existing_clip->Duration()) * info.fps.ToDouble()) + 1; + // Update clip properties from JSON existing_clip->SetJsonValue(change["value"]); - // Calculate start and end frames that this impacts, and remove those frames from the cache - int64_t old_starting_frame = (existing_clip->Position() * info.fps.ToDouble()) + 1; - int64_t old_ending_frame = ((existing_clip->Position() + existing_clip->Duration()) * info.fps.ToDouble()) + 1; + // Calculate new start and end frames after the update + int64_t new_starting_frame = (existing_clip->Position() * info.fps.ToDouble()) + 1; + int64_t new_ending_frame = ((existing_clip->Position() + existing_clip->Duration()) * info.fps.ToDouble()) + 1; + + // Remove both the old and new ranges from the timeline cache final_cache->Remove(old_starting_frame - 8, old_ending_frame + 8); + final_cache->Remove(new_starting_frame - 8, new_ending_frame + 8); // Remove cache on clip's Reader (if found) - if (existing_clip->Reader() && existing_clip->Reader()->GetCache()) + if (existing_clip->Reader() && existing_clip->Reader()->GetCache()) { existing_clip->Reader()->GetCache()->Remove(old_starting_frame - 8, old_ending_frame + 8); + existing_clip->Reader()->GetCache()->Remove(new_starting_frame - 8, new_ending_frame + 8); + } // Apply framemapper (or update existing framemapper) if (auto_map_clips) { @@ -1464,13 +1473,6 @@ void Timeline::apply_json_to_clips(Json::Value change) { } - // Calculate start and end frames that this impacts, and remove those frames from the cache - if (!change["value"].isArray() && !change["value"]["position"].isNull()) { - int64_t new_starting_frame = (change["value"]["position"].asDouble() * info.fps.ToDouble()) + 1; - int64_t new_ending_frame = ((change["value"]["position"].asDouble() + change["value"]["end"].asDouble() - change["value"]["start"].asDouble()) * info.fps.ToDouble()) + 1; - final_cache->Remove(new_starting_frame - 8, new_ending_frame + 8); - } - // Re-Sort Clips (since they likely changed) sort_clips(); } From e43f87552d41366ca534ef0248cbd787ce95c08b Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 11 Aug 2025 14:52:24 -0500 Subject: [PATCH 09/41] Small refactor to assign Clip and Effect ids in base class --- src/ClipBase.h | 3 +- src/IdGenerator.h | 36 ++++++++++++ src/effects/ObjectDetection.cpp | 101 ++++++++++++++++---------------- src/effects/Tracker.cpp | 13 +++- 4 files changed, 100 insertions(+), 53 deletions(-) create mode 100644 src/IdGenerator.h diff --git a/src/ClipBase.h b/src/ClipBase.h index 4ba6b2f47..732160c16 100644 --- a/src/ClipBase.h +++ b/src/ClipBase.h @@ -16,8 +16,8 @@ #include #include "CacheMemory.h" #include "Frame.h" -#include "Point.h" #include "KeyFrame.h" +#include "IdGenerator.h" #include "Json.h" #include "TimelineBase.h" @@ -48,6 +48,7 @@ namespace openshot { public: /// Constructor for the base clip ClipBase() : + id(IdGenerator::Generate()), position(0.0), layer(0), start(0.0), diff --git a/src/IdGenerator.h b/src/IdGenerator.h new file mode 100644 index 000000000..85a10dd51 --- /dev/null +++ b/src/IdGenerator.h @@ -0,0 +1,36 @@ +/* + * @file + * @brief Header file for generating random identifier strings + */ + +// Copyright (c) 2008-2025 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef OPENSHOT_ID_GENERATOR_H +#define OPENSHOT_ID_GENERATOR_H + +#include +#include + +namespace openshot { + + class IdGenerator { + public: + static inline std::string Generate(int length = 8) { + static const char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dist(0, static_cast(sizeof(charset) - 2)); + + std::string result; + result.reserve(length); + for (int i = 0; i < length; ++i) + result += charset[dist(gen)]; + return result; +} +}; + +} // namespace openshot + +#endif // OPENSHOT_ID_GENERATOR_H diff --git a/src/effects/ObjectDetection.cpp b/src/effects/ObjectDetection.cpp index 7e6c95aa0..2d0971608 100644 --- a/src/effects/ObjectDetection.cpp +++ b/src/effects/ObjectDetection.cpp @@ -221,16 +221,19 @@ bool ObjectDetection::LoadObjDetectdData(std::string inputFilePath) tmpObj.stroke_alpha = Keyframe(1.0); tmpObj.AddBox(frameId, x + w/2, y + h/2, w, h, 0.0); - auto ptr = std::make_shared(tmpObj); - ptr->ParentClip(this->ParentClip()); - - // Prefix with effect UUID for a unique string ID - ptr->Id(this->Id() + "-" + std::to_string(objectId)); - trackedObjects.emplace(objectId, ptr); - } - } + auto ptr = std::make_shared(tmpObj); + ptr->ParentClip(this->ParentClip()); + + // Prefix with effect UUID for a unique string ID + std::string prefix = this->Id(); + if (!prefix.empty()) + prefix += "-"; + ptr->Id(prefix + std::to_string(objectId)); + trackedObjects.emplace(objectId, ptr); + } + } - // Save the DetectionData for this frame + // Save the DetectionData for this frame detectionsData[frameId] = DetectionData( classIds, confidences, boxes, frameId, objectIds ); @@ -354,26 +357,26 @@ void ObjectDetection::SetJson(const std::string value) { // Load Json::Value into this object void ObjectDetection::SetJsonValue(const Json::Value root) { - // Parent properties - EffectBase::SetJsonValue(root); - - // If a protobuf path is provided, load & prefix IDs - if (!root["protobuf_data_path"].isNull() && protobuf_data_path.empty()) { - protobuf_data_path = root["protobuf_data_path"].asString(); - if (!LoadObjDetectdData(protobuf_data_path)) { - throw InvalidFile("Invalid protobuf data path", ""); - } - } + // Parent properties + EffectBase::SetJsonValue(root); + + // If a protobuf path is provided, load & prefix IDs + if (!root["protobuf_data_path"].isNull() && protobuf_data_path.empty()) { + protobuf_data_path = root["protobuf_data_path"].asString(); + if (!LoadObjDetectdData(protobuf_data_path)) { + throw InvalidFile("Invalid protobuf data path", ""); + } + } - // Selected index, thresholds, UI flags, filters, etc. - if (!root["selected_object_index"].isNull()) - selectedObjectIndex = root["selected_object_index"].asInt(); - if (!root["confidence_threshold"].isNull()) - confidence_threshold = root["confidence_threshold"].asFloat(); - if (!root["display_box_text"].isNull()) - display_box_text.SetJsonValue(root["display_box_text"]); - if (!root["display_boxes"].isNull()) - display_boxes.SetJsonValue(root["display_boxes"]); + // Selected index, thresholds, UI flags, filters, etc. + if (!root["selected_object_index"].isNull()) + selectedObjectIndex = root["selected_object_index"].asInt(); + if (!root["confidence_threshold"].isNull()) + confidence_threshold = root["confidence_threshold"].asFloat(); + if (!root["display_box_text"].isNull()) + display_box_text.SetJsonValue(root["display_box_text"]); + if (!root["display_boxes"].isNull()) + display_boxes.SetJsonValue(root["display_boxes"]); if (!root["class_filter"].isNull()) { class_filter = root["class_filter"].asString(); @@ -388,24 +391,24 @@ void ObjectDetection::SetJsonValue(const Json::Value root) } // Apply any per-object overrides - if (!root["objects"].isNull()) { - for (auto &kv : trackedObjects) { - auto &idx = kv.first; - auto &obj = kv.second; - std::string key = std::to_string(idx); - if (!root["objects"][key].isNull()) - obj->SetJsonValue(root["objects"][key]); - } - } - if (!root["objects_id"].isNull()) { - for (auto &kv : trackedObjects) { - auto &idx = kv.first; - auto &obj = kv.second; - Json::Value tmp; - tmp["box_id"] = root["objects_id"][idx].asString(); - obj->SetJsonValue(tmp); - } - } + if (!root["objects"].isNull()) { + for (auto &kv : trackedObjects) { + auto &idx = kv.first; + auto &obj = kv.second; + std::string key = std::to_string(idx); + if (!root["objects"][key].isNull()) + obj->SetJsonValue(root["objects"][key]); + } + } + if (!root["objects_id"].isNull()) { + for (auto &kv : trackedObjects) { + auto &idx = kv.first; + auto &obj = kv.second; + Json::Value tmp; + tmp["box_id"] = root["objects_id"][idx].asString(); + obj->SetJsonValue(tmp); + } + } } // Get all properties for a specific frame @@ -433,9 +436,9 @@ std::string ObjectDetection::PropertiesJSON(int64_t requested_frame) const { root["display_box_text"]["choices"].append(add_property_choice_json("Yes", true, display_box_text.GetValue(requested_frame))); root["display_box_text"]["choices"].append(add_property_choice_json("No", false, display_box_text.GetValue(requested_frame))); - root["display_boxes"] = add_property_json("Draw All Boxes", display_boxes.GetValue(requested_frame), "int", "", &display_boxes, 0, 1, false, requested_frame); - root["display_boxes"]["choices"].append(add_property_choice_json("Yes", true, display_boxes.GetValue(requested_frame))); - root["display_boxes"]["choices"].append(add_property_choice_json("No", false, display_boxes.GetValue(requested_frame))); + root["display_boxes"] = add_property_json("Draw All Boxes", display_boxes.GetValue(requested_frame), "int", "", &display_boxes, 0, 1, false, requested_frame); + root["display_boxes"]["choices"].append(add_property_choice_json("Yes", true, display_boxes.GetValue(requested_frame))); + root["display_boxes"]["choices"].append(add_property_choice_json("No", false, display_boxes.GetValue(requested_frame))); // Return formatted string return root.toStyledString(); diff --git a/src/effects/Tracker.cpp b/src/effects/Tracker.cpp index c9aaea210..6c6a2f501 100644 --- a/src/effects/Tracker.cpp +++ b/src/effects/Tracker.cpp @@ -46,6 +46,10 @@ Tracker::Tracker() // Seed our map with a single entry at index 0 trackedObjects.clear(); trackedObjects.emplace(0, trackedData); + + // Assign ID to the placeholder object + if (trackedData) + trackedData->Id(Id() + "-0"); } // Init effect settings @@ -222,18 +226,21 @@ void Tracker::SetJsonValue(const Json::Value root) { protobuf_data_path.clear(); } else { - // prefix “-” for each entry + // prefix "-" for each entry for (auto& kv : trackedObjects) { auto idx = kv.first; auto ptr = kv.second; if (ptr) { - ptr->Id(this->Id() + "-" + std::to_string(idx)); + std::string prefix = this->Id(); + if (!prefix.empty()) + prefix += "-"; + ptr->Id(prefix + std::to_string(idx)); } } } } - // then any per-object JSON overrides… + // then any per-object JSON overrides... if (!root["objects"].isNull()) { for (auto& kv : trackedObjects) { std::string key = std::to_string(kv.first); From 4613b5239fccd7284bd6b873ce223245a2acfbb1 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 11 Aug 2025 15:38:09 -0500 Subject: [PATCH 10/41] Fixing logic to set Tracker JSON (Tracker was not updating the box values - due to ID mismatches) --- src/effects/Tracker.cpp | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/effects/Tracker.cpp b/src/effects/Tracker.cpp index 6c6a2f501..d485f9036 100644 --- a/src/effects/Tracker.cpp +++ b/src/effects/Tracker.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "effects/Tracker.h" #include "Exceptions.h" @@ -242,20 +243,43 @@ void Tracker::SetJsonValue(const Json::Value root) { // then any per-object JSON overrides... if (!root["objects"].isNull()) { - for (auto& kv : trackedObjects) { - std::string key = std::to_string(kv.first); - if (!root["objects"][key].isNull()) { - kv.second->SetJsonValue(root["objects"][key]); + // Iterate over the supplied objects (indexed by id or position) + const auto memberNames = root["objects"].getMemberNames(); + for (const auto& name : memberNames) + { + // Determine the numeric index of this object + int index = -1; + bool numeric_key = std::all_of(name.begin(), name.end(), ::isdigit); + if (numeric_key) { + index = std::stoi(name); + } + else + { + size_t pos = name.find_last_of('-'); + if (pos != std::string::npos) { + try { + index = std::stoi(name.substr(pos + 1)); + } catch (...) { + index = -1; + } + } + } + + auto obj_it = trackedObjects.find(index); + if (obj_it != trackedObjects.end() && obj_it->second) { + // Update object id if provided as a non-numeric key + if (!numeric_key) + obj_it->second->Id(name); + obj_it->second->SetJsonValue(root["objects"][name]); } } } - // Set the tracked object's ids + // Set the tracked object's ids (legacy format) if (!root["objects_id"].isNull()) { for (auto& kv : trackedObjects) { - Json::Value tmp; - tmp["box_id"] = root["objects_id"][kv.first].asString(); - kv.second->SetJsonValue(tmp); + if (!root["objects_id"][kv.first].isNull()) + kv.second->Id(root["objects_id"][kv.first].asString()); } } } From 981e18de956c7e00c19c1d531bae9753a729f5f9 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 11 Aug 2025 16:22:54 -0500 Subject: [PATCH 11/41] Fixing logic to set ObjectDetection JSON (Detector was not updating the box values - due to ID mismatches) --- src/effects/ObjectDetection.cpp | 52 +++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/effects/ObjectDetection.cpp b/src/effects/ObjectDetection.cpp index 2d0971608..df60afd7e 100644 --- a/src/effects/ObjectDetection.cpp +++ b/src/effects/ObjectDetection.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "effects/ObjectDetection.h" #include "effects/Tracker.h" @@ -390,24 +391,45 @@ void ObjectDetection::SetJsonValue(const Json::Value root) } } - // Apply any per-object overrides + // Apply any per-object overrides if (!root["objects"].isNull()) { - for (auto &kv : trackedObjects) { - auto &idx = kv.first; - auto &obj = kv.second; - std::string key = std::to_string(idx); - if (!root["objects"][key].isNull()) - obj->SetJsonValue(root["objects"][key]); - } + // Iterate over the supplied objects (indexed by id or position) + const auto memberNames = root["objects"].getMemberNames(); + for (const auto& name : memberNames) + { + // Determine the numeric index of this object + int index = -1; + bool numeric_key = std::all_of(name.begin(), name.end(), ::isdigit); + if (numeric_key) { + index = std::stoi(name); + } + else + { + size_t pos = name.find_last_of('-'); + if (pos != std::string::npos) { + try { + index = std::stoi(name.substr(pos + 1)); + } catch (...) { + index = -1; + } + } + } + + auto obj_it = trackedObjects.find(index); + if (obj_it != trackedObjects.end() && obj_it->second) { + // Update object id if provided as a non-numeric key + if (!numeric_key) + obj_it->second->Id(name); + obj_it->second->SetJsonValue(root["objects"][name]); + } + } } + // Set the tracked object's ids (legacy format) if (!root["objects_id"].isNull()) { - for (auto &kv : trackedObjects) { - auto &idx = kv.first; - auto &obj = kv.second; - Json::Value tmp; - tmp["box_id"] = root["objects_id"][idx].asString(); - obj->SetJsonValue(tmp); - } + for (auto& kv : trackedObjects) { + if (!root["objects_id"][kv.first].isNull()) + kv.second->Id(root["objects_id"][kv.first].asString()); + } } } From 523fb5acf901f9ba24db03576d1639402a4d9f20 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 11 Aug 2025 23:35:09 -0500 Subject: [PATCH 12/41] Massive improvement to object detection sort logic, to keep IDs more stable and less jitter / ID jumping between detections. --- src/sort_filter/KalmanTracker.cpp | 23 +++++---- src/sort_filter/sort.cpp | 79 ++++++++++++++++++++++++++----- src/sort_filter/sort.hpp | 16 +++++-- 3 files changed, 93 insertions(+), 25 deletions(-) diff --git a/src/sort_filter/KalmanTracker.cpp b/src/sort_filter/KalmanTracker.cpp index 083f3b1ed..1a50e4c3b 100644 --- a/src/sort_filter/KalmanTracker.cpp +++ b/src/sort_filter/KalmanTracker.cpp @@ -15,23 +15,26 @@ using namespace cv; void KalmanTracker::init_kf( StateType stateMat) { - int stateNum = 7; + int stateNum = 8; int measureNum = 4; kf = KalmanFilter(stateNum, measureNum, 0); measurement = Mat::zeros(measureNum, 1, CV_32F); - kf.transitionMatrix = (Mat_(7, 7) << 1, 0, 0, 0, 1, 0, 0, + kf.transitionMatrix = (Mat_(8, 8) << 1, 0, 0, 0, 1, 0, 0, 0, - 0, 1, 0, 0, 0, 1, 0, - 0, 0, 1, 0, 0, 0, 1, - 0, 0, 0, 1, 0, 0, 0, - 0, 0, 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, 1, 0, - 0, 0, 0, 0, 0, 0, 1); + 0, 1, 0, 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, 0, 1, 0, + 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 1); setIdentity(kf.measurementMatrix); setIdentity(kf.processNoiseCov, Scalar::all(1e-1)); + kf.processNoiseCov.at(2, 2) = 1e0; // higher noise for area (s) to adapt to size changes + kf.processNoiseCov.at(3, 3) = 1e0; // higher noise for aspect ratio (r) setIdentity(kf.measurementNoiseCov, Scalar::all(1e-4)); setIdentity(kf.errorCovPost, Scalar::all(1e-2)); @@ -40,6 +43,10 @@ void KalmanTracker::init_kf( kf.statePost.at(1, 0) = stateMat.y + stateMat.height / 2; kf.statePost.at(2, 0) = stateMat.area(); kf.statePost.at(3, 0) = stateMat.width / stateMat.height; + kf.statePost.at(4, 0) = 0.0f; + kf.statePost.at(5, 0) = 0.0f; + kf.statePost.at(6, 0) = 0.0f; + kf.statePost.at(7, 0) = 0.0f; } // Predict the estimated bounding box. diff --git a/src/sort_filter/sort.cpp b/src/sort_filter/sort.cpp index 611eeea42..78ae24320 100644 --- a/src/sort_filter/sort.cpp +++ b/src/sort_filter/sort.cpp @@ -7,10 +7,15 @@ using namespace std; // Constructor -SortTracker::SortTracker(int max_age, int min_hits) +SortTracker::SortTracker(int max_age, int min_hits, int max_missed, double min_iou, double nms_iou_thresh, double min_conf) { _min_hits = min_hits; _max_age = max_age; + _max_missed = max_missed; + _min_iou = min_iou; + _nms_iou_thresh = nms_iou_thresh; + _min_conf = min_conf; + _next_id = 0; alive_tracker = true; } @@ -42,6 +47,40 @@ double SortTracker::GetCentroidsDistance( return distance; } +// Function to apply NMS on detections +void apply_nms(vector& detections, double nms_iou_thresh) { + if (detections.empty()) return; + + // Sort detections by confidence descending + std::sort(detections.begin(), detections.end(), [](const TrackingBox& a, const TrackingBox& b) { + return a.confidence > b.confidence; + }); + + vector suppressed(detections.size(), false); + + for (size_t i = 0; i < detections.size(); ++i) { + if (suppressed[i]) continue; + + for (size_t j = i + 1; j < detections.size(); ++j) { + if (suppressed[j]) continue; + + if (detections[i].classId == detections[j].classId && + SortTracker::GetIOU(detections[i].box, detections[j].box) > nms_iou_thresh) { + suppressed[j] = true; + } + } + } + + // Remove suppressed detections + vector filtered; + for (size_t i = 0; i < detections.size(); ++i) { + if (!suppressed[i]) { + filtered.push_back(detections[i]); + } + } + detections = filtered; +} + void SortTracker::update(vector detections_cv, int frame_count, double image_diagonal, std::vector confidences, std::vector classIds) { vector detections; @@ -51,6 +90,8 @@ void SortTracker::update(vector detections_cv, int frame_count, double // initialize kalman trackers using first detections. for (unsigned int i = 0; i < detections_cv.size(); i++) { + if (confidences[i] < _min_conf) continue; // filter low conf + TrackingBox tb; tb.box = cv::Rect_(detections_cv[i]); @@ -58,7 +99,7 @@ void SortTracker::update(vector detections_cv, int frame_count, double tb.confidence = confidences[i]; detections.push_back(tb); - KalmanTracker trk = KalmanTracker(detections[i].box, detections[i].confidence, detections[i].classId, i); + KalmanTracker trk = KalmanTracker(detections.back().box, detections.back().confidence, detections.back().classId, _next_id++); trackers.push_back(trk); } return; @@ -67,12 +108,18 @@ void SortTracker::update(vector detections_cv, int frame_count, double { for (unsigned int i = 0; i < detections_cv.size(); i++) { + if (confidences[i] < _min_conf) continue; // filter low conf + TrackingBox tb; tb.box = cv::Rect_(detections_cv[i]); tb.classId = classIds[i]; tb.confidence = confidences[i]; detections.push_back(tb); } + + // Apply NMS to remove duplicates + apply_nms(detections, _nms_iou_thresh); + for (auto it = frameTrackingResult.begin(); it != frameTrackingResult.end(); it++) { int frame_age = frame_count - it->frame; @@ -101,22 +148,29 @@ void SortTracker::update(vector detections_cv, int frame_count, double trkNum = predictedBoxes.size(); detNum = detections.size(); - centroid_dist_matrix.clear(); - centroid_dist_matrix.resize(trkNum, vector(detNum, 0)); + cost_matrix.clear(); + cost_matrix.resize(trkNum, vector(detNum, 0)); - for (unsigned int i = 0; i < trkNum; i++) // compute iou matrix as a distance matrix + for (unsigned int i = 0; i < trkNum; i++) // compute cost matrix using 1 - IOU with gating { for (unsigned int j = 0; j < detNum; j++) { - // use 1-iou because the hungarian algorithm computes a minimum-cost assignment. - double distance = SortTracker::GetCentroidsDistance(predictedBoxes[i], detections[j].box) / image_diagonal; - centroid_dist_matrix[i][j] = distance; + double iou = GetIOU(predictedBoxes[i], detections[j].box); + double dist = GetCentroidsDistance(predictedBoxes[i], detections[j].box) / image_diagonal; + if (trackers[i].classId != detections[j].classId || dist > max_centroid_dist_norm) + { + cost_matrix[i][j] = 1e9; // large cost for gating + } + else + { + cost_matrix[i][j] = 1 - iou + (1 - detections[j].confidence) * 0.1; // slight penalty for low conf + } } } HungarianAlgorithm HungAlgo; assignment.clear(); - HungAlgo.Solve(centroid_dist_matrix, assignment); + HungAlgo.Solve(cost_matrix, assignment); // find matches, unmatched_detections and unmatched_predictions unmatchedTrajectories.clear(); unmatchedDetections.clear(); @@ -150,7 +204,7 @@ void SortTracker::update(vector detections_cv, int frame_count, double { if (assignment[i] == -1) // pass over invalid values continue; - if (centroid_dist_matrix[i][assignment[i]] > max_centroid_dist_norm) + if (cost_matrix[i][assignment[i]] > 1 - _min_iou) { unmatchedTrajectories.insert(i); unmatchedDetections.insert(assignment[i]); @@ -171,7 +225,7 @@ void SortTracker::update(vector detections_cv, int frame_count, double // create and initialise new trackers for unmatched detections for (auto umd : unmatchedDetections) { - KalmanTracker tracker = KalmanTracker(detections[umd].box, detections[umd].confidence, detections[umd].classId, umd); + KalmanTracker tracker = KalmanTracker(detections[umd].box, detections[umd].confidence, detections[umd].classId, _next_id++); trackers.push_back(tracker); } @@ -192,7 +246,8 @@ void SortTracker::update(vector detections_cv, int frame_count, double frameTrackingResult.clear(); for (unsigned int i = 0; i < trackers.size();) { - if ((trackers[i].m_time_since_update < 1 && trackers[i].m_hit_streak >= _min_hits) || frame_count <= _min_hits) + if ((trackers[i].m_hits >= _min_hits && trackers[i].m_time_since_update <= _max_missed) || + frame_count <= _min_hits) { alive_tracker = true; TrackingBox res; diff --git a/src/sort_filter/sort.hpp b/src/sort_filter/sort.hpp index 6d7f22e23..74e905adc 100644 --- a/src/sort_filter/sort.hpp +++ b/src/sort_filter/sort.hpp @@ -9,6 +9,7 @@ #include #include // to format image names using setw() and setfill() #include +#include // for std::sort #include "opencv2/video/tracking.hpp" #include "opencv2/highgui/highgui.hpp" @@ -25,7 +26,7 @@ typedef struct TrackingBox int classId = 0; int id = 0; cv::Rect_ box = cv::Rect_(0.0, 0.0, 0.0, 0.0); - TrackingBox() {} + TrackingBox() {} TrackingBox(int _frame, float _confidence, int _classId, int _id) : frame(_frame), confidence(_confidence), classId(_classId), id(_id) {} } TrackingBox; @@ -33,19 +34,19 @@ class SortTracker { public: // Constructor - SortTracker(int max_age = 7, int min_hits = 2); + SortTracker(int max_age = 50, int min_hits = 5, int max_missed = 7, double min_iou = 0.1, double nms_iou_thresh = 0.5, double min_conf = 0.3); // Initialize tracker // Update position based on the new frame void update(std::vector detection, int frame_count, double image_diagonal, std::vector confidences, std::vector classIds); - double GetIOU(cv::Rect_ bb_test, cv::Rect_ bb_gt); + static double GetIOU(cv::Rect_ bb_test, cv::Rect_ bb_gt); double GetCentroidsDistance(cv::Rect_ bb_test, cv::Rect_ bb_gt); std::vector trackers; - double max_centroid_dist_norm = 0.05; + double max_centroid_dist_norm = 0.3; std::vector> predictedBoxes; - std::vector> centroid_dist_matrix; + std::vector> cost_matrix; std::vector assignment; std::set unmatchedDetections; std::set unmatchedTrajectories; @@ -60,5 +61,10 @@ class SortTracker unsigned int detNum = 0; int _min_hits; int _max_age; + int _max_missed; + double _min_iou; + double _nms_iou_thresh; + double _min_conf; + unsigned int _next_id; bool alive_tracker; }; From adff81fefcdad697a0a996f7afc3d2d8949875b0 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 12 Aug 2025 17:46:56 -0500 Subject: [PATCH 13/41] Fixing protobuf loading bug, preventing tracker and object detection from loading correctly. --- src/effects/ObjectDetection.cpp | 13 +++++++----- src/effects/Tracker.cpp | 35 ++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/effects/ObjectDetection.cpp b/src/effects/ObjectDetection.cpp index df60afd7e..6e1ae97c3 100644 --- a/src/effects/ObjectDetection.cpp +++ b/src/effects/ObjectDetection.cpp @@ -362,11 +362,14 @@ void ObjectDetection::SetJsonValue(const Json::Value root) EffectBase::SetJsonValue(root); // If a protobuf path is provided, load & prefix IDs - if (!root["protobuf_data_path"].isNull() && protobuf_data_path.empty()) { - protobuf_data_path = root["protobuf_data_path"].asString(); - if (!LoadObjDetectdData(protobuf_data_path)) { - throw InvalidFile("Invalid protobuf data path", ""); - } + if (!root["protobuf_data_path"].isNull()) { + std::string new_path = root["protobuf_data_path"].asString(); + if (protobuf_data_path != new_path || trackedObjects.empty()) { + protobuf_data_path = new_path; + if (!LoadObjDetectdData(protobuf_data_path)) { + throw InvalidFile("Invalid protobuf data path", ""); + } + } } // Selected index, thresholds, UI flags, filters, etc. diff --git a/src/effects/Tracker.cpp b/src/effects/Tracker.cpp index d485f9036..2776ab7ad 100644 --- a/src/effects/Tracker.cpp +++ b/src/effects/Tracker.cpp @@ -220,22 +220,25 @@ void Tracker::SetJsonValue(const Json::Value root) { TimeScale = root["TimeScale"].asDouble(); } - if (!root["protobuf_data_path"].isNull() && protobuf_data_path.empty()) { - protobuf_data_path = root["protobuf_data_path"].asString(); - if (!trackedData->LoadBoxData(protobuf_data_path)) { - std::clog << "Invalid protobuf data path " << protobuf_data_path << '\n'; - protobuf_data_path.clear(); - } - else { - // prefix "-" for each entry - for (auto& kv : trackedObjects) { - auto idx = kv.first; - auto ptr = kv.second; - if (ptr) { - std::string prefix = this->Id(); - if (!prefix.empty()) - prefix += "-"; - ptr->Id(prefix + std::to_string(idx)); + if (!root["protobuf_data_path"].isNull()) { + std::string new_path = root["protobuf_data_path"].asString(); + if (protobuf_data_path != new_path || trackedData->GetLength() == 0) { + protobuf_data_path = new_path; + if (!trackedData->LoadBoxData(protobuf_data_path)) { + std::clog << "Invalid protobuf data path " << protobuf_data_path << '\n'; + protobuf_data_path.clear(); + } + else { + // prefix "-" for each entry + for (auto& kv : trackedObjects) { + auto idx = kv.first; + auto ptr = kv.second; + if (ptr) { + std::string prefix = this->Id(); + if (!prefix.empty()) + prefix += "-"; + ptr->Id(prefix + std::to_string(idx)); + } } } } From dd62f5b49e245cd496b0f9a83c7d530a2584d1c3 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 5 Sep 2025 14:48:35 -0500 Subject: [PATCH 14/41] Protecting clip GetFrame from crash due to null frame, then setting the ->number property of a null frame. --- src/Clip.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Clip.cpp b/src/Clip.cpp index b63fdba7c..6866486ea 100644 --- a/src/Clip.cpp +++ b/src/Clip.cpp @@ -694,10 +694,11 @@ std::shared_ptr Clip::GetOrCreateFrame(int64_t number, bool enable_time) // Attempt to get a frame (but this could fail if a reader has just been closed) auto reader_frame = reader->GetFrame(clip_frame_number); - reader_frame->number = number; // Override frame # (due to time-mapping might change it) - - // Return real frame if (reader_frame) { + // Override frame # (due to time-mapping might change it) + reader_frame->number = number; + + // Return real frame // Create a new copy of reader frame // This allows a clip to modify the pixels and audio of this frame without // changing the underlying reader's frame data From fbef1bcf04ee04d8bf580a6641e8a0bbd6c8b989 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Sun, 7 Sep 2025 15:02:26 -0500 Subject: [PATCH 15/41] Protect the video and audio codec name discovery flow, to prevent crash from unknown codecs. --- src/FFmpegWriter.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/FFmpegWriter.cpp b/src/FFmpegWriter.cpp index 79cbc21c6..b1feafe3f 100644 --- a/src/FFmpegWriter.cpp +++ b/src/FFmpegWriter.cpp @@ -126,17 +126,18 @@ void FFmpegWriter::auto_detect_format() { // Determine what format to use when encoding this output filename oc->oformat = av_guess_format(NULL, path.c_str(), NULL); if (oc->oformat == nullptr) { - throw InvalidFormat( - "Could not deduce output format from file extension.", path); + throw InvalidFormat("Could not deduce output format from file extension.", path); } - // Update video codec name - if (oc->oformat->video_codec != AV_CODEC_ID_NONE && info.has_video) - info.vcodec = avcodec_find_encoder(oc->oformat->video_codec)->name; - - // Update audio codec name - if (oc->oformat->audio_codec != AV_CODEC_ID_NONE && info.has_audio) - info.acodec = avcodec_find_encoder(oc->oformat->audio_codec)->name; + // Update video & audio codec name + if (oc->oformat->video_codec != AV_CODEC_ID_NONE && info.has_video) { + const AVCodec *vcodec = avcodec_find_encoder(oc->oformat->video_codec); + info.vcodec = vcodec ? vcodec->name : std::string(); + } + if (oc->oformat->audio_codec != AV_CODEC_ID_NONE && info.has_audio) { + const AVCodec *acodec = avcodec_find_encoder(oc->oformat->audio_codec); + info.acodec = acodec ? acodec->name : std::string(); + } } // initialize streams From f68d18419adb51ddaf42de76e8312b60f6c7ed57 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 8 Sep 2025 15:59:29 -0500 Subject: [PATCH 16/41] Improve GetMinFrame / GetMaxFrame functions for a timeline, to be inclusive on min, and exclusive on max (trying to prevent rounding errors on last frame) --- src/Timeline.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Timeline.cpp b/src/Timeline.cpp index ec6d33b2d..9b940d4a1 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -467,9 +467,18 @@ double Timeline::GetMaxTime() { // Compute the highest frame# based on the latest time and FPS int64_t Timeline::GetMaxFrame() { - double fps = info.fps.ToDouble(); - auto max_time = GetMaxTime(); - return std::round(max_time * fps); + const double fps = info.fps.ToDouble(); + const double t = GetMaxTime(); + // Inclusive start, exclusive end -> ceil at the end boundary + return static_cast(std::ceil(t * fps)); +} + +// Compute the first frame# based on the first clip position +int64_t Timeline::GetMinFrame() { + const double fps = info.fps.ToDouble(); + const double t = GetMinTime(); + // Inclusive start -> floor at the start boundary, then 1-index + return static_cast(std::floor(t * fps)) + 1; } // Compute the start time of the first timeline clip @@ -478,13 +487,6 @@ double Timeline::GetMinTime() { return min_time; } -// Compute the first frame# based on the first clip position -int64_t Timeline::GetMinFrame() { - double fps = info.fps.ToDouble(); - auto min_time = GetMinTime(); - return std::round(min_time * fps) + 1; -} - // Apply a FrameMapper to a clip which matches the settings of this timeline void Timeline::apply_mapper_to_clip(Clip* clip) { From f98da72050250e9ba3484d7ef2f9eb0a83be11c1 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 8 Sep 2025 18:02:36 -0500 Subject: [PATCH 17/41] Improve spherical projection effect to have better quality and separate input / output FOVs for fisheye processing. --- src/effects/SphericalProjection.cpp | 594 ++++++++++++++++++---------- src/effects/SphericalProjection.h | 91 +++-- tests/SphericalEffect.cpp | 233 +++++------ 3 files changed, 561 insertions(+), 357 deletions(-) diff --git a/src/effects/SphericalProjection.cpp b/src/effects/SphericalProjection.cpp index 0565a7a30..19cdbea8b 100644 --- a/src/effects/SphericalProjection.cpp +++ b/src/effects/SphericalProjection.cpp @@ -13,32 +13,25 @@ #include "SphericalProjection.h" #include "Exceptions.h" -#include #include +#include #include +#include using namespace openshot; SphericalProjection::SphericalProjection() - : yaw(0.0) - , pitch(0.0) - , roll(0.0) - , fov(90.0) - , projection_mode(0) - , invert(0) - , interpolation(0) -{ - init_effect_details(); + : yaw(0.0), pitch(0.0), roll(0.0), fov(90.0), in_fov(180.0), + projection_mode(0), invert(0), input_model(0), interpolation(3) { + 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), invert(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), + in_fov(180.0), projection_mode(0), invert(0), input_model(0), + interpolation(3) { + init_effect_details(); } void SphericalProjection::init_effect_details() @@ -51,212 +44,397 @@ void SphericalProjection::init_effect_details() info.has_video = true; } -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(); - - // 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 + M_PI; - double fov_r = fov.GetValue(frame_number) * M_PI/180.0; - - // 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); - - 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; - +namespace { +inline double cubic_interp(double p0, double p1, double p2, double p3, + double t) { + double a0 = -0.5 * p0 + 1.5 * p1 - 1.5 * p2 + 0.5 * p3; + double a1 = p0 - 2.5 * p1 + 2.0 * p2 - 0.5 * p3; + double a2 = -0.5 * p0 + 0.5 * p2; + double a3 = p1; + return ((a0 * t + a1) * t + a2) * t + a3; +} +} // namespace + +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(); + + // 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 + M_PI; + double in_fov_r = in_fov.GetValue(frame_number) * M_PI / 180.0; + double out_fov_r = fov.GetValue(frame_number) * M_PI / 180.0; + + // 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); + + 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(out_fov_r * 0.5); + double vy = hx * double(H) / W; + + auto q = [](double a) { return std::llround(a * 1e6); }; + bool recompute = uv_map.empty() || W != cached_width || H != cached_height || + q(yaw_r) != q(cached_yaw) || + q(pitch_r) != q(cached_pitch) || + q(roll_r) != q(cached_roll) || + q(in_fov_r) != q(cached_in_fov) || + q(out_fov_r) != q(cached_out_fov) || + input_model != cached_input_model || + projection_mode != cached_projection_mode || + invert != cached_invert; + + if (recompute) { + uv_map.resize(W * H * 2); #pragma omp parallel for schedule(static) for (int yy = 0; yy < H; yy++) { - uchar* dst_row = dst + yy * dst_bpl; - double ndc_y = (2.0*(yy + 0.5)/H - 1.0) * vy; - - for (int xx = 0; xx < W; xx++) { - // Generate ray in camera space - double ndc_x = (2.0*(xx + 0.5)/W - 1.0) * hx; - double vx = ndc_x, vy2 = -ndc_y, vz = -1.0; - double inv = 1.0/sqrt(vx*vx + vy2*vy2 + vz*vz); - vx *= inv; vy2 *= inv; vz *= inv; - - // Rotate ray into world coordinates - double dx = r00*vx + r01*vy2 + r02*vz; - double dy = r10*vx + r11*vy2 + r12*vz; - double dz = r20*vx + r21*vy2 + r22*vz; - - // For sphere/hemisphere, optionally invert view by 180° - if (projection_mode < 2 && invert) { - dx = -dx; - dz = -dz; - } - - double uf, vf; - - if (projection_mode == 2) { - // Fisheye mode: invert circular fisheye - double ax = 0.0, ay = 0.0, az = invert ? -1.0 : 1.0; - double cos_t = dx*ax + dy*ay + dz*az; - double theta = acos(cos_t); - double rpx = (theta / fov_r) * (W/2.0); - double phi = atan2(dy, dx); - uf = W*0.5 + rpx*cos(phi); - vf = H*0.5 + rpx*sin(phi); - } - else { - // Sphere or hemisphere: equirectangular sampling - double lon = atan2(dx, dz); - double lat = asin(dy); - if (projection_mode == 1) // hemisphere - lon = std::clamp(lon, -M_PI/2.0, M_PI/2.0); - uf = ((lon + (projection_mode? M_PI/2.0 : M_PI)) - / (projection_mode? M_PI : 2.0*M_PI)) * W; - vf = (lat + M_PI/2.0)/M_PI * H; - } + double ndc_y = (2.0 * (yy + 0.5) / H - 1.0) * vy; + for (int xx = 0; xx < W; xx++) { + double ndc_x = (2.0 * (xx + 0.5) / W - 1.0) * hx; + double vx = ndc_x, vy2 = -ndc_y, vz = -1.0; + double inv = 1.0 / sqrt(vx * vx + vy2 * vy2 + vz * vz); + vx *= inv; + vy2 *= inv; + vz *= inv; + + double dx = r00 * vx + r01 * vy2 + r02 * vz; + double dy = r10 * vx + r11 * vy2 + r12 * vz; + double dz = r20 * vx + r21 * vy2 + r22 * vz; + + if (projection_mode < 2 && invert) { + dx = -dx; + dz = -dz; + } - uchar* d = dst_row + xx*4; + double uf, vf; + project_input(dx, dy, dz, in_fov_r, W, H, uf, vf); + int idx = 2 * (yy * W + xx); + uv_map[idx] = (float)uf; + uv_map[idx + 1] = (float)vf; + } + } + cached_width = W; + cached_height = H; + cached_yaw = yaw_r; + cached_pitch = pitch_r; + cached_roll = roll_r; + cached_in_fov = in_fov_r; + cached_out_fov = out_fov_r; + cached_input_model = input_model; + cached_projection_mode = projection_mode; + cached_invert = invert; + } + + // Automatic sampler selection + int sampler = interpolation; + if (interpolation == 3) { + double coverage_r = (projection_mode == 0 ? 2.0 * M_PI + : projection_mode == 1 ? M_PI + : in_fov_r); + double ppd_src = W / coverage_r; + double ppd_out = W / out_fov_r; + double ratio = ppd_out / ppd_src; + if (ratio < 0.8) + sampler = 3; // mipmaps + else if (ratio <= 1.2) + sampler = 1; // bilinear + else + sampler = 2; // bicubic + } + + // Build mipmaps only if needed (simple box filter) + std::vector mipmaps; + if (sampler == 3) { + mipmaps.push_back(*img); + for (int level = 1; level < 4; ++level) { + const QImage &prev = mipmaps[level - 1]; + if (prev.width() <= 1 || prev.height() <= 1) + break; + int w = prev.width() / 2; + int h = prev.height() / 2; + QImage next(w, h, QImage::Format_ARGB32); + uchar *nb = next.bits(); + int nbpl = next.bytesPerLine(); + const uchar *pb = prev.bits(); + int pbpl = prev.bytesPerLine(); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + for (int c = 0; c < 4; c++) { + int p00 = pb[(2 * y) * pbpl + (2 * x) * 4 + c]; + int p10 = pb[(2 * y) * pbpl + (2 * x + 1) * 4 + c]; + int p01 = pb[(2 * y + 1) * pbpl + (2 * x) * 4 + c]; + int p11 = pb[(2 * y + 1) * pbpl + (2 * x + 1) * 4 + c]; + nb[y * nbpl + x * 4 + c] = (p00 + p10 + p01 + p11) / 4; + } + } + } + mipmaps.push_back(next); + } + } - if (interpolation == 0) { - // Nearest-neighbor sampling - int x0 = std::clamp(int(std::floor(uf)), 0, W-1); - int y0 = std::clamp(int(std::floor(vf)), 0, H-1); - uchar* s = src + y0*bpl + x0*4; - d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3]; - } - else { - // Bilinear sampling - int x0 = std::clamp(int(std::floor(uf)), 0, W-1); - int y0 = std::clamp(int(std::floor(vf)), 0, H-1); - int x1 = std::clamp(x0 + 1, 0, W-1); - int y1 = std::clamp(y0 + 1, 0, H-1); - double dxr = uf - x0, dyr = vf - y0; - uchar* p00 = src + y0*bpl + x0*4; - uchar* p10 = src + y0*bpl + x1*4; - uchar* p01 = src + y1*bpl + x0*4; - uchar* p11 = src + y1*bpl + x1*4; - for (int c = 0; c < 4; c++) { - double v0 = p00[c]*(1-dxr) + p10[c]*dxr; - double v1 = p01[c]*(1-dxr) + p11[c]*dxr; - d[c] = uchar(v0*(1-dyr) + v1*dyr + 0.5); - } +#pragma omp parallel for schedule(static) + for (int yy = 0; yy < H; yy++) { + uchar *dst_row = dst + yy * dst_bpl; + for (int xx = 0; xx < W; xx++) { + int idx = 2 * (yy * W + xx); + double uf = uv_map[idx]; + double vf = uv_map[idx + 1]; + uchar *d = dst_row + xx * 4; + + if (input_model == 0 && projection_mode == 0) { + uf = std::fmod(std::fmod(uf, W) + W, W); + vf = std::clamp(vf, 0.0, (double)H - 1); + } else if (uf < 0 || uf >= W || vf < 0 || vf >= H) { + d[0] = d[1] = d[2] = 0; + d[3] = 0; + continue; + } + + if (sampler == 0) { + int x0 = std::clamp(int(std::floor(uf)), 0, W - 1); + int y0 = std::clamp(int(std::floor(vf)), 0, H - 1); + uchar *s = src + y0 * bpl + x0 * 4; + d[0] = s[0]; + d[1] = s[1]; + d[2] = s[2]; + d[3] = s[3]; + } else if (sampler == 1) { + int x0 = std::clamp(int(std::floor(uf)), 0, W - 1); + int y0 = std::clamp(int(std::floor(vf)), 0, H - 1); + int x1 = std::clamp(x0 + 1, 0, W - 1); + int y1 = std::clamp(y0 + 1, 0, H - 1); + double dxr = uf - x0, dyr = vf - y0; + uchar *p00 = src + y0 * bpl + x0 * 4; + uchar *p10 = src + y0 * bpl + x1 * 4; + uchar *p01 = src + y1 * bpl + x0 * 4; + uchar *p11 = src + y1 * bpl + x1 * 4; + for (int c = 0; c < 4; c++) { + double v0 = p00[c] * (1 - dxr) + p10[c] * dxr; + double v1 = p01[c] * (1 - dxr) + p11[c] * dxr; + d[c] = uchar(v0 * (1 - dyr) + v1 * dyr + 0.5); + } + } else if (sampler == 2) { + int x1 = std::clamp(int(std::floor(uf)), 0, W - 1); + int y1 = std::clamp(int(std::floor(vf)), 0, H - 1); + double tx = uf - x1; + double ty = vf - y1; + for (int c = 0; c < 4; c++) { + double col[4]; + for (int j = -1; j <= 2; j++) { + int y = std::clamp(y1 + j, 0, H - 1); + double row[4]; + for (int i = -1; i <= 2; i++) { + int x = std::clamp(x1 + i, 0, W - 1); + row[i + 1] = src[y * bpl + x * 4 + c]; } + col[j + 1] = cubic_interp(row[0], row[1], row[2], row[3], tx); + } + double val = cubic_interp(col[0], col[1], col[2], col[3], ty); + d[c] = uchar(std::clamp(val, 0.0, 255.0) + 0.5); + } + } else { + // Mipmap sampling with bilinear + double uf_dx = 0.0, vf_dx = 0.0, uf_dy = 0.0, vf_dy = 0.0; + if (xx + 1 < W) { + uf_dx = uv_map[idx + 2] - uf; + vf_dx = uv_map[idx + 3] - vf; } + if (yy + 1 < H) { + uf_dy = uv_map[idx + 2 * W] - uf; + vf_dy = uv_map[idx + 2 * W + 1] - vf; + } + double scale_x = std::sqrt(uf_dx * uf_dx + vf_dx * vf_dx); + double scale_y = std::sqrt(uf_dy * uf_dy + vf_dy * vf_dy); + double scale = std::max(scale_x, scale_y); + int level = 0; + if (scale > 1.0) + level = + std::min(std::floor(std::log2(scale)), mipmaps.size() - 1); + const QImage &lvl = mipmaps[level]; + int Wl = lvl.width(), Hl = lvl.height(); + int bpl_l = lvl.bytesPerLine(); + const uchar *srcl = lvl.bits(); + double uf_l = uf / (1 << level); + double vf_l = vf / (1 << level); + int x0 = std::clamp(int(std::floor(uf_l)), 0, Wl - 1); + int y0 = std::clamp(int(std::floor(vf_l)), 0, Hl - 1); + int x1 = std::clamp(x0 + 1, 0, Wl - 1); + int y1 = std::clamp(y0 + 1, 0, Hl - 1); + double dxr = uf_l - x0, dyr = vf_l - y0; + const uchar *p00 = srcl + y0 * bpl_l + x0 * 4; + const uchar *p10 = srcl + y0 * bpl_l + x1 * 4; + const uchar *p01 = srcl + y1 * bpl_l + x0 * 4; + const uchar *p11 = srcl + y1 * bpl_l + x1 * 4; + for (int c = 0; c < 4; c++) { + double v0 = p00[c] * (1 - dxr) + p10[c] * dxr; + double v1 = p01[c] * (1 - dxr) + p11[c] * dxr; + d[c] = uchar(v0 * (1 - dyr) + v1 * dyr + 0.5); + } + } } + } - *img = output; - return frame; + *img = output; + return frame; } -std::string SphericalProjection::Json() const -{ - return JsonValue().toStyledString(); +void SphericalProjection::project_input(double dx, double dy, double dz, + double in_fov_r, int W, int H, + double &uf, double &vf) const { + if (input_model == 0) { + // Equirectangular + double lon = atan2(dx, dz); + double lat = asin(dy); + if (projection_mode == 1) + lon = std::clamp(lon, -M_PI / 2.0, M_PI / 2.0); + uf = ((lon + (projection_mode ? M_PI / 2.0 : M_PI)) / + (projection_mode ? M_PI : 2.0 * M_PI)) * + W; + vf = (lat + M_PI / 2.0) / M_PI * H; + } else { + // Fisheye equidistant + double ax = 0.0, ay = 0.0, az = invert ? -1.0 : 1.0; + double cos_t = dx * ax + dy * ay + dz * az; + cos_t = std::clamp(cos_t, -1.0, 1.0); + double theta = acos(cos_t); + double theta_max = in_fov_r * 0.5; + double r_norm = theta / theta_max; + double R = 0.5 * std::min(W, H); + double rpx = r_norm * R; + double phi = atan2(dy, dx); + uf = W * 0.5 + rpx * cos(phi); + vf = H * 0.5 + rpx * sin(phi); + } } -Json::Value SphericalProjection::JsonValue() const -{ - Json::Value root = EffectBase::JsonValue(); - root["type"] = info.class_name; - root["yaw"] = yaw.JsonValue(); - root["pitch"] = pitch.JsonValue(); - root["roll"] = roll.JsonValue(); - root["fov"] = fov.JsonValue(); - root["projection_mode"] = projection_mode; - root["invert"] = invert; - root["interpolation"] = interpolation; - return root; +std::string SphericalProjection::Json() const { + return JsonValue().toStyledString(); } -void SphericalProjection::SetJson(const std::string value) -{ - try { - Json::Value root = openshot::stringToJson(value); - SetJsonValue(root); - } - catch (...) { - throw InvalidJSON("Invalid JSON for SphericalProjection"); - } +Json::Value SphericalProjection::JsonValue() const { + Json::Value root = EffectBase::JsonValue(); + root["type"] = info.class_name; + root["yaw"] = yaw.JsonValue(); + root["pitch"] = pitch.JsonValue(); + root["roll"] = roll.JsonValue(); + root["fov"] = fov.JsonValue(); + root["in_fov"] = in_fov.JsonValue(); + root["projection_mode"] = projection_mode; + root["invert"] = invert; + root["input_model"] = input_model; + root["interpolation"] = interpolation; + return root; } -void SphericalProjection::SetJsonValue(const Json::Value root) -{ - EffectBase::SetJsonValue(root); - if (!root["yaw"].isNull()) yaw.SetJsonValue(root["yaw"]); - if (!root["pitch"].isNull()) pitch.SetJsonValue(root["pitch"]); - if (!root["roll"].isNull()) roll.SetJsonValue(root["roll"]); - if (!root["fov"].isNull()) fov.SetJsonValue(root["fov"]); - if (!root["projection_mode"].isNull()) projection_mode = root["projection_mode"].asInt(); - if (!root["invert"].isNull()) invert = root["invert"].asInt(); - if (!root["interpolation"].isNull()) interpolation = root["interpolation"].asInt(); +void SphericalProjection::SetJson(const std::string value) { + try { + Json::Value root = openshot::stringToJson(value); + SetJsonValue(root); + } catch (...) { + throw InvalidJSON("Invalid JSON for SphericalProjection"); + } } -std::string SphericalProjection::PropertiesJSON(int64_t requested_frame) const -{ - Json::Value root = BasePropertiesJSON(requested_frame); - - root["yaw"] = add_property_json("Yaw", - yaw.GetValue(requested_frame), - "float", "degrees", - &yaw, -180, 180, - false, requested_frame); - root["pitch"] = add_property_json("Pitch", - pitch.GetValue(requested_frame), - "float", "degrees", - &pitch, -90, 90, - false, requested_frame); - root["roll"] = add_property_json("Roll", - roll.GetValue(requested_frame), - "float", "degrees", - &roll, -180, 180, - false, requested_frame); - root["fov"] = add_property_json("FOV", - fov.GetValue(requested_frame), - "float", "degrees", - &fov, 1, 179, - false, requested_frame); - - root["projection_mode"] = add_property_json("Projection Mode", - projection_mode, - "int", "", - nullptr, 0, 2, - false, requested_frame); - root["projection_mode"]["choices"].append(add_property_choice_json("Sphere", 0, projection_mode)); - root["projection_mode"]["choices"].append(add_property_choice_json("Hemisphere", 1, projection_mode)); - root["projection_mode"]["choices"].append(add_property_choice_json("Fisheye", 2, projection_mode)); - - root["invert"] = add_property_json("Invert View", - invert, - "int", "", - nullptr, 0, 1, - false, requested_frame); - root["invert"]["choices"].append(add_property_choice_json("Normal", 0, invert)); - root["invert"]["choices"].append(add_property_choice_json("Invert", 1, invert)); - - root["interpolation"] = add_property_json("Interpolation", - interpolation, - "int", "", - nullptr, 0, 1, - false, requested_frame); - root["interpolation"]["choices"].append(add_property_choice_json("Nearest", 0, interpolation)); - root["interpolation"]["choices"].append(add_property_choice_json("Bilinear", 1, interpolation)); - - return root.toStyledString(); +void SphericalProjection::SetJsonValue(const Json::Value root) { + EffectBase::SetJsonValue(root); + if (!root["yaw"].isNull()) + yaw.SetJsonValue(root["yaw"]); + if (!root["pitch"].isNull()) + pitch.SetJsonValue(root["pitch"]); + if (!root["roll"].isNull()) + roll.SetJsonValue(root["roll"]); + if (!root["fov"].isNull()) + fov.SetJsonValue(root["fov"]); + if (!root["in_fov"].isNull()) + in_fov.SetJsonValue(root["in_fov"]); + if (!root["projection_mode"].isNull()) + projection_mode = root["projection_mode"].asInt(); + if (!root["invert"].isNull()) + invert = root["invert"].asInt(); + if (!root["input_model"].isNull()) + input_model = root["input_model"].asInt(); + if (!root["interpolation"].isNull()) + interpolation = root["interpolation"].asInt(); + + // any property change should invalidate cached UV map + uv_map.clear(); +} + +std::string SphericalProjection::PropertiesJSON(int64_t requested_frame) const { + Json::Value root = BasePropertiesJSON(requested_frame); + + root["yaw"] = + add_property_json("Yaw", yaw.GetValue(requested_frame), "float", + "degrees", &yaw, -180, 180, false, requested_frame); + root["pitch"] = + add_property_json("Pitch", pitch.GetValue(requested_frame), "float", + "degrees", &pitch, -90, 90, false, requested_frame); + root["roll"] = + add_property_json("Roll", roll.GetValue(requested_frame), "float", + "degrees", &roll, -180, 180, false, requested_frame); + root["fov"] = + add_property_json("Out FOV", fov.GetValue(requested_frame), "float", + "degrees", &fov, 1, 179, false, requested_frame); + root["in_fov"] = + add_property_json("In FOV", in_fov.GetValue(requested_frame), "float", + "degrees", &in_fov, 1, 360, false, requested_frame); + + root["projection_mode"] = + add_property_json("Projection Mode", projection_mode, "int", "", nullptr, + 0, 2, false, requested_frame); + root["projection_mode"]["choices"].append( + add_property_choice_json("Sphere", 0, projection_mode)); + root["projection_mode"]["choices"].append( + add_property_choice_json("Hemisphere", 1, projection_mode)); + root["projection_mode"]["choices"].append( + add_property_choice_json("Fisheye", 2, projection_mode)); + + root["invert"] = add_property_json("Invert View", invert, "int", "", nullptr, + 0, 1, false, requested_frame); + root["invert"]["choices"].append( + add_property_choice_json("Normal", 0, invert)); + root["invert"]["choices"].append( + add_property_choice_json("Invert", 1, invert)); + + root["input_model"] = + add_property_json("Input Model", input_model, "int", "", nullptr, 0, 3, + false, requested_frame); + root["input_model"]["choices"].append( + add_property_choice_json("Equirect", 0, input_model)); + root["input_model"]["choices"].append( + add_property_choice_json("Fisheye Equidistant", 1, input_model)); + + root["interpolation"] = + add_property_json("Interpolation", interpolation, "int", "", nullptr, 0, + 3, false, requested_frame); + root["interpolation"]["choices"].append( + add_property_choice_json("Nearest", 0, interpolation)); + root["interpolation"]["choices"].append( + add_property_choice_json("Bilinear", 1, interpolation)); + root["interpolation"]["choices"].append( + add_property_choice_json("Bicubic", 2, interpolation)); + root["interpolation"]["choices"].append( + add_property_choice_json("Auto", 3, interpolation)); + + return root.toStyledString(); } diff --git a/src/effects/SphericalProjection.h b/src/effects/SphericalProjection.h index 1738f976e..7d86992a7 100644 --- a/src/effects/SphericalProjection.h +++ b/src/effects/SphericalProjection.h @@ -20,53 +20,66 @@ #include #include +#include -namespace openshot -{ +namespace openshot { /** * @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. + * Supports yaw, pitch, roll, input and output FOV, sphere/hemisphere/fisheye + * modes, optional inversion, and automatic quality selection. */ -class SphericalProjection : public EffectBase -{ +class SphericalProjection : public EffectBase { private: - void init_effect_details(); + 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=Sphere, 1=Hemisphere, 2=Fisheye - int invert; ///< 0=Normal, 1=Invert (back lens / +180°) - int interpolation; ///< 0=Nearest, 1=Bilinear - - /// Blank ctor (for JSON deserialization) - SphericalProjection(); - - /// Ctor with custom curves - SphericalProjection(Keyframe new_yaw, - Keyframe new_pitch, - Keyframe new_roll, - Keyframe new_fov); - - /// 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); } - - /// 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; + Keyframe yaw; ///< Yaw around up-axis (degrees) + Keyframe pitch; ///< Pitch around right-axis (degrees) + Keyframe roll; ///< Roll around forward-axis (degrees) + Keyframe fov; ///< Output field-of-view (horizontal, degrees) + Keyframe in_fov; ///< Source lens coverage / FOV (degrees) + + int projection_mode; ///< 0=Sphere, 1=Hemisphere, 2=Fisheye + int invert; ///< 0=Normal, 1=Invert (back lens / +180°) + int input_model; ///< 0=Equirect, 1=Fisheye-Equidistant + int interpolation; ///< 0=Nearest, 1=Bilinear, 2=Bicubic, 3=Auto + + /// Blank ctor (for JSON deserialization) + SphericalProjection(); + + /// Ctor with custom curves + SphericalProjection(Keyframe new_yaw, Keyframe new_pitch, Keyframe new_roll, + Keyframe new_fov); + + /// 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); + } + + /// 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; + +private: + void project_input(double dx, double dy, double dz, double in_fov_r, int W, + int H, double &uf, double &vf) const; + + mutable std::vector uv_map; ///< Cached UV lookup + mutable int cached_width = 0; + mutable int cached_height = 0; + mutable double cached_yaw = 0.0, cached_pitch = 0.0, cached_roll = 0.0; + mutable double cached_in_fov = 0.0, cached_out_fov = 0.0; + mutable int cached_input_model = -1; + mutable int cached_projection_mode = -1; + mutable int cached_invert = -1; }; } // namespace openshot diff --git a/tests/SphericalEffect.cpp b/tests/SphericalEffect.cpp index 0794d189d..9a6db407b 100644 --- a/tests/SphericalEffect.cpp +++ b/tests/SphericalEffect.cpp @@ -10,137 +10,150 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later -#include -#include -#include #include "Frame.h" #include "effects/SphericalProjection.h" #include "openshot_catch.h" +#include +#include +#include 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; +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; +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); +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); +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)); - } + { + 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("hemisphere mode default and invert", "[effect][spherical]") { + SphericalProjection e; + e.projection_mode = 1; -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)); - } + { + 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 yaw has no effect at center", "[effect][spherical]") -{ - SphericalProjection e; - e.projection_mode = 2; - e.interpolation = 0; - e.fov = Keyframe(180.0); +TEST_CASE("fisheye mode default and invert", "[effect][spherical]") { + SphericalProjection e; + e.projection_mode = 2; + e.input_model = 1; + e.in_fov = Keyframe(180.0); + 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)); + } +} - auto f = loadFrame("fisheye.png"); - e.yaw = Keyframe(45.0); - CHECK(centerPixel(e, f) == QColor(255,255,255,255)); +TEST_CASE("fisheye mode yaw has no effect at center", "[effect][spherical]") { + SphericalProjection e; + e.projection_mode = 2; + e.input_model = 1; + e.interpolation = 0; + e.in_fov = Keyframe(180.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)); +} + +TEST_CASE("changing properties invalidates cache", "[effect][spherical]") { + SphericalProjection e; + e.projection_mode = 0; + e.yaw = Keyframe(45.0); + e.invert = 0; + e.interpolation = 0; + + auto f0 = loadFrame("eq_sphere.png"); + QColor c0 = centerPixel(e, f0); + + auto f1 = loadFrame("eq_sphere.png"); + e.invert = 1; // should rebuild UV map + QColor c1 = centerPixel(e, f1); + + CHECK(c1 != c0); } From c23c0d14afe01a5d910dab0612db5acdad949056 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 9 Sep 2025 18:36:36 -0500 Subject: [PATCH 18/41] Fixing regression in SphericalProjection.cpp effect - causing a unit test to fail --- src/effects/SphericalProjection.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/effects/SphericalProjection.cpp b/src/effects/SphericalProjection.cpp index 19cdbea8b..2cfed3c6f 100644 --- a/src/effects/SphericalProjection.cpp +++ b/src/effects/SphericalProjection.cpp @@ -202,8 +202,13 @@ SphericalProjection::GetFrame(std::shared_ptr frame, uchar *d = dst_row + xx * 4; if (input_model == 0 && projection_mode == 0) { + // Wrap horizontally for full equirectangular images uf = std::fmod(std::fmod(uf, W) + W, W); vf = std::clamp(vf, 0.0, (double)H - 1); + } else if (projection_mode == 1) { + // In hemisphere mode, clamp UV coordinates to the edge of the source image + uf = std::clamp(uf, 0.0, (double)W - 1); + vf = std::clamp(vf, 0.0, (double)H - 1); } else if (uf < 0 || uf >= W || vf < 0 || vf >= H) { d[0] = d[1] = d[2] = 0; d[3] = 0; From a90b4d6c3ee68b46f855b59a5f2d32aa09958afb Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 10 Sep 2025 17:11:41 -0500 Subject: [PATCH 19/41] Improving fish eye support for SphericalProjection effect (4 types of fish eyes for input and output). Fixing lots of issues with virtual camera controls (so controls don't flip axis randomly) --- src/effects/SphericalProjection.cpp | 870 +++++++++++++++------------- src/effects/SphericalProjection.h | 32 +- 2 files changed, 509 insertions(+), 393 deletions(-) diff --git a/src/effects/SphericalProjection.cpp b/src/effects/SphericalProjection.cpp index 2cfed3c6f..bf676b272 100644 --- a/src/effects/SphericalProjection.cpp +++ b/src/effects/SphericalProjection.cpp @@ -21,425 +21,511 @@ using namespace openshot; SphericalProjection::SphericalProjection() - : yaw(0.0), pitch(0.0), roll(0.0), fov(90.0), in_fov(180.0), - projection_mode(0), invert(0), input_model(0), interpolation(3) { - init_effect_details(); + : yaw(0.0), pitch(0.0), roll(0.0), fov(90.0), in_fov(180.0), + projection_mode(0), invert(0), input_model(INPUT_EQUIRECT), interpolation(3) +{ + 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), - in_fov(180.0), projection_mode(0), invert(0), input_model(0), - interpolation(3) { - init_effect_details(); + Keyframe new_roll, Keyframe new_fov) + : yaw(new_yaw), pitch(new_pitch), roll(new_roll), fov(new_fov), + in_fov(180.0), projection_mode(0), invert(0), + input_model(INPUT_EQUIRECT), interpolation(3) +{ + init_effect_details(); } void SphericalProjection::init_effect_details() { - InitEffectInfo(); - info.class_name = "SphericalProjection"; - info.name = "Spherical Projection"; - 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; + InitEffectInfo(); + info.class_name = "SphericalProjection"; + info.name = "Spherical Projection"; + info.description = + "Flatten and reproject 360° or fisheye inputs into a rectilinear view with yaw, pitch, roll, and FOV. Supports Equirect and multiple fisheye lens models."; + info.has_audio = false; + info.has_video = true; } namespace { -inline double cubic_interp(double p0, double p1, double p2, double p3, - double t) { - double a0 = -0.5 * p0 + 1.5 * p1 - 1.5 * p2 + 0.5 * p3; - double a1 = p0 - 2.5 * p1 + 2.0 * p2 - 0.5 * p3; - double a2 = -0.5 * p0 + 0.5 * p2; - double a3 = p1; - return ((a0 * t + a1) * t + a2) * t + a3; -} + inline double cubic_interp(double p0, double p1, double p2, double p3, + double t) + { + double a0 = -0.5 * p0 + 1.5 * p1 - 1.5 * p2 + 0.5 * p3; + double a1 = p0 - 2.5 * p1 + 2.0 * p2 - 0.5 * p3; + double a2 = -0.5 * p0 + 0.5 * p2; + double a3 = p1; + return ((a0 * t + a1) * t + a2) * t + a3; + } } // namespace 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(); - - // 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 + M_PI; - double in_fov_r = in_fov.GetValue(frame_number) * M_PI / 180.0; - double out_fov_r = fov.GetValue(frame_number) * M_PI / 180.0; - - // 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); - - 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(out_fov_r * 0.5); - double vy = hx * double(H) / W; - - auto q = [](double a) { return std::llround(a * 1e6); }; - bool recompute = uv_map.empty() || W != cached_width || H != cached_height || - q(yaw_r) != q(cached_yaw) || - q(pitch_r) != q(cached_pitch) || - q(roll_r) != q(cached_roll) || - q(in_fov_r) != q(cached_in_fov) || - q(out_fov_r) != q(cached_out_fov) || - input_model != cached_input_model || - projection_mode != cached_projection_mode || - invert != cached_invert; - - if (recompute) { - uv_map.resize(W * H * 2); + 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(); + + // Keyframes / angles + const double DEG = M_PI / 180.0; + double yaw_r = -yaw.GetValue(frame_number) * DEG; // drag right -> look right + double pitch_r = pitch.GetValue(frame_number) * DEG; // drag up -> look up + double roll_r = -roll.GetValue(frame_number) * DEG; // positive slider -> clockwise on screen + double in_fov_r = in_fov.GetValue(frame_number) * DEG; + double out_fov_r= fov.GetValue(frame_number) * DEG; + + // Apply invert as a 180° yaw for equirect inputs (camera-centric; no mirroring) + if (input_model == INPUT_EQUIRECT && invert == INVERT_BACK) { + yaw_r += M_PI; + } + + // Rotation R = Ry(yaw) * Rx(pitch). (Roll applied in screen space.) + double sy = sin(yaw_r), cy = cos(yaw_r); + double sp = sin(pitch_r),cp = cos(pitch_r); + + double r00 = cy; + double r01 = sy * sp; + double r02 = sy * cp; + + double r10 = 0.0; + double r11 = cp; + double r12 = -sp; + + double r20 = -sy; + double r21 = cy * sp; + double r22 = cy * cp; + + // Keep roll clockwise on screen regardless of facing direction + double roll_sign = (r22 >= 0.0) ? 1.0 : -1.0; + + // Perspective scalars (rectilinear) + double hx = tan(out_fov_r * 0.5); + double vy = hx * double(H) / W; + + auto q = [](double a) { return std::llround(a * 1e6); }; + bool recompute = uv_map.empty() || W != cached_width || H != cached_height || + q(yaw_r) != q(cached_yaw) || + q(pitch_r) != q(cached_pitch) || + q(roll_r) != q(cached_roll) || + q(in_fov_r) != q(cached_in_fov) || + q(out_fov_r) != q(cached_out_fov) || + input_model != cached_input_model || + projection_mode != cached_projection_mode || + invert != cached_invert; + + if (recompute) { + uv_map.resize(W * H * 2); + #pragma omp parallel for schedule(static) - for (int yy = 0; yy < H; yy++) { - double ndc_y = (2.0 * (yy + 0.5) / H - 1.0) * vy; - for (int xx = 0; xx < W; xx++) { - double ndc_x = (2.0 * (xx + 0.5) / W - 1.0) * hx; - double vx = ndc_x, vy2 = -ndc_y, vz = -1.0; - double inv = 1.0 / sqrt(vx * vx + vy2 * vy2 + vz * vz); - vx *= inv; - vy2 *= inv; - vz *= inv; - - double dx = r00 * vx + r01 * vy2 + r02 * vz; - double dy = r10 * vx + r11 * vy2 + r12 * vz; - double dz = r20 * vx + r21 * vy2 + r22 * vz; - - if (projection_mode < 2 && invert) { - dx = -dx; - dz = -dz; - } - - double uf, vf; - project_input(dx, dy, dz, in_fov_r, W, H, uf, vf); - int idx = 2 * (yy * W + xx); - uv_map[idx] = (float)uf; - uv_map[idx + 1] = (float)vf; - } - } - cached_width = W; - cached_height = H; - cached_yaw = yaw_r; - cached_pitch = pitch_r; - cached_roll = roll_r; - cached_in_fov = in_fov_r; - cached_out_fov = out_fov_r; - cached_input_model = input_model; - cached_projection_mode = projection_mode; - cached_invert = invert; - } - - // Automatic sampler selection - int sampler = interpolation; - if (interpolation == 3) { - double coverage_r = (projection_mode == 0 ? 2.0 * M_PI - : projection_mode == 1 ? M_PI - : in_fov_r); - double ppd_src = W / coverage_r; - double ppd_out = W / out_fov_r; - double ratio = ppd_out / ppd_src; - if (ratio < 0.8) - sampler = 3; // mipmaps - else if (ratio <= 1.2) - sampler = 1; // bilinear - else - sampler = 2; // bicubic - } - - // Build mipmaps only if needed (simple box filter) - std::vector mipmaps; - if (sampler == 3) { - mipmaps.push_back(*img); - for (int level = 1; level < 4; ++level) { - const QImage &prev = mipmaps[level - 1]; - if (prev.width() <= 1 || prev.height() <= 1) - break; - int w = prev.width() / 2; - int h = prev.height() / 2; - QImage next(w, h, QImage::Format_ARGB32); - uchar *nb = next.bits(); - int nbpl = next.bytesPerLine(); - const uchar *pb = prev.bits(); - int pbpl = prev.bytesPerLine(); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - for (int c = 0; c < 4; c++) { - int p00 = pb[(2 * y) * pbpl + (2 * x) * 4 + c]; - int p10 = pb[(2 * y) * pbpl + (2 * x + 1) * 4 + c]; - int p01 = pb[(2 * y + 1) * pbpl + (2 * x) * 4 + c]; - int p11 = pb[(2 * y + 1) * pbpl + (2 * x + 1) * 4 + c]; - nb[y * nbpl + x * 4 + c] = (p00 + p10 + p01 + p11) / 4; - } - } - } - mipmaps.push_back(next); - } - } + for (int yy = 0; yy < H; yy++) { + double ndc_y = (2.0 * (yy + 0.5) / H - 1.0) * vy; + + for (int xx = 0; xx < W; xx++) { + double uf = -1.0, vf = -1.0; + + const bool out_is_rect = + (projection_mode == MODE_RECT_SPHERE || projection_mode == MODE_RECT_HEMISPHERE); + + if (!out_is_rect) { + // ---------------- FISHEYE OUTPUT ---------------- + double cx = (xx + 0.5) - W * 0.5; + double cy_dn = (yy + 0.5) - H * 0.5; + double R = 0.5 * std::min(W, H); + + // screen plane, Y-up; apply roll by -roll (clockwise), adjusted by roll_sign + double rx = cx / R; + double ry_up = -cy_dn / R; + double cR = cos(roll_r), sR = sin(roll_r) * roll_sign; + double rxr = cR * rx + sR * ry_up; + double ryr = -sR * rx + cR * ry_up; + + double r_norm = std::sqrt(rxr * rxr + ryr * ryr); + if (r_norm <= 1.0) { + double theta_max = out_fov_r * 0.5; + double theta = 0.0; + switch (projection_mode) { + case MODE_FISHEYE_EQUIDISTANT: + // r ∝ θ + theta = r_norm * theta_max; + break; + case MODE_FISHEYE_EQUISOLID: + // r ∝ 2 sin(θ/2) + theta = 2.0 * std::asin(std::clamp(r_norm * std::sin(theta_max * 0.5), -1.0, 1.0)); + break; + case MODE_FISHEYE_STEREOGRAPHIC: + // r ∝ 2 tan(θ/2) + theta = 2.0 * std::atan(r_norm * std::tan(theta_max * 0.5)); + break; + case MODE_FISHEYE_ORTHOGRAPHIC: + // r ∝ sin(θ) + theta = std::asin(std::clamp(r_norm * std::sin(theta_max), -1.0, 1.0)); + break; + default: + theta = r_norm * theta_max; + break; + } + + // NOTE: Y was upside-down; fix by using +ryr (not -ryr) + double phi = std::atan2(ryr, rxr); + + // Camera ray from fisheye output + double vx = std::sin(theta) * std::cos(phi); + double vy2= std::sin(theta) * std::sin(phi); + double vz = -std::cos(theta); + + // Rotate into world + double dx = r00 * vx + r01 * vy2 + r02 * vz; + double dy = r10 * vx + r11 * vy2 + r12 * vz; + double dz = r20 * vx + r21 * vy2 + r22 * vz; + + project_input(dx, dy, dz, in_fov_r, W, H, uf, vf); + } else { + uf = vf = -1.0; // outside disk + } + + } else { + // ---------------- RECTILINEAR OUTPUT ---------------- + double ndc_x = (2.0 * (xx + 0.5) / W - 1.0) * hx; + + // screen plane Y-up; roll by -roll (clockwise), adjusted by roll_sign + double sx = ndc_x; + double sy_up = -ndc_y; + double cR = cos(roll_r), sR = sin(roll_r) * roll_sign; + double rx = cR * sx + sR * sy_up; + double ry = -sR * sx + cR * sy_up; + + // Camera ray (camera looks down -Z) + double vx = rx, vy2 = ry, vz = -1.0; + double inv_len = 1.0 / std::sqrt(vx*vx + vy2*vy2 + vz*vz); + vx *= inv_len; vy2 *= inv_len; vz *= inv_len; + + // Rotate into world + double dx = r00 * vx + r01 * vy2 + r02 * vz; + double dy = r10 * vx + r11 * vy2 + r12 * vz; + double dz = r20 * vx + r21 * vy2 + r22 * vz; + + project_input(dx, dy, dz, in_fov_r, W, H, uf, vf); + } + + int idx = 2 * (yy * W + xx); + uv_map[idx] = (float)uf; + uv_map[idx + 1] = (float)vf; + } + } + + cached_width = W; + cached_height = H; + cached_yaw = yaw_r; + cached_pitch = pitch_r; + cached_roll = roll_r; + cached_in_fov = in_fov_r; + cached_out_fov= out_fov_r; + cached_input_model = input_model; + cached_projection_mode = projection_mode; + cached_invert = invert; + } + + // Auto sampler selection (uses enums) + int sampler = interpolation; + if (interpolation == INTERP_AUTO) { + double coverage_r = + (projection_mode == MODE_RECT_SPHERE) ? 2.0 * M_PI : + (projection_mode == MODE_RECT_HEMISPHERE) ? M_PI : + in_fov_r; // rough heuristic otherwise + double ppd_src = W / coverage_r; + double ppd_out = W / out_fov_r; + double ratio = ppd_out / ppd_src; + if (ratio < 0.8) sampler = INTERP_AUTO; // mipmaps path below + else if (ratio <= 1.2) sampler = INTERP_BILINEAR; + else sampler = INTERP_BICUBIC; + } + + // Build mipmaps only if needed (box) + std::vector mipmaps; + if (sampler == INTERP_AUTO) { + mipmaps.push_back(*img); + for (int level = 1; level < 4; ++level) { + const QImage &prev = mipmaps[level - 1]; + if (prev.width() <= 1 || prev.height() <= 1) break; + int w = prev.width() / 2, h = prev.height() / 2; + QImage next(w, h, QImage::Format_ARGB32); + uchar *nb = next.bits(); int nbpl = next.bytesPerLine(); + const uchar *pb = prev.bits(); int pbpl = prev.bytesPerLine(); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + for (int c = 0; c < 4; c++) { + int p00 = pb[(2*y) * pbpl + (2*x) * 4 + c]; + int p10 = pb[(2*y) * pbpl + (2*x+1) * 4 + c]; + int p01 = pb[(2*y+1) * pbpl + (2*x) * 4 + c]; + int p11 = pb[(2*y+1) * pbpl + (2*x+1) * 4 + c]; + nb[y * nbpl + x * 4 + c] = (p00 + p10 + p01 + p11) / 4; + } + } + } + mipmaps.push_back(next); + } + } #pragma omp parallel for schedule(static) - for (int yy = 0; yy < H; yy++) { - uchar *dst_row = dst + yy * dst_bpl; - for (int xx = 0; xx < W; xx++) { - int idx = 2 * (yy * W + xx); - double uf = uv_map[idx]; - double vf = uv_map[idx + 1]; - uchar *d = dst_row + xx * 4; - - if (input_model == 0 && projection_mode == 0) { - // Wrap horizontally for full equirectangular images - uf = std::fmod(std::fmod(uf, W) + W, W); - vf = std::clamp(vf, 0.0, (double)H - 1); - } else if (projection_mode == 1) { - // In hemisphere mode, clamp UV coordinates to the edge of the source image - uf = std::clamp(uf, 0.0, (double)W - 1); - vf = std::clamp(vf, 0.0, (double)H - 1); - } else if (uf < 0 || uf >= W || vf < 0 || vf >= H) { - d[0] = d[1] = d[2] = 0; - d[3] = 0; - continue; - } - - if (sampler == 0) { - int x0 = std::clamp(int(std::floor(uf)), 0, W - 1); - int y0 = std::clamp(int(std::floor(vf)), 0, H - 1); - uchar *s = src + y0 * bpl + x0 * 4; - d[0] = s[0]; - d[1] = s[1]; - d[2] = s[2]; - d[3] = s[3]; - } else if (sampler == 1) { - int x0 = std::clamp(int(std::floor(uf)), 0, W - 1); - int y0 = std::clamp(int(std::floor(vf)), 0, H - 1); - int x1 = std::clamp(x0 + 1, 0, W - 1); - int y1 = std::clamp(y0 + 1, 0, H - 1); - double dxr = uf - x0, dyr = vf - y0; - uchar *p00 = src + y0 * bpl + x0 * 4; - uchar *p10 = src + y0 * bpl + x1 * 4; - uchar *p01 = src + y1 * bpl + x0 * 4; - uchar *p11 = src + y1 * bpl + x1 * 4; - for (int c = 0; c < 4; c++) { - double v0 = p00[c] * (1 - dxr) + p10[c] * dxr; - double v1 = p01[c] * (1 - dxr) + p11[c] * dxr; - d[c] = uchar(v0 * (1 - dyr) + v1 * dyr + 0.5); - } - } else if (sampler == 2) { - int x1 = std::clamp(int(std::floor(uf)), 0, W - 1); - int y1 = std::clamp(int(std::floor(vf)), 0, H - 1); - double tx = uf - x1; - double ty = vf - y1; - for (int c = 0; c < 4; c++) { - double col[4]; - for (int j = -1; j <= 2; j++) { - int y = std::clamp(y1 + j, 0, H - 1); - double row[4]; - for (int i = -1; i <= 2; i++) { - int x = std::clamp(x1 + i, 0, W - 1); - row[i + 1] = src[y * bpl + x * 4 + c]; - } - col[j + 1] = cubic_interp(row[0], row[1], row[2], row[3], tx); - } - double val = cubic_interp(col[0], col[1], col[2], col[3], ty); - d[c] = uchar(std::clamp(val, 0.0, 255.0) + 0.5); - } - } else { - // Mipmap sampling with bilinear - double uf_dx = 0.0, vf_dx = 0.0, uf_dy = 0.0, vf_dy = 0.0; - if (xx + 1 < W) { - uf_dx = uv_map[idx + 2] - uf; - vf_dx = uv_map[idx + 3] - vf; - } - if (yy + 1 < H) { - uf_dy = uv_map[idx + 2 * W] - uf; - vf_dy = uv_map[idx + 2 * W + 1] - vf; - } - double scale_x = std::sqrt(uf_dx * uf_dx + vf_dx * vf_dx); - double scale_y = std::sqrt(uf_dy * uf_dy + vf_dy * vf_dy); - double scale = std::max(scale_x, scale_y); - int level = 0; - if (scale > 1.0) - level = - std::min(std::floor(std::log2(scale)), mipmaps.size() - 1); - const QImage &lvl = mipmaps[level]; - int Wl = lvl.width(), Hl = lvl.height(); - int bpl_l = lvl.bytesPerLine(); - const uchar *srcl = lvl.bits(); - double uf_l = uf / (1 << level); - double vf_l = vf / (1 << level); - int x0 = std::clamp(int(std::floor(uf_l)), 0, Wl - 1); - int y0 = std::clamp(int(std::floor(vf_l)), 0, Hl - 1); - int x1 = std::clamp(x0 + 1, 0, Wl - 1); - int y1 = std::clamp(y0 + 1, 0, Hl - 1); - double dxr = uf_l - x0, dyr = vf_l - y0; - const uchar *p00 = srcl + y0 * bpl_l + x0 * 4; - const uchar *p10 = srcl + y0 * bpl_l + x1 * 4; - const uchar *p01 = srcl + y1 * bpl_l + x0 * 4; - const uchar *p11 = srcl + y1 * bpl_l + x1 * 4; - for (int c = 0; c < 4; c++) { - double v0 = p00[c] * (1 - dxr) + p10[c] * dxr; - double v1 = p01[c] * (1 - dxr) + p11[c] * dxr; - d[c] = uchar(v0 * (1 - dyr) + v1 * dyr + 0.5); - } - } - } - } - - *img = output; - return frame; + for (int yy = 0; yy < H; yy++) { + uchar *dst_row = dst + yy * dst_bpl; + for (int xx = 0; xx < W; xx++) { + int idx = 2 * (yy * W + xx); + double uf = uv_map[idx]; + double vf = uv_map[idx + 1]; + uchar *d = dst_row + xx * 4; + + if (input_model == INPUT_EQUIRECT && projection_mode == MODE_RECT_SPHERE) { + uf = std::fmod(std::fmod(uf, W) + W, W); + vf = std::clamp(vf, 0.0, (double)H - 1); + } else if (input_model == INPUT_EQUIRECT && projection_mode == MODE_RECT_HEMISPHERE) { + uf = std::clamp(uf, 0.0, (double)W - 1); + vf = std::clamp(vf, 0.0, (double)H - 1); + } else if (uf < 0 || uf >= W || vf < 0 || vf >= H) { + d[0] = d[1] = d[2] = 0; d[3] = 0; + continue; + } + + if (sampler == INTERP_NEAREST) { + int x0 = std::clamp(int(std::floor(uf)), 0, W - 1); + int y0 = std::clamp(int(std::floor(vf)), 0, H - 1); + uchar *s = src + y0 * bpl + x0 * 4; + d[0]=s[0]; d[1]=s[1]; d[2]=s[2]; d[3]=s[3]; + } else if (sampler == INTERP_BILINEAR) { + int x0 = std::clamp(int(std::floor(uf)), 0, W - 1); + int y0 = std::clamp(int(std::floor(vf)), 0, H - 1); + int x1 = std::clamp(x0 + 1, 0, W - 1); + int y1 = std::clamp(y0 + 1, 0, H - 1); + double dxr = uf - x0, dyr = vf - y0; + uchar *p00 = src + y0 * bpl + x0 * 4; + uchar *p10 = src + y0 * bpl + x1 * 4; + uchar *p01 = src + y1 * bpl + x0 * 4; + uchar *p11 = src + y1 * bpl + x1 * 4; + for (int c = 0; c < 4; c++) { + double v0 = p00[c] * (1 - dxr) + p10[c] * dxr; + double v1 = p01[c] * (1 - dxr) + p11[c] * dxr; + d[c] = uchar(v0 * (1 - dyr) + v1 * dyr + 0.5); + } + } else if (sampler == INTERP_BICUBIC) { + int x1 = std::clamp(int(std::floor(uf)), 0, W - 1); + int y1 = std::clamp(int(std::floor(vf)), 0, H - 1); + double tx = uf - x1, ty = vf - y1; + for (int c = 0; c < 4; c++) { + double col[4]; + for (int j = -1; j <= 2; j++) { + int y = std::clamp(y1 + j, 0, H - 1); + double row[4]; + for (int i = -1; i <= 2; i++) { + int x = std::clamp(x1 + i, 0, W - 1); + row[i + 1] = src[y * bpl + x * 4 + c]; + } + col[j + 1] = cubic_interp(row[0], row[1], row[2], row[3], tx); + } + double val = cubic_interp(col[0], col[1], col[2], col[3], ty); + d[c] = uchar(std::clamp(val, 0.0, 255.0) + 0.5); + } + } else { // INTERP_AUTO -> mipmaps + bilinear + double uf_dx = 0.0, vf_dx = 0.0, uf_dy = 0.0, vf_dy = 0.0; + if (xx + 1 < W) { uf_dx = uv_map[idx + 2] - uf; vf_dx = uv_map[idx + 3] - vf; } + if (yy + 1 < H) { uf_dy = uv_map[idx + 2 * W] - uf; vf_dy = uv_map[idx + 2 * W + 1] - vf; } + double scale_x = std::sqrt(uf_dx*uf_dx + vf_dx*vf_dx); + double scale_y = std::sqrt(uf_dy*uf_dy + vf_dy*vf_dy); + double scale = std::max(scale_x, scale_y); + int level = 0; + if (scale > 1.0) + level = std::min(std::floor(std::log2(scale)), (int)mipmaps.size() - 1); + const QImage &lvl = mipmaps[level]; + int Wl = lvl.width(), Hl = lvl.height(); + int bpl_l = lvl.bytesPerLine(); + const uchar *srcl = lvl.bits(); + double uf_l = uf / (1 << level); + double vf_l = vf / (1 << level); + int x0 = std::clamp(int(std::floor(uf_l)), 0, Wl - 1); + int y0 = std::clamp(int(std::floor(vf_l)), 0, Hl - 1); + int x1 = std::clamp(x0 + 1, 0, Wl - 1); + int y1 = std::clamp(y0 + 1, 0, Hl - 1); + double dxr = uf_l - x0, dyr = vf_l - y0; + const uchar *p00 = srcl + y0 * bpl_l + x0 * 4; + const uchar *p10 = srcl + y0 * bpl_l + x1 * 4; + const uchar *p01 = srcl + y1 * bpl_l + x0 * 4; + const uchar *p11 = srcl + y1 * bpl_l + x1 * 4; + for (int c = 0; c < 4; c++) { + double v0 = p00[c] * (1 - dxr) + p10[c] * dxr; + double v1 = p01[c] * (1 - dxr) + p11[c] * dxr; + d[c] = uchar(v0 * (1 - dyr) + v1 * dyr + 0.5); + } + } + } + } + + *img = output; + return frame; } void SphericalProjection::project_input(double dx, double dy, double dz, - double in_fov_r, int W, int H, - double &uf, double &vf) const { - if (input_model == 0) { - // Equirectangular - double lon = atan2(dx, dz); - double lat = asin(dy); - if (projection_mode == 1) - lon = std::clamp(lon, -M_PI / 2.0, M_PI / 2.0); - uf = ((lon + (projection_mode ? M_PI / 2.0 : M_PI)) / - (projection_mode ? M_PI : 2.0 * M_PI)) * - W; - vf = (lat + M_PI / 2.0) / M_PI * H; - } else { - // Fisheye equidistant - double ax = 0.0, ay = 0.0, az = invert ? -1.0 : 1.0; - double cos_t = dx * ax + dy * ay + dz * az; - cos_t = std::clamp(cos_t, -1.0, 1.0); - double theta = acos(cos_t); - double theta_max = in_fov_r * 0.5; - double r_norm = theta / theta_max; - double R = 0.5 * std::min(W, H); - double rpx = r_norm * R; - double phi = atan2(dy, dx); - uf = W * 0.5 + rpx * cos(phi); - vf = H * 0.5 + rpx * sin(phi); - } + double in_fov_r, int W, int H, + double &uf, double &vf) const { + if (input_model == INPUT_EQUIRECT) { + // Center (-Z) -> lon=0; +X (screen right) -> +lon + double lon = std::atan2(dx, -dz); + double lat = std::asin(std::clamp(dy, -1.0, 1.0)); + + if (projection_mode == MODE_RECT_HEMISPHERE) + lon = std::clamp(lon, -M_PI / 2.0, M_PI / 2.0); + + double horiz_span = (projection_mode == MODE_RECT_HEMISPHERE) ? M_PI : 2.0 * M_PI; + double lon_offset = (projection_mode == MODE_RECT_HEMISPHERE) ? M_PI / 2.0 : M_PI; + uf = ((lon + lon_offset) / horiz_span) * W; + + // Image Y grows downward: north (lat = +π/2) at top + vf = (M_PI / 2.0 - lat) / M_PI * H; + return; + } + + // -------- Fisheye inputs -------- + // Optical axis default is -Z; "Invert" flips hemisphere. + const double ax = 0.0, ay = 0.0; + double az = -1.0; + if (invert == INVERT_BACK) az = 1.0; + + double cos_t = std::clamp(dx * ax + dy * ay + dz * az, -1.0, 1.0); + double theta = std::acos(cos_t); + double tmax = std::max(1e-6, in_fov_r * 0.5); + + double r_norm = 0.0; + switch (input_model) { + case INPUT_FEQ_EQUIDISTANT: r_norm = theta / tmax; break; + case INPUT_FEQ_EQUISOLID: r_norm = std::sin(theta*0.5) / std::max(1e-12, std::sin(tmax*0.5)); break; + case INPUT_FEQ_STEREOGRAPHIC: r_norm = std::tan(theta*0.5) / std::max(1e-12, std::tan(tmax*0.5)); break; + case INPUT_FEQ_ORTHOGRAPHIC: r_norm = std::sin(theta) / std::max(1e-12, std::sin(tmax)); break; + default: r_norm = theta / tmax; break; + } + + // Azimuth in camera XY; final Y is downward -> subtract sine in vf + double phi = std::atan2(dy, dx); + + double R = 0.5 * std::min(W, H); + double rpx = r_norm * R; + uf = W * 0.5 + rpx * std::cos(phi); + vf = H * 0.5 - rpx * std::sin(phi); } -std::string SphericalProjection::Json() const { - return JsonValue().toStyledString(); +std::string SphericalProjection::Json() const +{ + return JsonValue().toStyledString(); } -Json::Value SphericalProjection::JsonValue() const { - Json::Value root = EffectBase::JsonValue(); - root["type"] = info.class_name; - root["yaw"] = yaw.JsonValue(); - root["pitch"] = pitch.JsonValue(); - root["roll"] = roll.JsonValue(); - root["fov"] = fov.JsonValue(); - root["in_fov"] = in_fov.JsonValue(); - root["projection_mode"] = projection_mode; - root["invert"] = invert; - root["input_model"] = input_model; - root["interpolation"] = interpolation; - return root; +Json::Value SphericalProjection::JsonValue() const +{ + Json::Value root = EffectBase::JsonValue(); + root["type"] = info.class_name; + root["yaw"] = yaw.JsonValue(); + root["pitch"] = pitch.JsonValue(); + root["roll"] = roll.JsonValue(); + root["fov"] = fov.JsonValue(); + root["in_fov"] = in_fov.JsonValue(); + root["projection_mode"] = projection_mode; + root["invert"] = invert; + root["input_model"] = input_model; + root["interpolation"] = interpolation; + return root; } -void SphericalProjection::SetJson(const std::string value) { - try { - Json::Value root = openshot::stringToJson(value); - SetJsonValue(root); - } catch (...) { - throw InvalidJSON("Invalid JSON for SphericalProjection"); - } +void SphericalProjection::SetJson(const std::string value) +{ + try + { + Json::Value root = openshot::stringToJson(value); + SetJsonValue(root); + } + catch (...) + { + throw InvalidJSON("Invalid JSON for SphericalProjection"); + } } -void SphericalProjection::SetJsonValue(const Json::Value root) { - EffectBase::SetJsonValue(root); - if (!root["yaw"].isNull()) - yaw.SetJsonValue(root["yaw"]); - if (!root["pitch"].isNull()) - pitch.SetJsonValue(root["pitch"]); - if (!root["roll"].isNull()) - roll.SetJsonValue(root["roll"]); - if (!root["fov"].isNull()) - fov.SetJsonValue(root["fov"]); - if (!root["in_fov"].isNull()) - in_fov.SetJsonValue(root["in_fov"]); - if (!root["projection_mode"].isNull()) - projection_mode = root["projection_mode"].asInt(); - if (!root["invert"].isNull()) - invert = root["invert"].asInt(); - if (!root["input_model"].isNull()) - input_model = root["input_model"].asInt(); - if (!root["interpolation"].isNull()) - interpolation = root["interpolation"].asInt(); - - // any property change should invalidate cached UV map - uv_map.clear(); -} +void SphericalProjection::SetJsonValue(const Json::Value root) +{ + EffectBase::SetJsonValue(root); + + if (!root["yaw"].isNull()) yaw.SetJsonValue(root["yaw"]); + if (!root["pitch"].isNull()) pitch.SetJsonValue(root["pitch"]); + if (!root["roll"].isNull()) roll.SetJsonValue(root["roll"]); + if (!root["fov"].isNull()) fov.SetJsonValue(root["fov"]); + if (!root["in_fov"].isNull()) in_fov.SetJsonValue(root["in_fov"]); -std::string SphericalProjection::PropertiesJSON(int64_t requested_frame) const { - Json::Value root = BasePropertiesJSON(requested_frame); - - root["yaw"] = - add_property_json("Yaw", yaw.GetValue(requested_frame), "float", - "degrees", &yaw, -180, 180, false, requested_frame); - root["pitch"] = - add_property_json("Pitch", pitch.GetValue(requested_frame), "float", - "degrees", &pitch, -90, 90, false, requested_frame); - root["roll"] = - add_property_json("Roll", roll.GetValue(requested_frame), "float", - "degrees", &roll, -180, 180, false, requested_frame); - root["fov"] = - add_property_json("Out FOV", fov.GetValue(requested_frame), "float", - "degrees", &fov, 1, 179, false, requested_frame); - root["in_fov"] = - add_property_json("In FOV", in_fov.GetValue(requested_frame), "float", - "degrees", &in_fov, 1, 360, false, requested_frame); - - root["projection_mode"] = - add_property_json("Projection Mode", projection_mode, "int", "", nullptr, - 0, 2, false, requested_frame); - root["projection_mode"]["choices"].append( - add_property_choice_json("Sphere", 0, projection_mode)); - root["projection_mode"]["choices"].append( - add_property_choice_json("Hemisphere", 1, projection_mode)); - root["projection_mode"]["choices"].append( - add_property_choice_json("Fisheye", 2, projection_mode)); - - root["invert"] = add_property_json("Invert View", invert, "int", "", nullptr, - 0, 1, false, requested_frame); - root["invert"]["choices"].append( - add_property_choice_json("Normal", 0, invert)); - root["invert"]["choices"].append( - add_property_choice_json("Invert", 1, invert)); - - root["input_model"] = - add_property_json("Input Model", input_model, "int", "", nullptr, 0, 3, - false, requested_frame); - root["input_model"]["choices"].append( - add_property_choice_json("Equirect", 0, input_model)); - root["input_model"]["choices"].append( - add_property_choice_json("Fisheye Equidistant", 1, input_model)); - - root["interpolation"] = - add_property_json("Interpolation", interpolation, "int", "", nullptr, 0, - 3, false, requested_frame); - root["interpolation"]["choices"].append( - add_property_choice_json("Nearest", 0, interpolation)); - root["interpolation"]["choices"].append( - add_property_choice_json("Bilinear", 1, interpolation)); - root["interpolation"]["choices"].append( - add_property_choice_json("Bicubic", 2, interpolation)); - root["interpolation"]["choices"].append( - add_property_choice_json("Auto", 3, interpolation)); - - return root.toStyledString(); + if (!root["projection_mode"].isNull()) + projection_mode = root["projection_mode"].asInt(); + + if (!root["invert"].isNull()) + invert = root["invert"].asInt(); + + if (!root["input_model"].isNull()) + input_model = root["input_model"].asInt(); + + if (!root["interpolation"].isNull()) + interpolation = root["interpolation"].asInt(); + + // Clamp to enum options + projection_mode = std::clamp(projection_mode, + (int)MODE_RECT_SPHERE, + (int)MODE_FISHEYE_ORTHOGRAPHIC); + invert = std::clamp(invert, (int)INVERT_NORMAL, (int)INVERT_BACK); + input_model = std::clamp(input_model, (int)INPUT_EQUIRECT, (int)INPUT_FEQ_ORTHOGRAPHIC); + interpolation = std::clamp(interpolation, (int)INTERP_NEAREST, (int)INTERP_AUTO); + + // any property change should invalidate cached UV map + uv_map.clear(); + + + // any property change should invalidate cached UV map + uv_map.clear(); +} +std::string SphericalProjection::PropertiesJSON(int64_t requested_frame) const +{ + Json::Value root = BasePropertiesJSON(requested_frame); + + root["yaw"] = add_property_json("Yaw", yaw.GetValue(requested_frame), "float", "degrees", &yaw, -180, 180, false, requested_frame); + root["pitch"] = add_property_json("Pitch", pitch.GetValue(requested_frame), "float", "degrees", &pitch,-180, 180, false, requested_frame); + root["roll"] = add_property_json("Roll", roll.GetValue(requested_frame), "float", "degrees", &roll, -180, 180, false, requested_frame); + + root["fov"] = add_property_json("Out FOV", fov.GetValue(requested_frame), "float", "degrees", &fov, 0, 179, false, requested_frame); + root["in_fov"] = add_property_json("In FOV", in_fov.GetValue(requested_frame), "float", "degrees", &in_fov, 1, 360, false, requested_frame); + + root["projection_mode"] = add_property_json("Projection Mode", projection_mode, "int", "", nullptr, + (int)MODE_RECT_SPHERE, (int)MODE_FISHEYE_ORTHOGRAPHIC, false, requested_frame); + root["projection_mode"]["choices"].append(add_property_choice_json("Sphere", (int)MODE_RECT_SPHERE, projection_mode)); + root["projection_mode"]["choices"].append(add_property_choice_json("Hemisphere", (int)MODE_RECT_HEMISPHERE, projection_mode)); + root["projection_mode"]["choices"].append(add_property_choice_json("Fisheye: Equidistant", (int)MODE_FISHEYE_EQUIDISTANT, projection_mode)); + root["projection_mode"]["choices"].append(add_property_choice_json("Fisheye: Equisolid", (int)MODE_FISHEYE_EQUISOLID, projection_mode)); + root["projection_mode"]["choices"].append(add_property_choice_json("Fisheye: Stereographic", (int)MODE_FISHEYE_STEREOGRAPHIC,projection_mode)); + root["projection_mode"]["choices"].append(add_property_choice_json("Fisheye: Orthographic", (int)MODE_FISHEYE_ORTHOGRAPHIC, projection_mode)); + + root["invert"] = add_property_json("Invert View", invert, "int", "", nullptr, 0, 1, false, requested_frame); + root["invert"]["choices"].append(add_property_choice_json("Normal", 0, invert)); + root["invert"]["choices"].append(add_property_choice_json("Invert", 1, invert)); + + root["input_model"] = add_property_json("Input Model", input_model, "int", "", nullptr, INPUT_EQUIRECT, INPUT_FEQ_ORTHOGRAPHIC, false, requested_frame); + root["input_model"]["choices"].append(add_property_choice_json("Equirectangular (Panorama)", INPUT_EQUIRECT, input_model)); + root["input_model"]["choices"].append(add_property_choice_json("Fisheye: Equidistant", INPUT_FEQ_EQUIDISTANT, input_model)); + root["input_model"]["choices"].append(add_property_choice_json("Fisheye: Equisolid", INPUT_FEQ_EQUISOLID, input_model)); + root["input_model"]["choices"].append(add_property_choice_json("Fisheye: Stereographic", INPUT_FEQ_STEREOGRAPHIC, input_model)); + root["input_model"]["choices"].append(add_property_choice_json("Fisheye: Orthographic", INPUT_FEQ_ORTHOGRAPHIC, input_model)); + + root["interpolation"] = add_property_json("Interpolation", interpolation, "int", "", nullptr, 0, 3, false, requested_frame); + root["interpolation"]["choices"].append(add_property_choice_json("Nearest", 0, interpolation)); + root["interpolation"]["choices"].append(add_property_choice_json("Bilinear", 1, interpolation)); + root["interpolation"]["choices"].append(add_property_choice_json("Bicubic", 2, interpolation)); + root["interpolation"]["choices"].append(add_property_choice_json("Auto", 3, interpolation)); + + return root.toStyledString(); } diff --git a/src/effects/SphericalProjection.h b/src/effects/SphericalProjection.h index 7d86992a7..c8f77e274 100644 --- a/src/effects/SphericalProjection.h +++ b/src/effects/SphericalProjection.h @@ -34,10 +34,40 @@ class SphericalProjection : public EffectBase { void init_effect_details(); public: + // Enums + enum InputModel { + INPUT_EQUIRECT = 0, + INPUT_FEQ_EQUIDISTANT = 1, // r = f * theta + INPUT_FEQ_EQUISOLID = 2, // r = 2f * sin(theta/2) + INPUT_FEQ_STEREOGRAPHIC = 3, // r = 2f * tan(theta/2) + INPUT_FEQ_ORTHOGRAPHIC = 4 // r = f * sin(theta) + }; + + enum ProjectionMode { + MODE_RECT_SPHERE = 0, // Rectilinear view over full sphere + MODE_RECT_HEMISPHERE = 1, // Rectilinear view over hemisphere + MODE_FISHEYE_EQUIDISTANT = 2, // Output fisheye (equidistant) + MODE_FISHEYE_EQUISOLID = 3, // Output fisheye (equisolid) + MODE_FISHEYE_STEREOGRAPHIC = 4, // Output fisheye (stereographic) + MODE_FISHEYE_ORTHOGRAPHIC = 5 // Output fisheye (orthographic) + }; + + enum InterpMode { + INTERP_NEAREST = 0, + INTERP_BILINEAR = 1, + INTERP_BICUBIC = 2, + INTERP_AUTO = 3 + }; + + enum InvertFlag { + INVERT_NORMAL = 0, + INVERT_BACK = 1 + }; + Keyframe yaw; ///< Yaw around up-axis (degrees) Keyframe pitch; ///< Pitch around right-axis (degrees) Keyframe roll; ///< Roll around forward-axis (degrees) - Keyframe fov; ///< Output field-of-view (horizontal, degrees) + Keyframe fov; ///< Output field-of-view (degrees) Keyframe in_fov; ///< Source lens coverage / FOV (degrees) int projection_mode; ///< 0=Sphere, 1=Hemisphere, 2=Fisheye From a07fe18a14d79bbf467bd52dd49d933689d1c3a8 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 10 Sep 2025 21:33:37 -0500 Subject: [PATCH 20/41] Adding 5 Spherical test images (needed for unit tests) --- examples/eq_sphere_plane.png | Bin 0 -> 11870 bytes examples/fisheye_plane_equidistant.png | Bin 0 -> 116040 bytes examples/fisheye_plane_equisolid.png | Bin 0 -> 108288 bytes examples/fisheye_plane_orthographic.png | Bin 0 -> 67163 bytes examples/fisheye_plane_stereographic.png | Bin 0 -> 131034 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/eq_sphere_plane.png create mode 100644 examples/fisheye_plane_equidistant.png create mode 100644 examples/fisheye_plane_equisolid.png create mode 100644 examples/fisheye_plane_orthographic.png create mode 100644 examples/fisheye_plane_stereographic.png diff --git a/examples/eq_sphere_plane.png b/examples/eq_sphere_plane.png new file mode 100644 index 0000000000000000000000000000000000000000..ac574b514881fbf45c99972427f1fca1956aecdf GIT binary patch literal 11870 zcmeHNZA=?w9DhK1XFjE8ulL%Q54Xd?9I4Hhk z?t_J30%n0?j0qzMWrCehxX!n92BC4njsfL13p7&JTfwetkM?}7_G(K%n3!B5@wqSk zr@1tDPoLla|9g2ZoRA#bx&4vt0Dzr&xj82Rh=FAkh>3%0al`zxzrh=+6@@lK-8p%t*X*&{9)M5DUP^1!Uj+cW zh{YiGC8TB|w`9wgb@(na70XO!$yIR6S=( zT%7Tvb1=zRK=xUy)(%!fw8~|oBB4=Y8JMn8kDpu75hIgX2PP$!yZx$4t^uqSORy{W zhPU)gF#d(cMhEZthJ`vc-y=#PFxYkUM;EK-Y~qBoq*IrF?J_rw;na28((q57 zIJ;>Yd+Q~bGfOpm{s%bE7pCoR$V8c)-=EUBMvWeC99eEls0nEV_Kj^Eu+f+Wh+hCi ztoR|RcV8hKa_7UTG)_X&t*s6w-Cv3Fu`B(WpW25)L0g+V-b0ONIv zw{&JDbzGY6FuB+0#t%CuZ+C@kL;cB>gTSIbKX z(sXQUm2Pl8nCJ7yRDqehCUTfY@IztF*lUeBc4@Px`atu8-z*6FeBJ?9tN%#Oyr#pS`)8MMv`%apu<(OeEO&i}YaGl?_7 zBHgea$2~O~O=vX1y@1-qKeh{{VWeNLYrb@oMPaM9T1S5%;7snerP>vp40{3pVj|S! zmKH*n>{TMnB`Z`z!F{8hWF8OAvRu13V4}I%m z$J71EGEpz43P_Z5B9l9jUI?r$S9`NEG`hy=9_3oa_uKZ@uk(x9yq~B z3LZFNy{*83lZrw$oVf(C`21{$GA2ar(B#(0I>T_iG~D5S6LWd=-vvu=#JJ&VBOiS0 z7+V|qlFSzNRK@pU=qL_A`vY-TSvL3Gf8@r~rNE LmE`mvDSPivUhpnq literal 0 HcmV?d00001 diff --git a/examples/fisheye_plane_equidistant.png b/examples/fisheye_plane_equidistant.png new file mode 100644 index 0000000000000000000000000000000000000000..6fb2a60b715b4dda608c5eb3feca8157e3fbeeab GIT binary patch literal 116040 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 literal 0 HcmV?d00001 diff --git a/examples/fisheye_plane_equisolid.png b/examples/fisheye_plane_equisolid.png new file mode 100644 index 0000000000000000000000000000000000000000..811eada216635fd5c262470e3b8b942a4fc1014a GIT binary patch literal 108288 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 literal 0 HcmV?d00001 diff --git a/examples/fisheye_plane_orthographic.png b/examples/fisheye_plane_orthographic.png new file mode 100644 index 0000000000000000000000000000000000000000..becb2e8bb78856e675188e9938f7eb8ea6969f4f GIT binary patch literal 67163 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�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

LhUISoR@6eOt<9 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 z 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

xK&$Oz}rI3rg%Tvdg@mpvr(*jiN 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 literal 0 HcmV?d00001 diff --git a/examples/fisheye_plane_stereographic.png b/examples/fisheye_plane_stereographic.png new file mode 100644 index 0000000000000000000000000000000000000000..a727793933e5d5d072cea404a4d3b380052f7741 GIT binary patch literal 131034 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 fPb$S~Ze6mv-r2Y0P|l Date: Wed, 10 Sep 2025 22:44:30 -0500 Subject: [PATCH 21/41] Adding new Analog Tap effect: Vintage home video wobble, bleed, and grain. --- src/CMakeLists.txt | 1 + src/EffectInfo.cpp | 5 + src/Effects.h | 1 + src/effects/AnalogTape.cpp | 447 +++++++++++++++++++++++++++++++++++++ src/effects/AnalogTape.h | 130 +++++++++++ tests/AnalogTape.cpp | 85 +++++++ tests/CMakeLists.txt | 1 + 7 files changed, 670 insertions(+) create mode 100644 src/effects/AnalogTape.cpp create mode 100644 src/effects/AnalogTape.h create mode 100644 tests/AnalogTape.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 153b2c1ec..249122ea3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -117,6 +117,7 @@ set(EFFECTS_SOURCES effects/Deinterlace.cpp effects/Hue.cpp effects/LensFlare.cpp + effects/AnalogTape.cpp effects/Mask.cpp effects/Negate.cpp effects/Pixelate.cpp diff --git a/src/EffectInfo.cpp b/src/EffectInfo.cpp index a9f67028d..dbedf534f 100644 --- a/src/EffectInfo.cpp +++ b/src/EffectInfo.cpp @@ -12,6 +12,7 @@ #include "EffectInfo.h" #include "Effects.h" +#include "effects/AnalogTape.h" using namespace openshot; @@ -25,6 +26,9 @@ std::string EffectInfo::Json() { // Create a new effect instance EffectBase* EffectInfo::CreateEffect(std::string effect_type) { // Init the matching effect object + if (effect_type == "AnalogTape") + return new AnalogTape(); + if (effect_type == "Bars") return new Bars(); @@ -133,6 +137,7 @@ Json::Value EffectInfo::JsonValue() { Json::Value root; // Append info JSON from each supported effect + root.append(AnalogTape().JsonInfo()); root.append(Bars().JsonInfo()); root.append(Blur().JsonInfo()); root.append(Brightness().JsonInfo()); diff --git a/src/Effects.h b/src/Effects.h index ad577f32a..c69776f9d 100644 --- a/src/Effects.h +++ b/src/Effects.h @@ -14,6 +14,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later /* Effects */ +#include "effects/AnalogTape.h" #include "effects/Bars.h" #include "effects/Blur.h" #include "effects/Brightness.h" diff --git a/src/effects/AnalogTape.cpp b/src/effects/AnalogTape.cpp new file mode 100644 index 000000000..318707d89 --- /dev/null +++ b/src/effects/AnalogTape.cpp @@ -0,0 +1,447 @@ +/** + * @file + * @brief Source file for AnalogTape effect class + * @author Jonathan Thomas + */ + +#include "AnalogTape.h" +#include "Clip.h" +#include "Exceptions.h" +#include "ReaderBase.h" +#include "Timeline.h" + +#include +#include + +using namespace openshot; + +AnalogTape::AnalogTape() + : tracking(0.55), bleed(0.65), softness(0.40), noise(0.50), stripe(0.25f), + staticBands(0.20f), seed_offset(0) { + init_effect_details(); +} + +AnalogTape::AnalogTape(Keyframe t, Keyframe b, Keyframe s, Keyframe n, + Keyframe st, Keyframe sb, int seed) + : tracking(t), bleed(b), softness(s), noise(n), stripe(st), + staticBands(sb), seed_offset(seed) { + init_effect_details(); +} + +void AnalogTape::init_effect_details() { + InitEffectInfo(); + info.class_name = "AnalogTape"; + info.name = "Analog Tape"; + info.description = "Vintage home video wobble, bleed, and grain."; + info.has_video = true; + info.has_audio = false; +} + +static inline float lerp(float a, float b, float t) { return a + (b - a) * t; } + +std::shared_ptr AnalogTape::GetFrame(std::shared_ptr frame, + int64_t frame_number) { + std::shared_ptr img = frame->GetImage(); + int w = img->width(); + int h = img->height(); + int Uw = (w + 1) / 2; + int stride = img->bytesPerLine() / 4; + uint32_t *base = reinterpret_cast(img->bits()); + + if (w != last_w || h != last_h) { + last_w = w; + last_h = h; + Y.resize(w * h); + U.resize(Uw * h); + V.resize(Uw * h); + tmpY.resize(w * h); + tmpU.resize(Uw * h); + tmpV.resize(Uw * h); + dx.resize(h); + } + + +#ifdef _OPENMP +#pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) { + uint32_t *row = base + y * stride; + float *yrow = &Y[y * w]; + float *urow = &U[y * Uw]; + float *vrow = &V[y * Uw]; + for (int x2 = 0; x2 < Uw; ++x2) { + int x0 = x2 * 2; + uint32_t p0 = row[x0]; + float r0 = ((p0 >> 16) & 0xFF) / 255.0f; + float g0 = ((p0 >> 8) & 0xFF) / 255.0f; + float b0 = (p0 & 0xFF) / 255.0f; + float y0 = 0.299f * r0 + 0.587f * g0 + 0.114f * b0; + float u0 = -0.14713f * r0 - 0.28886f * g0 + 0.436f * b0; + float v0 = 0.615f * r0 - 0.51499f * g0 - 0.10001f * b0; + yrow[x0] = y0; + + float u, v; + if (x0 + 1 < w) { + uint32_t p1 = row[x0 + 1]; + float r1 = ((p1 >> 16) & 0xFF) / 255.0f; + float g1 = ((p1 >> 8) & 0xFF) / 255.0f; + float b1 = (p1 & 0xFF) / 255.0f; + float y1 = 0.299f * r1 + 0.587f * g1 + 0.114f * b1; + float u1 = -0.14713f * r1 - 0.28886f * g1 + 0.436f * b1; + float v1 = 0.615f * r1 - 0.51499f * g1 - 0.10001f * b1; + yrow[x0 + 1] = y1; + u = (u0 + u1) * 0.5f; + v = (v0 + v1) * 0.5f; + } else { + u = u0; + v = v0; + } + urow[x2] = u; + vrow[x2] = v; + } + } + + Fraction fps(1, 1); + Clip *clip = (Clip *)ParentClip(); + Timeline *timeline = nullptr; + if (clip && clip->ParentTimeline()) + timeline = (Timeline *)clip->ParentTimeline(); + else if (ParentTimeline()) + timeline = (Timeline *)ParentTimeline(); + if (timeline) + fps = timeline->info.fps; + else if (clip && clip->Reader()) + fps = clip->Reader()->info.fps; + double fps_d = fps.ToDouble(); + double t = fps_d > 0 ? frame_number / fps_d : frame_number; + + const float k_track = tracking.GetValue(frame_number); + const float k_bleed = bleed.GetValue(frame_number); + const float k_soft = softness.GetValue(frame_number); + const float k_noise = noise.GetValue(frame_number); + 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)); + if (k_noise > 0.6f) + r_y = std::min(r_y, 1); + if (r_y > 0) { +#ifdef _OPENMP +#pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) + box_blur_row(&Y[y * w], &tmpY[y * w], w, r_y); + Y.swap(tmpY); + } + + float shift = lerp(0.0f, 2.5f, k_bleed); + int r_c = std::round(lerp(0.0f, 3.0f, k_bleed)); + float sat = 1.0f - 0.30f * k_bleed; + float shift_h = shift * 0.5f; +#ifdef _OPENMP +#pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) { + const float *srcU = &U[y * Uw]; + const float *srcV = &V[y * Uw]; + float *dstU = &tmpU[y * Uw]; + float *dstV = &tmpV[y * Uw]; + for (int x = 0; x < Uw; ++x) { + float xs = std::clamp(x - shift_h, 0.0f, float(Uw - 1)); + int x0 = int(xs); + int x1 = std::min(x0 + 1, Uw - 1); + float t = xs - x0; + dstU[x] = srcU[x0] * (1 - t) + srcU[x1] * t; + dstV[x] = srcV[x0] * (1 - t) + srcV[x1] * t; + } + } + U.swap(tmpU); + V.swap(tmpV); + + if (r_c > 0) { +#ifdef _OPENMP +#pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) + box_blur_row(&U[y * Uw], &tmpU[y * Uw], Uw, r_c); + U.swap(tmpU); +#ifdef _OPENMP +#pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) + box_blur_row(&V[y * Uw], &tmpV[y * Uw], Uw, r_c); + V.swap(tmpV); + } + + uint32_t SEED = fnv1a_32(Id()) ^ (uint32_t)seed_offset; + uint32_t schedSalt = (uint32_t)(k_bands * 64.0f) ^ + ((uint32_t)(k_stripe * 64.0f) << 8) ^ + ((uint32_t)(k_noise * 64.0f) << 16); + uint32_t SCHED_SEED = SEED ^ fnv1a_32(schedSalt, 0x9e3779b9u); + const float PI = 3.14159265358979323846f; + + float sigmaY = 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 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); + float Nburst = 1.0f + 2.0f * k_bands; + + struct Band { float center; double t0; }; + std::vector bands; + if (k_bands > 0.0f && rate > 0.0f) { + const double win_len = 0.25; + int win_idx = int(t / win_len); + double lambda = rate * win_len * + (0.25 + 1.5f * row_density(SCHED_SEED, frame_number, 0)); + double prob_ge1 = 1.0 - std::exp(-lambda); + double prob_ge2 = 1.0 - std::exp(-lambda) - lambda * std::exp(-lambda); + + auto spawn_band = [&](int kseed) { + float r1 = hash01(SCHED_SEED, uint32_t(win_idx), 11 + kseed, 0); + float start = r1 * win_len; + float center = + hash01(SCHED_SEED, uint32_t(win_idx), 12 + kseed, 0) * (h - Hburst) + + 0.5f * Hburst; + double t0 = win_idx * win_len + start; + double t1 = t0 + dur_frames / (fps_d > 0 ? fps_d : 1.0); + if (t >= t0 && t < t1) + bands.push_back({center, t0}); + }; + + float r = hash01(SCHED_SEED, uint32_t(win_idx), 9, 0); + if (r < prob_ge1) + spawn_band(0); + if (r < prob_ge2) + spawn_band(1); + } + + float ft = 2.0f; + int kf = int(std::floor(t * ft)); + float a = float(t * ft - kf); + +#ifdef _OPENMP +#pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) { + float bandF = 0.0f; + if (Hfixed > 0.0f && y >= h - Hfixed) + bandF = (y - (h - Hfixed)) / std::max(1.0f, Hfixed); + float burstF = 0.0f; + for (const auto &b : bands) { + float halfH = Hburst * 0.5f; + float dist = std::abs(y - b.center); + float profile = std::max(0.0f, 1.0f - dist / halfH); + float life = float((t - b.t0) * fps_d); + float env = (life < 1.0f) + ? life + : (life < dur_frames - 1 ? 1.0f + : std::max(0.0f, dur_frames - life)); + burstF = std::max(burstF, profile * env); + } + + float sat_row = 1.0f - (1.0f - sat_band) * burstF; + if (burstF > 0.0f && sat_row != 1.0f) { + float *urow = &U[y * Uw]; + float *vrow = &V[y * Uw]; + for (int xh = 0; xh < Uw; ++xh) { + urow[xh] *= sat_row; + vrow[xh] *= sat_row; + } + } + + float rowBias = row_density(SEED, frame_number, y); + float p = baseP * (0.25f + 1.5f * rowBias); + p *= (1.0f + 1.5f * bandF + 2.0f * burstF); + + float hum = 0.008f * k_noise * + std::sin(2 * PI * (y * (6.0f / h) + 0.08f * t)); + uint32_t s0 = SEED ^ 0x9e37u * kf ^ 0x85ebu * y; + uint32_t s1 = SEED ^ 0x9e37u * (kf + 1) ^ 0x85ebu * y ^ 0x1234567u; + auto step = [](uint32_t &s) { + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + return s; + }; + float lift = Gfixed * bandF + Gburst * burstF; + float rowSigma = sigmaY * (1 + (Nfixed - 1) * bandF + + (Nburst - 1) * burstF); + float k = 0.15f + 0.35f * hash01(SEED, uint32_t(frame_number), y, 777); + float sL = 0.0f, sR = 0.0f; + for (int x = 0; x < w; ++x) { + if (hash01(SEED, uint32_t(frame_number), y, x) < p) + sL = 1.0f; + if (hash01(SEED, uint32_t(frame_number), y, w - 1 - x) < p * 0.7f) + sR = 1.0f; + float n = ((step(s0) & 0xFFFFFF) / 16777215.0f) * (1 - a) + + ((step(s1) & 0xFFFFFF) / 16777215.0f) * a; + int idx = y * w + x; + float mt = std::clamp((Y[idx] - 0.2f) / (0.8f - 0.2f), 0.0f, 1.0f); + float val = Y[idx] + lift + rowSigma * (2 * n - 1) * + (0.6f + 0.4f * mt) + hum; + float streak = amp * (sL + sR); + float newY = val + streak * (k + (1.0f - val)); + Y[idx] = std::clamp(newY, 0.0f, 1.0f); + sL *= decay; + sR *= decay; + } + } + + float A = lerp(0.0f, 3.0f, k_track); // 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); // pixels + float phase = 2 * PI * (f * t) + 0.7f * (SEED * 0.001f); + for (int y = 0; y < h; ++y) { + float base = A * std::sin(2 * PI * 0.0035f * y + phase); + float skew = (y >= h - Hsk) + ? S * ((y - (h - Hsk)) / std::max(1.0f, Hsk)) + : 0.0f; + dx[y] = base + skew; + } + + auto remap_line = [&](const float *src, float *dst, int width, float scale) { +#ifdef _OPENMP +#pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) { + float off = dx[y] * scale; + const float *s = src + y * width; + float *d = dst + y * width; + int start = std::max(0, int(std::ceil(-off))); + int end = std::min(width, int(std::floor(width - off))); + float xs = start + off; + int x0 = int(xs); + float t = xs - x0; + for (int x = start; x < end; ++x) { + int x1 = x0 + 1; + d[x] = s[x0] * (1 - t) + s[x1] * t; + xs += 1.0f; + x0 = int(xs); + t = xs - x0; + } + for (int x = 0; x < start; ++x) + d[x] = s[0]; + for (int x = end; x < width; ++x) + d[x] = s[width - 1]; + } + }; + + remap_line(Y.data(), tmpY.data(), w, 1.0f); + Y.swap(tmpY); + remap_line(U.data(), tmpU.data(), Uw, 0.5f); + U.swap(tmpU); + remap_line(V.data(), tmpV.data(), Uw, 0.5f); + V.swap(tmpV); + +#ifdef _OPENMP +#pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) { + float *yrow = &Y[y * w]; + float *urow = &U[y * Uw]; + float *vrow = &V[y * Uw]; + uint32_t *row = base + y * stride; + for (int x = 0; x < w; ++x) { + float xs = x * 0.5f; + int x0 = int(xs); + int x1 = std::min(x0 + 1, Uw - 1); + float t = xs - x0; + float u = (urow[x0] * (1 - t) + urow[x1] * t) * sat; + float v = (vrow[x0] * (1 - t) + vrow[x1] * t) * sat; + float yv = yrow[x]; + float r = yv + 1.13983f * v; + float g = yv - 0.39465f * u - 0.58060f * v; + float b = yv + 2.03211f * u; + int R = int(std::clamp(r, 0.0f, 1.0f) * 255.0f); + int G = int(std::clamp(g, 0.0f, 1.0f) * 255.0f); + int B = int(std::clamp(b, 0.0f, 1.0f) * 255.0f); + uint32_t A = row[x] & 0xFF000000u; + row[x] = A | (R << 16) | (G << 8) | B; + } + } + + return frame; +} + +// JSON +std::string AnalogTape::Json() const { return JsonValue().toStyledString(); } + +Json::Value AnalogTape::JsonValue() const { + Json::Value root = EffectBase::JsonValue(); + root["type"] = info.class_name; + root["tracking"] = tracking.JsonValue(); + root["bleed"] = bleed.JsonValue(); + root["softness"] = softness.JsonValue(); + root["noise"] = noise.JsonValue(); + root["stripe"] = stripe.JsonValue(); + root["static_bands"] = staticBands.JsonValue(); + root["seed_offset"] = seed_offset; + return root; +} + +void AnalogTape::SetJson(const std::string value) { + try { + Json::Value root = openshot::stringToJson(value); + SetJsonValue(root); + } catch (const std::exception &) { + throw InvalidJSON("JSON is invalid (missing keys or invalid data types)"); + } +} + +void AnalogTape::SetJsonValue(const Json::Value root) { + EffectBase::SetJsonValue(root); + if (!root["tracking"].isNull()) + tracking.SetJsonValue(root["tracking"]); + if (!root["bleed"].isNull()) + bleed.SetJsonValue(root["bleed"]); + if (!root["softness"].isNull()) + softness.SetJsonValue(root["softness"]); + if (!root["noise"].isNull()) + noise.SetJsonValue(root["noise"]); + if (!root["stripe"].isNull()) + stripe.SetJsonValue(root["stripe"]); + if (!root["static_bands"].isNull()) + staticBands.SetJsonValue(root["static_bands"]); + if (!root["seed_offset"].isNull()) + seed_offset = root["seed_offset"].asInt(); +} + +std::string AnalogTape::PropertiesJSON(int64_t requested_frame) const { + Json::Value root = BasePropertiesJSON(requested_frame); + root["tracking"] = + add_property_json("Tracking", tracking.GetValue(requested_frame), "float", + "", &tracking, 0, 1, false, requested_frame); + root["bleed"] = + add_property_json("Bleed", bleed.GetValue(requested_frame), "float", "", + &bleed, 0, 1, false, requested_frame); + root["softness"] = + add_property_json("Softness", softness.GetValue(requested_frame), "float", + "", &softness, 0, 1, false, requested_frame); + root["noise"] = + add_property_json("Noise", noise.GetValue(requested_frame), "float", "", + &noise, 0, 1, false, requested_frame); + root["stripe"] = + add_property_json("Stripe", stripe.GetValue(requested_frame), "float", + "Bottom tracking stripe brightness and noise.", + &stripe, 0, 1, false, requested_frame); + root["static_bands"] = + add_property_json("Static Bands", staticBands.GetValue(requested_frame), + "float", + "Short bright static bands and extra dropouts.", + &staticBands, 0, 1, false, requested_frame); + root["seed_offset"] = + add_property_json("Seed Offset", seed_offset, "int", "", NULL, 0, 1000, + false, requested_frame); + return root.toStyledString(); +} diff --git a/src/effects/AnalogTape.h b/src/effects/AnalogTape.h new file mode 100644 index 000000000..d10ddd5a4 --- /dev/null +++ b/src/effects/AnalogTape.h @@ -0,0 +1,130 @@ +/** + * @file + * @brief Header file for AnalogTape effect class + * + * Vintage home video wobble, bleed, and grain. + * + * @author Jonathan Thomas + */ + +#ifndef OPENSHOT_ANALOGTAPE_EFFECT_H +#define OPENSHOT_ANALOGTAPE_EFFECT_H + +#include "../EffectBase.h" +#include "../Frame.h" +#include "../Json.h" +#include "../KeyFrame.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__GNUC__) || defined(__clang__) +#define OS_RESTRICT __restrict__ +#else +#define OS_RESTRICT +#endif + +namespace openshot { + +/// Analog video tape simulation effect. +class AnalogTape : public EffectBase { +private: + void init_effect_details(); + static inline uint32_t fnv1a_32(const std::string &s) { + uint32_t h = 2166136261u; + for (unsigned char c : s) { + h ^= c; + h *= 16777619u; + } + return h; + } + static inline uint32_t fnv1a_32(uint32_t h, uint32_t d) { + unsigned char bytes[4]; + bytes[0] = d & 0xFF; + bytes[1] = (d >> 8) & 0xFF; + bytes[2] = (d >> 16) & 0xFF; + bytes[3] = (d >> 24) & 0xFF; + for (int i = 0; i < 4; ++i) { + h ^= bytes[i]; + h *= 16777619u; + } + return h; + } + static inline float hash01(uint32_t seed, uint32_t a, uint32_t b, uint32_t c) { + uint32_t h = fnv1a_32(seed, a); + h = fnv1a_32(h, b); + h = fnv1a_32(h, c); + return h / 4294967295.0f; + } + static inline float row_density(uint32_t seed, int frame, int y) { + int tc = (frame >> 3); + int y0 = (y >> 3); + float a = (y & 7) / 8.0f; + float h0 = hash01(seed, tc, y0, 31); + float h1 = hash01(seed, tc, y0 + 1, 31); + float m = (1 - a) * h0 + a * h1; + return m * m; + } + static inline void box_blur_row(const float *OS_RESTRICT src, + float *OS_RESTRICT dst, int w, int r) { + if (r == 0) { + std::memcpy(dst, src, w * sizeof(float)); + return; + } + const int win = 2 * r + 1; + float sum = 0.0f; + for (int k = -r; k <= r; ++k) + sum += src[std::clamp(k, 0, w - 1)]; + dst[0] = sum / win; + for (int x = 1; x < w; ++x) { + int add = std::min(w - 1, x + r); + int sub = std::max(0, x - r - 1); + sum += src[add] - src[sub]; + dst[x] = sum / win; + } + } + + int last_w = 0, last_h = 0; + std::vector Y, U, V, tmpY, tmpU, tmpV, dx; + +public: + Keyframe tracking; ///< tracking wobble amount + Keyframe bleed; ///< color bleed amount + Keyframe softness; ///< luma blur radius + Keyframe noise; ///< grain/dropouts amount + Keyframe stripe; ///< bottom tracking stripe strength + Keyframe staticBands; ///< burst static band strength + int seed_offset; ///< seed offset for deterministic randomness + + AnalogTape(); + AnalogTape(Keyframe tracking, Keyframe bleed, Keyframe softness, + Keyframe noise, Keyframe stripe, Keyframe staticBands, + int seed_offset = 0); + + std::shared_ptr + GetFrame(std::shared_ptr frame, + int64_t frame_number) override; + + std::shared_ptr GetFrame(int64_t frame_number) override { + return GetFrame(std::make_shared(), frame_number); + } + + // JSON + 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 + +#undef OS_RESTRICT + +#endif diff --git a/tests/AnalogTape.cpp b/tests/AnalogTape.cpp new file mode 100644 index 000000000..0a7ee45d5 --- /dev/null +++ b/tests/AnalogTape.cpp @@ -0,0 +1,85 @@ +/** + * @file + * @brief Unit tests for AnalogTape effect + * @author Jonathan Thomas + */ + +#include +#include +#include + +#include "Frame.h" +#include "effects/AnalogTape.h" +#include "openshot_catch.h" + +using namespace openshot; + +static std::shared_ptr makeGrayFrame() { + QImage img(5, 5, QImage::Format_ARGB32); + img.fill(QColor(100, 100, 100, 255)); + auto f = std::make_shared(); + *f->GetImage() = img; + return f; +} + +static std::shared_ptr makeGrayFrame(int w, int h) { + QImage img(w, h, QImage::Format_ARGB32); + img.fill(QColor(100, 100, 100, 255)); + auto f = std::make_shared(); + *f->GetImage() = img; + return f; +} + +TEST_CASE("AnalogTape modifies frame", "[effect][analogtape]") { + AnalogTape eff; + auto frame = makeGrayFrame(); + QColor before = frame->GetImage()->pixelColor(2, 2); + auto out = eff.GetFrame(frame, 1); + QColor after = out->GetImage()->pixelColor(2, 2); + CHECK(after != before); +} + +TEST_CASE("AnalogTape deterministic per id", "[effect][analogtape]") { + AnalogTape e1; + e1.Id("same"); + AnalogTape e2; + e2.Id("same"); + auto f1 = makeGrayFrame(); + auto f2 = makeGrayFrame(); + auto o1 = e1.GetFrame(f1, 1); + auto o2 = e2.GetFrame(f2, 1); + QColor c1 = o1->GetImage()->pixelColor(1, 1); + QColor c2 = o2->GetImage()->pixelColor(1, 1); + CHECK(c1 == c2); +} + +TEST_CASE("AnalogTape seed offset alters output", "[effect][analogtape]") { + AnalogTape e1; + e1.Id("seed"); + e1.seed_offset = 0; + AnalogTape e2; + e2.Id("seed"); + e2.seed_offset = 5; + auto f1 = makeGrayFrame(); + auto f2 = makeGrayFrame(); + auto o1 = e1.GetFrame(f1, 1); + auto o2 = e2.GetFrame(f2, 1); + QColor c1 = o1->GetImage()->pixelColor(1, 1); + QColor c2 = o2->GetImage()->pixelColor(1, 1); + CHECK(c1 != c2); +} + +TEST_CASE("AnalogTape stripe lifts bottom", "[effect][analogtape]") { + AnalogTape e; + e.tracking = Keyframe(0.0); + e.bleed = Keyframe(0.0); + e.softness = Keyframe(0.0); + e.noise = Keyframe(0.0); + e.stripe = Keyframe(1.0); + e.staticBands = Keyframe(0.0); + auto frame = makeGrayFrame(20, 20); + auto out = e.GetFrame(frame, 1); + QColor top = out->GetImage()->pixelColor(10, 0); + QColor bottom = out->GetImage()->pixelColor(10, 19); + CHECK(bottom.red() > top.red()); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c24b1f617..475ac4eb0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,6 +48,7 @@ set(OPENSHOT_TESTS ChromaKey Crop LensFlare + AnalogTape Sharpen SphericalEffect ) From 9ca7e07b12e77fb4e3a2a5d4abd00277ac9a1afd Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 11 Sep 2025 20:31:24 -0500 Subject: [PATCH 22/41] Adding more SphericalProjection unit tests - still a WIP --- tests/SphericalEffect.cpp | 311 +++++++++++++++++++++++++------------- 1 file changed, 205 insertions(+), 106 deletions(-) diff --git a/tests/SphericalEffect.cpp b/tests/SphericalEffect.cpp index 9a6db407b..c678d48d5 100644 --- a/tests/SphericalEffect.cpp +++ b/tests/SphericalEffect.cpp @@ -1,32 +1,33 @@ /** * @file - * @brief Unit tests for openshot::SphericalProjection effect using PNG fixtures - * @author Jonathan Thomas + * @brief Unit tests for openshot::SphericalProjection using PNG fixtures + * @author Jonathan Thomas * * @ref License + * + * Copyright (c) 2008-2025 OpenShot Studios, LLC + * SPDX-License-Identifier: LGPL-3.0-or-later */ -// Copyright (c) 2008-2025 OpenShot Studios, LLC -// -// SPDX-License-Identifier: LGPL-3.0-or-later - #include "Frame.h" #include "effects/SphericalProjection.h" #include "openshot_catch.h" + #include #include #include +#include +#include using namespace openshot; -// allow Catch2 to print QColor on failure +// Pretty-print QColor on failure static std::ostream &operator<<(std::ostream &os, QColor const &c) { - os << "QColor(" << c.red() << "," << c.green() << "," << c.blue() << "," - << c.alpha() << ")"; + os << "QColor(" << c.red() << "," << c.green() << "," << c.blue() << "," << c.alpha() << ")"; return os; } -// load a PNG into a Frame +// Load a PNG fixture into a fresh Frame static std::shared_ptr loadFrame(const char *filename) { QImage img(QString(TEST_MEDIA_PATH) + filename); img = img.convertToFormat(QImage::Format_ARGB32); @@ -35,7 +36,7 @@ static std::shared_ptr loadFrame(const char *filename) { return f; } -// apply effect and sample center pixel +// Helpers to sample pixels static QColor centerPixel(SphericalProjection &e, std::shared_ptr f) { auto img = e.GetFrame(f, 1)->GetImage(); int cx = img->width() / 2; @@ -43,117 +44,215 @@ static QColor centerPixel(SphericalProjection &e, std::shared_ptr f) { return img->pixelColor(cx, cy); } -TEST_CASE("sphere mode default and invert", "[effect][spherical]") { +static QColor offsetPixel(std::shared_ptr img, int dx, int dy) { + const int cx = img->width() / 2 + dx; + const int cy = img->height() / 2 + dy; + return img->pixelColor(std::clamp(cx, 0, img->width() - 1), + std::clamp(cy, 0, img->height() - 1)); +} + +// Loose classifiers for our colored guide lines +static bool is_red(QColor c) { return c.red() >= 200 && c.green() <= 60 && c.blue() <= 60; } +static bool is_yellow(QColor c) { return c.red() >= 200 && c.green() >= 170 && c.blue() <= 60; } + +/* ---------------------------------------------------------------------------- + * Invert behavior vs Yaw+180 (Equirect input) + * ---------------------------------------------------------------------------- + * In both RECT_SPHERE and RECT_HEMISPHERE, Invert should match adding 180° of + * yaw (no mirroring). Compare the center pixel using *fresh* inputs. + */ + +TEST_CASE("sphere mode: invert equals yaw+180 (center pixel)", "[effect][spherical]") { + // A: invert=BACK, yaw=0 + SphericalProjection eA; + eA.input_model = SphericalProjection::INPUT_EQUIRECT; + eA.projection_mode = SphericalProjection::MODE_RECT_SPHERE; + eA.in_fov = Keyframe(180.0); + eA.fov = Keyframe(90.0); + eA.interpolation = SphericalProjection::INTERP_NEAREST; + eA.invert = SphericalProjection::INVERT_BACK; + eA.yaw = Keyframe(0.0); + + // B: invert=NORMAL, yaw=180 + SphericalProjection eB = eA; + eB.invert = SphericalProjection::INVERT_NORMAL; + eB.yaw = Keyframe(180.0); + + auto fA = loadFrame("eq_sphere.png"); + auto fB = loadFrame("eq_sphere.png"); + + CHECK(centerPixel(eA, fA) == centerPixel(eB, fB)); +} + +TEST_CASE("hemisphere mode: invert equals yaw+180 (center pixel)", "[effect][spherical]") { + // A: invert=BACK, yaw=0 + SphericalProjection eA; + eA.input_model = SphericalProjection::INPUT_EQUIRECT; + eA.projection_mode = SphericalProjection::MODE_RECT_HEMISPHERE; + eA.in_fov = Keyframe(180.0); + eA.fov = Keyframe(90.0); + eA.interpolation = SphericalProjection::INTERP_NEAREST; + eA.invert = SphericalProjection::INVERT_BACK; + eA.yaw = Keyframe(0.0); + + // B: invert=NORMAL, yaw=180 + SphericalProjection eB = eA; + eB.invert = SphericalProjection::INVERT_NORMAL; + eB.yaw = Keyframe(180.0); + + auto fA = loadFrame("eq_sphere.png"); + auto fB = loadFrame("eq_sphere.png"); + + CHECK(centerPixel(eA, fA) == centerPixel(eB, fB)); +} + +/* ---------------------------------------------------------------------------- + * Fisheye input: center pixel should be invariant to yaw/invert + * ---------------------------------------------------------------------------- + */ + +TEST_CASE("fisheye input: center pixel invariant under invert and yaw", "[effect][spherical]") { + SphericalProjection base; + base.input_model = SphericalProjection::INPUT_FEQ_EQUIDISTANT; + base.projection_mode = SphericalProjection::MODE_RECT_SPHERE; + base.in_fov = Keyframe(180.0); + base.fov = Keyframe(180.0); + base.interpolation = SphericalProjection::INTERP_NEAREST; + + // Baseline + SphericalProjection e0 = base; + e0.invert = SphericalProjection::INVERT_NORMAL; + e0.yaw = Keyframe(0.0); + QColor c0 = centerPixel(e0, loadFrame("fisheye.png")); + + // Invert + SphericalProjection e1 = base; + e1.invert = SphericalProjection::INVERT_BACK; + e1.yaw = Keyframe(0.0); + QColor c1 = centerPixel(e1, loadFrame("fisheye.png")); + + // Yaw +45 + SphericalProjection e2 = base; + e2.invert = SphericalProjection::INVERT_NORMAL; + e2.yaw = Keyframe(45.0); + QColor c2 = centerPixel(e2, loadFrame("fisheye.png")); + + CHECK(c0 == c1); + CHECK(c0 == c2); +} + +/* ---------------------------------------------------------------------------- + * Cache invalidation sanity check + * ---------------------------------------------------------------------------- + */ + +TEST_CASE("changing properties invalidates cache", "[effect][spherical]") { SphericalProjection e; - e.projection_mode = 0; + e.input_model = SphericalProjection::INPUT_EQUIRECT; + e.projection_mode = SphericalProjection::MODE_RECT_SPHERE; e.yaw = Keyframe(45.0); + e.invert = SphericalProjection::INVERT_NORMAL; + e.interpolation = SphericalProjection::INTERP_NEAREST; - { - 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)); - } + QColor c0 = centerPixel(e, loadFrame("eq_sphere.png")); + e.invert = SphericalProjection::INVERT_BACK; // should rebuild UV map + QColor c1 = centerPixel(e, loadFrame("eq_sphere.png")); + + CHECK(c1 != c0); } -TEST_CASE("hemisphere mode default and invert", "[effect][spherical]") { +/* ---------------------------------------------------------------------------- + * Checker-plane fixtures (rectilinear output) + * ---------------------------------------------------------------------------- + * Validate the colored guide lines (red vertical meridian at center, yellow + * equator horizontally). We use tolerant classifiers to avoid brittle + * single-pixel mismatches. + */ + +TEST_CASE("input models: checker-plane colored guides are consistent", "[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)); + e.projection_mode = SphericalProjection::MODE_RECT_SPHERE; + e.fov = Keyframe(90.0); + e.in_fov = Keyframe(180.0); + e.yaw = Keyframe(0.0); + e.pitch = Keyframe(0.0); + e.roll = Keyframe(0.0); + e.interpolation = SphericalProjection::INTERP_NEAREST; + + auto check_guides = [&](int input_model, const char *file) { + e.input_model = input_model; + auto out = e.GetFrame(loadFrame(file), 1)->GetImage(); + + // Center column should hit the red meridian + REQUIRE(is_red(offsetPixel(out, 0, 0))); + + // A bit left/right along the equator should be yellow + CHECK(is_yellow(offsetPixel(out, -60, 0))); + CHECK(is_yellow(offsetPixel(out, 60, 0))); + }; + + SECTION("equirect input") { + check_guides(SphericalProjection::INPUT_EQUIRECT, "eq_sphere_plane.png"); } - { - 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)); + SECTION("fisheye equidistant input") { + check_guides(SphericalProjection::INPUT_FEQ_EQUIDISTANT, "fisheye_plane_equidistant.png"); } - { - 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)); + SECTION("fisheye equisolid input") { + check_guides(SphericalProjection::INPUT_FEQ_EQUISOLID, "fisheye_plane_equisolid.png"); } -} - -TEST_CASE("fisheye mode default and invert", "[effect][spherical]") { - SphericalProjection e; - e.projection_mode = 2; - e.input_model = 1; - e.in_fov = Keyframe(180.0); - 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)); + SECTION("fisheye stereographic input") { + check_guides(SphericalProjection::INPUT_FEQ_STEREOGRAPHIC, "fisheye_plane_stereographic.png"); } - { - 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)); + SECTION("fisheye orthographic input") { + check_guides(SphericalProjection::INPUT_FEQ_ORTHOGRAPHIC, "fisheye_plane_orthographic.png"); } } -TEST_CASE("fisheye mode yaw has no effect at center", "[effect][spherical]") { - SphericalProjection e; - e.projection_mode = 2; - e.input_model = 1; - e.interpolation = 0; - e.in_fov = Keyframe(180.0); - e.fov = Keyframe(180.0); - e.invert = 0; +/* ---------------------------------------------------------------------------- + * Fisheye output modes from equirect plane + * ---------------------------------------------------------------------------- + * - Center pixel should match the rect view's center (same yaw). + * - Corners are outside the fisheye disk and should be fully transparent. + */ - auto f = loadFrame("fisheye.png"); - e.yaw = Keyframe(45.0); - CHECK(centerPixel(e, f) == QColor(255, 255, 255, 255)); -} +TEST_CASE("output fisheye modes: center matches rect view, corners outside disk", "[effect][spherical]") { + // Expected center color using rectilinear view + SphericalProjection rect; + rect.input_model = SphericalProjection::INPUT_EQUIRECT; + rect.projection_mode = SphericalProjection::MODE_RECT_SPHERE; + rect.in_fov = Keyframe(180.0); + rect.fov = Keyframe(90.0); + rect.interpolation = SphericalProjection::INTERP_NEAREST; + QColor expected_center = centerPixel(rect, loadFrame("eq_sphere_plane.png")); -TEST_CASE("changing properties invalidates cache", "[effect][spherical]") { - SphericalProjection e; - e.projection_mode = 0; - e.yaw = Keyframe(45.0); - e.invert = 0; - e.interpolation = 0; + auto verify_mode = [&](int mode) { + SphericalProjection e; + e.input_model = SphericalProjection::INPUT_EQUIRECT; + e.projection_mode = mode; // one of the fisheye outputs + e.in_fov = Keyframe(180.0); + e.fov = Keyframe(180.0); + e.interpolation = SphericalProjection::INTERP_NEAREST; - auto f0 = loadFrame("eq_sphere.png"); - QColor c0 = centerPixel(e, f0); + auto img = e.GetFrame(loadFrame("eq_sphere_plane.png"), 1)->GetImage(); - auto f1 = loadFrame("eq_sphere.png"); - e.invert = 1; // should rebuild UV map - QColor c1 = centerPixel(e, f1); + // Center matches rect view + CHECK(is_red(expected_center) == is_red(offsetPixel(img, 0, 0))); - CHECK(c1 != c0); + // Corners are fully outside disk => transparent black + QColor transparent(0,0,0,0); + QColor tl = offsetPixel(img, -img->width()/2 + 2, -img->height()/2 + 2); + QColor tr = offsetPixel(img, img->width()/2 - 2, -img->height()/2 + 2); + QColor bl = offsetPixel(img, -img->width()/2 + 2, img->height()/2 - 2); + QColor br = offsetPixel(img, img->width()/2 - 2, img->height()/2 - 2); + + CHECK(tl == transparent); + CHECK(tr == transparent); + CHECK(bl == transparent); + CHECK(br == transparent); + }; + + verify_mode(SphericalProjection::MODE_FISHEYE_EQUIDISTANT); + verify_mode(SphericalProjection::MODE_FISHEYE_EQUISOLID); + verify_mode(SphericalProjection::MODE_FISHEYE_STEREOGRAPHIC); + verify_mode(SphericalProjection::MODE_FISHEYE_ORTHOGRAPHIC); } From 1533b6ab1fcb15d9608f192cb6dcc6c471445e97 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 11 Sep 2025 22:08:38 -0500 Subject: [PATCH 23/41] Fixing SphericalEffect.cpp tests --- tests/SphericalEffect.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/SphericalEffect.cpp b/tests/SphericalEffect.cpp index c678d48d5..b528f0bcc 100644 --- a/tests/SphericalEffect.cpp +++ b/tests/SphericalEffect.cpp @@ -111,7 +111,7 @@ TEST_CASE("hemisphere mode: invert equals yaw+180 (center pixel)", "[effect][sph * ---------------------------------------------------------------------------- */ -TEST_CASE("fisheye input: center pixel invariant under invert and yaw", "[effect][spherical]") { +TEST_CASE("fisheye input: center pixel invariant under invert", "[effect][spherical]") { SphericalProjection base; base.input_model = SphericalProjection::INPUT_FEQ_EQUIDISTANT; base.projection_mode = SphericalProjection::MODE_RECT_SPHERE; @@ -131,14 +131,14 @@ TEST_CASE("fisheye input: center pixel invariant under invert and yaw", "[effect e1.yaw = Keyframe(0.0); QColor c1 = centerPixel(e1, loadFrame("fisheye.png")); - // Yaw +45 + // Yaw +45 should point elsewhere SphericalProjection e2 = base; e2.invert = SphericalProjection::INVERT_NORMAL; e2.yaw = Keyframe(45.0); QColor c2 = centerPixel(e2, loadFrame("fisheye.png")); CHECK(c0 == c1); - CHECK(c0 == c2); + CHECK(c0 != c2); } /* ---------------------------------------------------------------------------- @@ -183,8 +183,12 @@ TEST_CASE("input models: checker-plane colored guides are consistent", "[effect] e.input_model = input_model; auto out = e.GetFrame(loadFrame(file), 1)->GetImage(); - // Center column should hit the red meridian - REQUIRE(is_red(offsetPixel(out, 0, 0))); + // Center column should hit the red meridian (allow 1px tolerance) + // Sample above the equator to avoid overlap with the yellow line + bool center_red = false; + for (int dx = -5; dx <= 5 && !center_red; ++dx) + center_red = center_red || is_red(offsetPixel(out, dx, -60)); + REQUIRE(center_red); // A bit left/right along the equator should be yellow CHECK(is_yellow(offsetPixel(out, -60, 0))); From 0570ad084bad2e8760de49d9554fc4bfb3f6f796 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 11 Sep 2025 23:27:41 -0500 Subject: [PATCH 24/41] Large timeline clean-up, speed-up, and fix concurrency bugs: - make Add/Remove Effect methods thread safe - Fix RemoveClip memory leak - Improve performance of sorting clips by position and layer, cache some common accessors, and speed up "clip intersection" logic - Don't resize audio container in loop - do it once - Large refactor of looping through clips and finding top clip - Protect ClearAllCache from empty Readers, prevent crash - Expanded unit tests to include RemoveEffect, and test many of the changes in the commit. --- src/Timeline.cpp | 183 +++++++++++++++++++++++---------------- src/Timeline.h | 18 ++-- tests/Timeline.cpp | 211 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 303 insertions(+), 109 deletions(-) diff --git a/src/Timeline.cpp b/src/Timeline.cpp index 9b940d4a1..272ae0fd8 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -21,6 +21,9 @@ #include #include +#include +#include +#include using namespace openshot; @@ -357,6 +360,9 @@ void Timeline::AddClip(Clip* clip) // Add an effect to the timeline void Timeline::AddEffect(EffectBase* effect) { + // Get lock (prevent getting frames while this happens) + const std::lock_guard guard(getFrameMutex); + // Assign timeline to effect effect->ParentTimeline(this); @@ -370,14 +376,16 @@ void Timeline::AddEffect(EffectBase* effect) // Remove an effect from the timeline void Timeline::RemoveEffect(EffectBase* effect) { + // Get lock (prevent getting frames while this happens) + const std::lock_guard guard(getFrameMutex); + effects.remove(effect); // Delete effect object (if timeline allocated it) - bool allocated = allocated_effects.count(effect); - if (allocated) { + if (allocated_effects.count(effect)) { + allocated_effects.erase(effect); // erase before nulling the pointer delete effect; effect = NULL; - allocated_effects.erase(effect); } // Sort effects @@ -393,11 +401,10 @@ void Timeline::RemoveClip(Clip* clip) clips.remove(clip); // Delete clip object (if timeline allocated it) - bool allocated = allocated_clips.count(clip); - if (allocated) { + if (allocated_clips.count(clip)) { + allocated_clips.erase(clip); // erase before nulling the pointer delete clip; clip = NULL; - allocated_clips.erase(clip); } // Sort clips @@ -551,8 +558,9 @@ std::shared_ptr Timeline::apply_effects(std::shared_ptr frame, int for (auto effect : effects) { // Does clip intersect the current requested time - long effect_start_position = round(effect->Position() * info.fps.ToDouble()) + 1; - long effect_end_position = round((effect->Position() + (effect->Duration())) * info.fps.ToDouble()); + const double fpsD = info.fps.ToDouble(); + int64_t effect_start_position = static_cast(std::llround(effect->Position() * fpsD)) + 1; + int64_t effect_end_position = static_cast(std::llround((effect->Position() + effect->Duration()) * fpsD)); bool does_effect_intersect = (effect_start_position <= timeline_frame_number && effect_end_position >= timeline_frame_number && effect->Layer() == layer); @@ -560,8 +568,8 @@ std::shared_ptr Timeline::apply_effects(std::shared_ptr frame, int if (does_effect_intersect) { // Determine the frame needed for this clip (based on the position on the timeline) - long effect_start_frame = (effect->Start() * info.fps.ToDouble()) + 1; - long effect_frame_number = timeline_frame_number - effect_start_position + effect_start_frame; + int64_t effect_start_frame = static_cast(std::llround(effect->Start() * fpsD)) + 1; + int64_t effect_frame_number = timeline_frame_number - effect_start_position + effect_start_frame; if (!options->is_top_clip) continue; // skip effect, if overlapped/covered by another clip on same layer @@ -626,14 +634,13 @@ std::shared_ptr Timeline::GetOrCreateFrame(std::shared_ptr backgro void Timeline::add_layer(std::shared_ptr new_frame, Clip* source_clip, int64_t clip_frame_number, bool is_top_clip, float max_volume) { // Create timeline options (with details about this current frame request) - TimelineInfoStruct* options = new TimelineInfoStruct(); - options->is_top_clip = is_top_clip; - options->is_before_clip_keyframes = true; + TimelineInfoStruct options{}; + options.is_top_clip = is_top_clip; + options.is_before_clip_keyframes = true; // Get the clip's frame, composited on top of the current timeline frame std::shared_ptr source_frame; - source_frame = GetOrCreateFrame(new_frame, source_clip, clip_frame_number, options); - delete options; + source_frame = GetOrCreateFrame(new_frame, source_clip, clip_frame_number, &options); // No frame found... so bail if (!source_frame) @@ -656,6 +663,12 @@ void Timeline::add_layer(std::shared_ptr new_frame, Clip* source_clip, in "clip_frame_number", clip_frame_number); if (source_frame->GetAudioChannelsCount() == info.channels && source_clip->has_audio.GetInt(clip_frame_number) != 0) + { + // Ensure timeline frame matches the source samples once per frame + if (new_frame->GetAudioSamplesCount() != source_frame->GetAudioSamplesCount()){ + new_frame->ResizeAudio(info.channels, source_frame->GetAudioSamplesCount(), info.sample_rate, info.channel_layout); + } + for (int channel = 0; channel < source_frame->GetAudioChannelsCount(); channel++) { // Get volume from previous frame and this frame @@ -692,18 +705,11 @@ void Timeline::add_layer(std::shared_ptr new_frame, Clip* source_clip, in if (!isEqual(previous_volume, 1.0) || !isEqual(volume, 1.0)) source_frame->ApplyGainRamp(channel_mapping, 0, source_frame->GetAudioSamplesCount(), previous_volume, volume); - // TODO: Improve FrameMapper (or Timeline) to always get the correct number of samples per frame. - // Currently, the ResampleContext sometimes leaves behind a few samples for the next call, and the - // number of samples returned is variable... and does not match the number expected. - // This is a crude solution at best. =) - if (new_frame->GetAudioSamplesCount() != source_frame->GetAudioSamplesCount()){ - // Force timeline frame to match the source frame - new_frame->ResizeAudio(info.channels, source_frame->GetAudioSamplesCount(), info.sample_rate, info.channel_layout); - } // Copy audio samples (and set initial volume). Mix samples with existing audio samples. The gains are added together, to // be sure to set the gain's correctly, so the sum does not exceed 1.0 (of audio distortion will happen). new_frame->AddAudio(false, channel_mapping, 0, source_frame->GetAudioSamples(channel), source_frame->GetAudioSamplesCount(), 1.0); } + } else // Debug output ZmqLogger::Instance()->AppendDebugMethod( @@ -1005,67 +1011,90 @@ std::shared_ptr Timeline::GetFrame(int64_t requested_frame) "clips.size()", clips.size(), "nearby_clips.size()", nearby_clips.size()); - // Find Clips near this time + // Precompute per-clip timing for this requested frame + struct ClipInfo { + Clip* clip; + int64_t start_pos; + int64_t end_pos; + int64_t start_frame; + int64_t frame_number; + bool intersects; + }; + std::vector clip_infos; + clip_infos.reserve(nearby_clips.size()); + const double fpsD = info.fps.ToDouble(); + for (auto clip : nearby_clips) { - long clip_start_position = round(clip->Position() * info.fps.ToDouble()) + 1; - long clip_end_position = round((clip->Position() + clip->Duration()) * info.fps.ToDouble()); - bool does_clip_intersect = (clip_start_position <= requested_frame && clip_end_position >= requested_frame); + int64_t start_pos = static_cast(std::llround(clip->Position() * fpsD)) + 1; + int64_t end_pos = static_cast(std::llround((clip->Position() + clip->Duration()) * fpsD)); + bool intersects = (start_pos <= requested_frame && end_pos >= requested_frame); + int64_t start_frame = static_cast(std::llround(clip->Start() * fpsD)) + 1; + int64_t frame_number = requested_frame - start_pos + start_frame; + clip_infos.push_back({clip, start_pos, end_pos, start_frame, frame_number, intersects}); + } + + // Determine top clip per layer (linear, no nested loop) + std::unordered_map top_start_for_layer; + std::unordered_map top_clip_for_layer; + for (const auto& ci : clip_infos) { + if (!ci.intersects) continue; + const int layer = ci.clip->Layer(); + auto it = top_start_for_layer.find(layer); + if (it == top_start_for_layer.end() || ci.start_pos > it->second) { + top_start_for_layer[layer] = ci.start_pos; // strictly greater to match prior logic + top_clip_for_layer[layer] = ci.clip; + } + } + // Compute max_volume across all overlapping clips once + float max_volume_sum = 0.0f; + for (const auto& ci : clip_infos) { + if (!ci.intersects) continue; + if (ci.clip->Reader() && ci.clip->Reader()->info.has_audio && + ci.clip->has_audio.GetInt(ci.frame_number) != 0) { + max_volume_sum += static_cast(ci.clip->volume.GetValue(ci.frame_number)); + } + } + + // Compose intersecting clips in a single pass + for (const auto& ci : clip_infos) { // Debug output ZmqLogger::Instance()->AppendDebugMethod( "Timeline::GetFrame (Does clip intersect)", "requested_frame", requested_frame, - "clip->Position()", clip->Position(), - "clip->Duration()", clip->Duration(), - "does_clip_intersect", does_clip_intersect); + "clip->Position()", ci.clip->Position(), + "clip->Duration()", ci.clip->Duration(), + "does_clip_intersect", ci.intersects); // Clip is visible - if (does_clip_intersect) { - // Determine if clip is "top" clip on this layer (only happens when multiple clips are overlapping) - bool is_top_clip = true; - float max_volume = 0.0; - for (auto nearby_clip : nearby_clips) { - long nearby_clip_start_position = round(nearby_clip->Position() * info.fps.ToDouble()) + 1; - long nearby_clip_end_position = round((nearby_clip->Position() + nearby_clip->Duration()) * info.fps.ToDouble()) + 1; - long nearby_clip_start_frame = (nearby_clip->Start() * info.fps.ToDouble()) + 1; - long nearby_clip_frame_number = requested_frame - nearby_clip_start_position + nearby_clip_start_frame; - - // Determine if top clip - if (clip->Id() != nearby_clip->Id() && clip->Layer() == nearby_clip->Layer() && - nearby_clip_start_position <= requested_frame && nearby_clip_end_position >= requested_frame && - nearby_clip_start_position > clip_start_position && is_top_clip == true) { - is_top_clip = false; - } - - // Determine max volume of overlapping clips - if (nearby_clip->Reader() && nearby_clip->Reader()->info.has_audio && - nearby_clip->has_audio.GetInt(nearby_clip_frame_number) != 0 && - nearby_clip_start_position <= requested_frame && nearby_clip_end_position >= requested_frame) { - max_volume += nearby_clip->volume.GetValue(nearby_clip_frame_number); - } - } + if (ci.intersects) { + // Is this the top clip on its layer? + bool is_top_clip = false; + const int layer = ci.clip->Layer(); + auto top_it = top_clip_for_layer.find(layer); + if (top_it != top_clip_for_layer.end()) + is_top_clip = (top_it->second == ci.clip); // Determine the frame needed for this clip (based on the position on the timeline) - long clip_start_frame = (clip->Start() * info.fps.ToDouble()) + 1; - long clip_frame_number = requested_frame - clip_start_position + clip_start_frame; + int64_t clip_frame_number = ci.frame_number; // Debug output ZmqLogger::Instance()->AppendDebugMethod( "Timeline::GetFrame (Calculate clip's frame #)", - "clip->Position()", clip->Position(), - "clip->Start()", clip->Start(), + "clip->Position()", ci.clip->Position(), + "clip->Start()", ci.clip->Start(), "info.fps.ToFloat()", info.fps.ToFloat(), "clip_frame_number", clip_frame_number); // Add clip's frame as layer - add_layer(new_frame, clip, clip_frame_number, is_top_clip, max_volume); + add_layer(new_frame, ci.clip, clip_frame_number, is_top_clip, max_volume_sum); } else { // Debug output ZmqLogger::Instance()->AppendDebugMethod( "Timeline::GetFrame (clip does not intersect)", "requested_frame", requested_frame, - "does_clip_intersect", does_clip_intersect); + "does_clip_intersect", ci.intersects); } } // end clip loop @@ -1097,15 +1126,17 @@ std::vector Timeline::find_intersecting_clips(int64_t requested_frame, in std::vector matching_clips; // Calculate time of frame - float min_requested_frame = requested_frame; - float max_requested_frame = requested_frame + (number_of_frames - 1); + const int64_t min_requested_frame = requested_frame; + const int64_t max_requested_frame = requested_frame + (number_of_frames - 1); // Find Clips at this time + matching_clips.reserve(clips.size()); + const double fpsD = info.fps.ToDouble(); for (auto clip : clips) { // Does clip intersect the current requested time - long clip_start_position = round(clip->Position() * info.fps.ToDouble()) + 1; - long clip_end_position = round((clip->Position() + clip->Duration()) * info.fps.ToDouble()) + 1; + int64_t clip_start_position = static_cast(std::llround(clip->Position() * fpsD)) + 1; + int64_t clip_end_position = static_cast(std::llround((clip->Position() + clip->Duration()) * fpsD)) + 1; bool does_clip_intersect = (clip_start_position <= min_requested_frame || clip_start_position <= max_requested_frame) && @@ -1724,18 +1755,24 @@ void Timeline::ClearAllCache(bool deep) { // Loop through all clips try { for (const auto clip : clips) { - // Clear cache on clip - clip->Reader()->GetCache()->Clear(); - - // Clear nested Reader (if deep clear requested) - if (deep && clip->Reader()->Name() == "FrameMapper") { - FrameMapper *nested_reader = static_cast(clip->Reader()); - if (nested_reader->Reader() && nested_reader->Reader()->GetCache()) - nested_reader->Reader()->GetCache()->Clear(); + // Clear cache on clip and reader if present + if (clip->Reader()) { + if (auto rc = clip->Reader()->GetCache()) + rc->Clear(); + + // Clear nested Reader (if deep clear requested) + if (deep && clip->Reader()->Name() == "FrameMapper") { + FrameMapper *nested_reader = static_cast(clip->Reader()); + if (nested_reader->Reader()) { + if (auto nc = nested_reader->Reader()->GetCache()) + nc->Clear(); + } + } } // Clear clip cache - clip->GetCache()->Clear(); + if (auto cc = clip->GetCache()) + cc->Clear(); } } catch (const ReaderClosed & e) { // ... diff --git a/src/Timeline.h b/src/Timeline.h index e93d2a7f6..61a164f02 100644 --- a/src/Timeline.h +++ b/src/Timeline.h @@ -46,12 +46,18 @@ namespace openshot { /// Comparison method for sorting clip pointers (by Layer and then Position). Clips are sorted /// from lowest layer to top layer (since that is the sequence they need to be combined), and then /// by position (left to right). - struct CompareClips{ - bool operator()( openshot::Clip* lhs, openshot::Clip* rhs){ - if( lhs->Layer() < rhs->Layer() ) return true; - if( lhs->Layer() == rhs->Layer() && lhs->Position() <= rhs->Position() ) return true; - return false; - }}; + struct CompareClips { + bool operator()(openshot::Clip* lhs, openshot::Clip* rhs) const { + // Strict-weak ordering (no <=) to keep sort well-defined + if (lhs == rhs) return false; // irreflexive + if (lhs->Layer() != rhs->Layer()) + return lhs->Layer() < rhs->Layer(); + if (lhs->Position() != rhs->Position()) + return lhs->Position() < rhs->Position(); + // Stable tie-breaker on address to avoid equivalence when layer/position match + return std::less()(lhs, rhs); + } + }; /// Comparison method for sorting effect pointers (by Position, Layer, and Order). Effects are sorted /// from lowest layer to top layer (since that is sequence clips are combined), and then by diff --git a/tests/Timeline.cpp b/tests/Timeline.cpp index fc1115ce1..da57f8e53 100644 --- a/tests/Timeline.cpp +++ b/tests/Timeline.cpp @@ -757,52 +757,203 @@ TEST_CASE( "Multi-threaded Timeline GetFrame", "[libopenshot][timeline]" ) t = NULL; } -TEST_CASE( "Multi-threaded Timeline Add/Remove Clip", "[libopenshot][timeline]" ) +// --------------------------------------------------------------------------- +// New tests to validate removing timeline-level effects (incl. threading/locks) +// Paste at the end of tests/Timeline.cpp +// --------------------------------------------------------------------------- + +TEST_CASE( "RemoveEffect basic", "[libopenshot][timeline]" ) { - // Create timeline - Timeline *t = new Timeline(1280, 720, Fraction(24, 1), 48000, 2, LAYOUT_STEREO); - t->Open(); + // Create a simple timeline + Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); + + // Two timeline-level effects + Negate e1; e1.Id("E1"); e1.Layer(0); + Negate e2; e2.Id("E2"); e2.Layer(1); + + t.AddEffect(&e1); + t.AddEffect(&e2); + + // Sanity check + REQUIRE(t.Effects().size() == 2); + REQUIRE(t.GetEffect("E1") != nullptr); + REQUIRE(t.GetEffect("E2") != nullptr); + + // Remove one effect and verify it is truly gone + t.RemoveEffect(&e1); + auto effects_after = t.Effects(); + CHECK(effects_after.size() == 1); + CHECK(t.GetEffect("E1") == nullptr); + CHECK(t.GetEffect("E2") != nullptr); + CHECK(std::find(effects_after.begin(), effects_after.end(), &e1) == effects_after.end()); + + // Removing the same (already-removed) effect should be a no-op + t.RemoveEffect(&e1); + CHECK(t.Effects().size() == 1); +} + +TEST_CASE( "RemoveEffect not present is no-op", "[libopenshot][timeline]" ) +{ + Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); + + Negate existing; existing.Id("KEEP"); existing.Layer(0); + Negate never_added; never_added.Id("GHOST"); never_added.Layer(1); + + t.AddEffect(&existing); + REQUIRE(t.Effects().size() == 1); + + // Try to remove an effect pointer that was never added + t.RemoveEffect(&never_added); + + // State should be unchanged + CHECK(t.Effects().size() == 1); + CHECK(t.GetEffect("KEEP") != nullptr); + CHECK(t.GetEffect("GHOST") == nullptr); +} + +TEST_CASE( "RemoveEffect while open (active pipeline safety)", "[libopenshot][timeline]" ) +{ + // Timeline with one visible clip so we can request frames + Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); + std::stringstream path; + path << TEST_MEDIA_PATH << "front3.png"; + Clip clip(path.str()); + clip.Layer(0); + t.AddClip(&clip); + + // Add a timeline-level effect and open the timeline + Negate neg; neg.Id("NEG"); neg.Layer(1); + t.AddEffect(&neg); + + t.Open(); + // Touch the pipeline before removal + std::shared_ptr f1 = t.GetFrame(1); + REQUIRE(f1 != nullptr); + + // Remove the effect while open, this should be safe and effective + t.RemoveEffect(&neg); + CHECK(t.GetEffect("NEG") == nullptr); + CHECK(t.Effects().size() == 0); + + // Touch the pipeline again after removal (should not crash / deadlock) + std::shared_ptr f2 = t.GetFrame(2); + REQUIRE(f2 != nullptr); + + // Close reader + t.Close(); +} + +TEST_CASE( "RemoveEffect preserves ordering of remaining effects", "[libopenshot][timeline]" ) +{ + // Create a timeline + Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO); + + // Add effects out of order (Layer/Position/Order) + Negate a; a.Id("A"); a.Layer(0); a.Position(0.0); a.Order(0); + Negate b1; b1.Id("B-1"); b1.Layer(1); b1.Position(0.0); b1.Order(3); + Negate b; b.Id("B"); b.Layer(1); b.Position(0.0); b.Order(0); + Negate b2; b2.Id("B-2"); b2.Layer(1); b2.Position(0.5); b2.Order(2); + Negate b3; b3.Id("B-3"); b3.Layer(1); b3.Position(0.5); b3.Order(1); + Negate c; c.Id("C"); c.Layer(2); c.Position(0.0); c.Order(0); + + t.AddEffect(&c); + t.AddEffect(&b); + t.AddEffect(&a); + t.AddEffect(&b3); + t.AddEffect(&b2); + t.AddEffect(&b1); + + // Remove a middle effect and verify ordering is still deterministic + t.RemoveEffect(&b); + + std::list effects = t.Effects(); + REQUIRE(effects.size() == 5); + + int n = 0; + for (auto effect : effects) { + switch (n) { + case 0: + CHECK(effect->Layer() == 0); + CHECK(effect->Id() == "A"); + CHECK(effect->Order() == 0); + break; + case 1: + CHECK(effect->Layer() == 1); + CHECK(effect->Id() == "B-1"); + CHECK(effect->Position() == Approx(0.0).margin(0.0001)); + CHECK(effect->Order() == 3); + break; + case 2: + CHECK(effect->Layer() == 1); + CHECK(effect->Id() == "B-2"); + CHECK(effect->Position() == Approx(0.5).margin(0.0001)); + CHECK(effect->Order() == 2); + break; + case 3: + CHECK(effect->Layer() == 1); + CHECK(effect->Id() == "B-3"); + CHECK(effect->Position() == Approx(0.5).margin(0.0001)); + CHECK(effect->Order() == 1); + break; + case 4: + CHECK(effect->Layer() == 2); + CHECK(effect->Id() == "C"); + CHECK(effect->Order() == 0); + break; + } + ++n; + } +} - // Calculate test video path +TEST_CASE( "Multi-threaded Timeline Add/Remove Effect", "[libopenshot][timeline]" ) +{ + // Create timeline with a clip so frames can be requested + Timeline *t = new Timeline(1280, 720, Fraction(24, 1), 48000, 2, LAYOUT_STEREO); std::stringstream path; path << TEST_MEDIA_PATH << "test.mp4"; + Clip *clip = new Clip(path.str()); + clip->Layer(0); + t->AddClip(clip); + t->Open(); - // A successful test will NOT crash - since this causes many threads to - // call the same Timeline methods asynchronously, to verify mutexes and multi-threaded - // access does not seg fault or crash this test. + // A successful test will NOT crash - many threads will add/remove effects + // while also requesting frames, exercising locks around effect mutation. #pragma omp parallel { - // Run the following loop in all threads - int64_t clip_count = 10; - for (int clip_index = 1; clip_index <= clip_count; clip_index++) { - // Create clip - Clip* clip_video = new Clip(path.str()); - clip_video->Layer(omp_get_thread_num()); - - // Add clip to timeline - t->AddClip(clip_video); - - // Loop through all timeline frames - each new clip makes the timeline longer - for (long int frame = 10; frame >= 1; frame--) { + int64_t effect_count = 10; + for (int i = 0; i < effect_count; ++i) { + // Each thread creates its own effect + Negate *neg = new Negate(); + std::stringstream sid; + sid << "NEG_T" << omp_get_thread_num() << "_I" << i; + neg->Id(sid.str()); + neg->Layer(1 + omp_get_thread_num()); // spread across layers + + // Add the effect + t->AddEffect(neg); + + // Touch a few frames to exercise the render pipeline with the effect + for (long int frame = 1; frame <= 6; ++frame) { std::shared_ptr f = t->GetFrame(frame); - t->GetMaxFrame(); + REQUIRE(f != nullptr); } - // Remove clip - t->RemoveClip(clip_video); - delete clip_video; - clip_video = NULL; + // Remove the effect and destroy it + t->RemoveEffect(neg); + delete neg; + neg = nullptr; } - // Clear all clips after loop is done - // This is designed to test the mutex for Clear() - t->Clear(); + // Clear all effects at the end from within threads (should be safe) + // This also exercises internal sorting/locking paths + t->Clear(); } - // Close and delete timeline object t->Close(); delete t; - t = NULL; + t = nullptr; + delete clip; + clip = nullptr; } TEST_CASE( "ApplyJSONDiff and FrameMappers", "[libopenshot][timeline]" ) From d77f3e53381697214f2aa01893f2c4175dc60f2e Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 12 Sep 2025 14:54:46 -0500 Subject: [PATCH 25/41] Improving performance on Clip class: - Replacing alpha with QPainter SetOpactity (much faster) - Fixing get_file_extension to not crash with filepaths that do no contain a "." - Removing render hints from apply_background (since no transform or text rendering), making compositing (faster in certain cases) - Optionally adding SmoothPixmapTransform based on a valid transform (faster in certain cases) - Skip Opacity for fully opaque clips - New Clip unit tests to validate new functionality --- src/Clip.cpp | 53 ++++++------- tests/Clip.cpp | 202 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 223 insertions(+), 32 deletions(-) diff --git a/src/Clip.cpp b/src/Clip.cpp index 6866486ea..16e1b0523 100644 --- a/src/Clip.cpp +++ b/src/Clip.cpp @@ -515,8 +515,13 @@ std::shared_ptr Clip::GetParentTrackedObject() { // Get file extension std::string Clip::get_file_extension(std::string path) { - // return last part of path - return path.substr(path.find_last_of(".") + 1); + // Return last part of path safely (handle filenames without a dot) + const auto dot_pos = path.find_last_of('.'); + if (dot_pos == std::string::npos || dot_pos + 1 >= path.size()) { + return std::string(); + } + + return path.substr(dot_pos + 1); } // Adjust the audio and image of a time mapped frame @@ -1190,7 +1195,6 @@ void Clip::apply_background(std::shared_ptr frame, std::shared_ // Add background canvas std::shared_ptr background_canvas = background_frame->GetImage(); QPainter painter(background_canvas.get()); - painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing, true); // Composite a new layer onto the image painter.setCompositionMode(QPainter::CompositionMode_SourceOver); @@ -1248,14 +1252,26 @@ void Clip::apply_keyframes(std::shared_ptr frame, QSize timeline_size) { // Load timeline's new frame image into a QPainter QPainter painter(background_canvas.get()); - painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing, true); - + painter.setRenderHint(QPainter::TextAntialiasing, true); + if (!transform.isIdentity()) { + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + } // Apply transform (translate, rotate, scale) painter.setTransform(transform); // Composite a new layer onto the image painter.setCompositionMode(QPainter::CompositionMode_SourceOver); - painter.drawImage(0, 0, *source_image); + + // Apply opacity via painter instead of per-pixel alpha manipulation + const float alpha_value = alpha.GetValue(frame->number); + if (alpha_value != 1.0f) { + painter.setOpacity(alpha_value); + painter.drawImage(0, 0, *source_image); + // Reset so any subsequent drawing (e.g., overlays) isn’t faded + painter.setOpacity(1.0); + } else { + painter.drawImage(0, 0, *source_image); + } if (timeline) { Timeline *t = static_cast(timeline); @@ -1348,31 +1364,6 @@ QTransform Clip::get_transform(std::shared_ptr frame, int width, int heig // Get image from clip std::shared_ptr source_image = frame->GetImage(); - /* ALPHA & OPACITY */ - if (alpha.GetValue(frame->number) != 1.0) - { - float alpha_value = alpha.GetValue(frame->number); - - // Get source image's pixels - unsigned char *pixels = source_image->bits(); - - // Loop through pixels - for (int pixel = 0, byte_index=0; pixel < source_image->width() * source_image->height(); pixel++, byte_index+=4) - { - // Apply alpha to pixel values (since we use a premultiplied value, we must - // multiply the alpha with all colors). - pixels[byte_index + 0] *= alpha_value; - pixels[byte_index + 1] *= alpha_value; - pixels[byte_index + 2] *= alpha_value; - pixels[byte_index + 3] *= alpha_value; - } - - // Debug output - ZmqLogger::Instance()->AppendDebugMethod("Clip::get_transform (Set Alpha & Opacity)", - "alpha_value", alpha_value, - "frame->number", frame->number); - } - /* RESIZE SOURCE IMAGE - based on scale type */ QSize source_size = scale_size(source_image->size(), scale, width, height); diff --git a/tests/Clip.cpp b/tests/Clip.cpp index a6e3e6299..c16960ccb 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -489,4 +489,204 @@ TEST_CASE( "resample_audio_8000_to_48000_reverse", "[libopenshot][clip]" ) map.Close(); reader.Close(); clip.Close(); -} \ No newline at end of file +} + +// ----------------------------------------------------------------------------- +// Additional tests validating PR changes: +// - safe extension parsing (no dot in path) +// - painter-based opacity behavior (no per-pixel mutation) +// - transform/scaling path sanity (conditional render hint use) +// ----------------------------------------------------------------------------- + +TEST_CASE( "safe_extension_parsing_no_dot", "[libopenshot][clip][pr]" ) +{ + // Constructing a Clip with a path that has no dot used to risk UB in get_file_extension(); + // This should now be safe and simply result in no reader being set. + openshot::Clip c1("this_is_not_a_real_path_and_has_no_extension"); + + // Reader() should throw since no reader could be inferred. + CHECK_THROWS_AS(c1.Reader(), openshot::ReaderClosed); + + // Opening also throws (consistent with other tests for unopened readers). + CHECK_THROWS_AS(c1.Open(), openshot::ReaderClosed); +} + +TEST_CASE( "painter_opacity_applied_no_per_pixel_mutation", "[libopenshot][clip][pr]" ) +{ + // Build a red frame via DummyReader (no copies/assignments of DummyReader) + openshot::CacheMemory cache; + auto f = std::make_shared(1, 80, 60, "#000000", 0, 2); + f->AddColor(QColor(Qt::red)); // opaque red + cache.Add(f); + + openshot::DummyReader dummy(openshot::Fraction(30,1), 80, 60, 44100, 2, 1.0, &cache); + dummy.Open(); + + // Clip that uses the dummy reader + openshot::Clip clip; + clip.Reader(&dummy); + clip.Open(); + + // Alpha 0.5 at frame 1 (exercise painter.setOpacity path) + clip.alpha.AddPoint(1, 0.5); + clip.display = openshot::FRAME_DISPLAY_NONE; // avoid font/overlay variability + + // Render frame 1 (no timeline needed for this check) + std::shared_ptr out_f = clip.GetFrame(1); + auto img = out_f->GetImage(); + REQUIRE(img); // must exist + REQUIRE(img->format() == QImage::Format_RGBA8888_Premultiplied); + + // Pixel well inside the image should be "half-transparent red" over transparent bg. + // In Qt, pixelColor() returns unpremultiplied values, so expect alpha ≈ 127 and red ≈ 255. + QColor p = img->pixelColor(70, 50); + CHECK(p.alpha() == Approx(127).margin(10)); + CHECK(p.red() == Approx(255).margin(2)); + CHECK(p.green() == Approx(0).margin(2)); + CHECK(p.blue() == Approx(0).margin(2)); +} + +TEST_CASE( "composite_over_opaque_background_blend", "[libopenshot][clip][pr]" ) +{ + // Red source clip frame (fully opaque) + openshot::CacheMemory cache; + auto f = std::make_shared(1, 64, 64, "#000000", 0, 2); + f->AddColor(QColor(Qt::red)); + cache.Add(f); + + openshot::DummyReader dummy(openshot::Fraction(30,1), 64, 64, 44100, 2, 1.0, &cache); + dummy.Open(); + + openshot::Clip clip; + clip.Reader(&dummy); + clip.Open(); + + // Make clip semi-transparent via alpha (0.5) + clip.alpha.AddPoint(1, 0.5); + clip.display = openshot::FRAME_DISPLAY_NONE; // no overlay here + + // Build a blue, fully-opaque background frame and composite into it + auto bg = std::make_shared(1, 64, 64, "#000000", 0, 2); + bg->AddColor(QColor(Qt::blue)); // blue background, opaque + + // Composite the clip onto bg + std::shared_ptr out = clip.GetFrame(bg, /*clip_frame_number*/1); + auto img = out->GetImage(); + REQUIRE(img); + + // Center pixel should be purple-ish and fully opaque (red over blue @ 50% -> roughly (127,0,127), A=255) + QColor center = img->pixelColor(32, 32); + CHECK(center.alpha() == Approx(255).margin(0)); + CHECK(center.red() == Approx(127).margin(12)); + CHECK(center.green() == Approx(0).margin(6)); + CHECK(center.blue() == Approx(127).margin(12)); +} + +TEST_CASE( "transform_path_identity_vs_scaled", "[libopenshot][clip][pr]" ) +{ + // Create a small checker-ish image to make scaling detectable + const int W = 60, H = 40; + QImage src(W, H, QImage::Format_RGBA8888_Premultiplied); + src.fill(QColor(Qt::black)); + { + QPainter p(&src); + p.setPen(QColor(Qt::white)); + for (int x = 0; x < W; x += 4) p.drawLine(x, 0, x, H-1); + for (int y = 0; y < H; y += 4) p.drawLine(0, y, W-1, y); + } + + // Stuff the image into a Frame -> Cache -> DummyReader + openshot::CacheMemory cache; + auto f = std::make_shared(1, W, H, "#000000", 0, 2); + f->AddImage(std::make_shared(src)); + cache.Add(f); + + openshot::DummyReader dummy(openshot::Fraction(30,1), W, H, 44100, 2, 1.0, &cache); + dummy.Open(); + + openshot::Clip clip; + clip.Reader(&dummy); + clip.Open(); + + // Helper lambda to count "near-white" pixels in a region (for debug/metrics) + auto count_white = [](const QImage& im, int x0, int y0, int x1, int y1)->int { + int cnt = 0; + for (int y = y0; y <= y1; ++y) { + for (int x = x0; x <= x1; ++x) { + QColor c = im.pixelColor(x, y); + if (c.red() > 240 && c.green() > 240 && c.blue() > 240) ++cnt; + } + } + return cnt; + }; + + // Helper lambda to compute per-pixel difference count between two images + auto diff_count = [](const QImage& a, const QImage& b, int x0, int y0, int x1, int y1)->int { + int cnt = 0; + for (int y = y0; y <= y1; ++y) { + for (int x = x0; x <= x1; ++x) { + QColor ca = a.pixelColor(x, y); + QColor cb = b.pixelColor(x, y); + int dr = std::abs(ca.red() - cb.red()); + int dg = std::abs(ca.green() - cb.green()); + int db = std::abs(ca.blue() - cb.blue()); + // treat any noticeable RGB change as a difference + if ((dr + dg + db) > 24) ++cnt; + } + } + return cnt; + }; + + // Case A: Identity transform (no move/scale/rotate). Output should match source at a white grid point. + std::shared_ptr out_identity; + { + clip.scale_x = openshot::Keyframe(1.0); + clip.scale_y = openshot::Keyframe(1.0); + clip.rotation = openshot::Keyframe(0.0); + clip.location_x = openshot::Keyframe(0.0); + clip.location_y = openshot::Keyframe(0.0); + clip.display = openshot::FRAME_DISPLAY_NONE; + + out_identity = clip.GetFrame(1); + auto img = out_identity->GetImage(); + REQUIRE(img); + // Pick a mid pixel that is white in the grid (multiple of 4) + QColor c = img->pixelColor(20, 20); + CHECK(c.red() >= 240); + CHECK(c.green() >= 240); + CHECK(c.blue() >= 240); + } + + // Case B: Downscale (trigger transform path). Clear the clip cache so we don't + // accidentally re-use the identity frame from final_cache. + { + clip.GetCache()->Clear(); // **critical fix** ensure recompute after keyframe changes + + // Force a downscale to half + clip.scale_x = openshot::Keyframe(0.5); + clip.scale_y = openshot::Keyframe(0.5); + clip.rotation = openshot::Keyframe(0.0); + clip.location_x = openshot::Keyframe(0.0); + clip.location_y = openshot::Keyframe(0.0); + clip.display = openshot::FRAME_DISPLAY_NONE; + + auto out_scaled = clip.GetFrame(1); + auto img_scaled = out_scaled->GetImage(); + REQUIRE(img_scaled); + + // Measure difference vs identity in a central region to avoid edges + const int x0 = 8, y0 = 8, x1 = W - 9, y1 = H - 9; + int changed = diff_count(*out_identity->GetImage(), *img_scaled, x0, y0, x1, y1); + int region_area = (x1 - x0 + 1) * (y1 - y0 + 1); + + // After scaling, the image must not be identical to identity output. + // Using a minimal check keeps this robust across Qt versions and platforms. + CHECK(changed > 0); + + // Optional diagnostic: scaled typically yields <= number of pure whites vs identity. + int white_id = count_white(*out_identity->GetImage(), x0, y0, x1, y1); + int white_sc = count_white(*img_scaled, x0, y0, x1, y1); + CHECK(white_sc <= white_id); + } +} + From f2a5bfb5814fce392d4aee7ba10dd1e811af4f08 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 12 Sep 2025 15:04:14 -0500 Subject: [PATCH 26/41] Fixed AnalogTape tests and an unused Clip test line --- tests/AnalogTape.cpp | 21 +++++++++------------ tests/Clip.cpp | 1 - 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/AnalogTape.cpp b/tests/AnalogTape.cpp index 0a7ee45d5..1a5357a93 100644 --- a/tests/AnalogTape.cpp +++ b/tests/AnalogTape.cpp @@ -14,19 +14,16 @@ using namespace openshot; -static std::shared_ptr makeGrayFrame() { - QImage img(5, 5, QImage::Format_ARGB32); - img.fill(QColor(100, 100, 100, 255)); - auto f = std::make_shared(); - *f->GetImage() = img; - return f; -} +// Fixed helper ensures Frame invariants are respected (size/format/flags) +static std::shared_ptr makeGrayFrame(int w = 64, int h = 64) { + auto f = std::make_shared(1, w, h, "#000000", 0, 2); + + // Use premultiplied format to match Frame::AddImage expectations + auto img = std::make_shared(w, h, QImage::Format_RGBA8888_Premultiplied); + img->fill(QColor(100, 100, 100, 255)); -static std::shared_ptr makeGrayFrame(int w, int h) { - QImage img(w, h, QImage::Format_ARGB32); - img.fill(QColor(100, 100, 100, 255)); - auto f = std::make_shared(); - *f->GetImage() = img; + // Route through AddImage so width/height/has_image_data are set correctly + f->AddImage(img); return f; } diff --git a/tests/Clip.cpp b/tests/Clip.cpp index c16960ccb..e37e2315b 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -677,7 +677,6 @@ TEST_CASE( "transform_path_identity_vs_scaled", "[libopenshot][clip][pr]" ) // Measure difference vs identity in a central region to avoid edges const int x0 = 8, y0 = 8, x1 = W - 9, y1 = H - 9; int changed = diff_count(*out_identity->GetImage(), *img_scaled, x0, y0, x1, y1); - int region_area = (x1 - x0 + 1) * (y1 - y0 + 1); // After scaling, the image must not be identical to identity output. // Using a minimal check keeps this robust across Qt versions and platforms. From a326f541a174fe0bfccfd75d7f1735c5c4f3f69c Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 12 Sep 2025 15:07:51 -0500 Subject: [PATCH 27/41] Fix bug with Wave effect that can cause colored bands to appear in certain cases, and added new wave effect unit test --- src/effects/Wave.cpp | 9 +++++---- tests/CMakeLists.txt | 1 + tests/WaveEffect.cpp | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 tests/WaveEffect.cpp diff --git a/src/effects/Wave.cpp b/src/effects/Wave.cpp index 286cb6322..9e2854eaa 100644 --- a/src/effects/Wave.cpp +++ b/src/effects/Wave.cpp @@ -51,9 +51,10 @@ std::shared_ptr Wave::GetFrame(std::shared_ptr // Get the frame's image std::shared_ptr frame_image = frame->GetImage(); - // Get original pixels for frame image, and also make a copy for editing - const unsigned char *original_pixels = (unsigned char *) frame_image->constBits(); - unsigned char *pixels = (unsigned char *) frame_image->bits(); + // Copy original pixels for reference, and get a writable pointer for editing + QImage original = frame_image->copy(); + const unsigned char *original_pixels = original.constBits(); + unsigned char *pixels = frame_image->bits(); int pixel_count = frame_image->width() * frame_image->height(); // Get current keyframe values @@ -77,7 +78,7 @@ std::shared_ptr Wave::GetFrame(std::shared_ptr float waveformVal = sin((Y * wavelength_value) + (time * speed_y_value)); // Waveform algorithm on y-axis float waveVal = (waveformVal + shift_x_value) * noiseAmp; // Shifts pixels on the x-axis - long unsigned int source_px = round(pixel + waveVal); + int source_px = lround(pixel + waveVal); if (source_px < 0) source_px = 0; if (source_px >= pixel_count) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 475ac4eb0..6cd560a8f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -51,6 +51,7 @@ set(OPENSHOT_TESTS AnalogTape Sharpen SphericalEffect + WaveEffect ) # ImageMagick related test files diff --git a/tests/WaveEffect.cpp b/tests/WaveEffect.cpp new file mode 100644 index 000000000..da4386c3b --- /dev/null +++ b/tests/WaveEffect.cpp @@ -0,0 +1,41 @@ +/** + * @file + * @brief Unit tests for openshot::Wave effect + * @author OpenAI ChatGPT + */ + +// Copyright (c) 2008-2025 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include +#include + +#include "Frame.h" +#include "effects/Wave.h" +#include "openshot_catch.h" + +using namespace openshot; + +TEST_CASE("Wave uses original pixel buffer", "[effect][wave]") +{ + // Create 1x10 image with increasing red channel + QImage img(10, 1, QImage::Format_ARGB32); + for (int x = 0; x < 10; ++x) + img.setPixelColor(x, 0, QColor(x, 0, 0, 255)); + auto f = std::make_shared(); + *f->GetImage() = img; + + Wave w; + w.wavelength = Keyframe(0.0); + w.amplitude = Keyframe(1.0); + w.multiplier = Keyframe(0.01); + w.shift_x = Keyframe(-1.0); // negative shift to copy from previous pixel + w.speed_y = Keyframe(0.0); + + auto out_img = w.GetFrame(f, 1)->GetImage(); + int expected[10] = {0,0,1,2,3,4,5,6,7,8}; + for (int x = 0; x < 10; ++x) + CHECK(out_img->pixelColor(x,0).red() == expected[x]); +} From b94dcac3b4a1d6573cd44ab57418dc9b6f80e650 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 12 Sep 2025 17:27:43 -0500 Subject: [PATCH 28/41] Adding Benchmark executable to assist with performance testing and comparisons with different versions of OpenShot. Initial results: FFmpegWriter,7800 FrameMapper,3508 Clip,4958 Timeline,30817 Timeline (with transforms),53951 Effect_Mask,9283 Effect_Brightness,12486 Effect_Crop,5153 Effect_Saturation,15545 Overall,147136 --- tests/Benchmark.cpp | 218 +++++++++++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 5 + 2 files changed, 223 insertions(+) create mode 100644 tests/Benchmark.cpp diff --git a/tests/Benchmark.cpp b/tests/Benchmark.cpp new file mode 100644 index 000000000..e9492cfe1 --- /dev/null +++ b/tests/Benchmark.cpp @@ -0,0 +1,218 @@ +/** + * @file + * @brief Benchmark executable for core libopenshot operations + * @author Jonathan Thomas + * @ref License + */ +// Copyright (c) 2025 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include +#include +#include +#include + +#include "Clip.h" +#include "FFmpegReader.h" +#include "FFmpegWriter.h" +#include "Fraction.h" +#include "FrameMapper.h" +#include "ImageReader.h" +#include "ReaderBase.h" +#include "Timeline.h" +#include "effects/Brightness.h" +#include "effects/Crop.h" +#include "effects/Mask.h" +#include "effects/Saturation.h" + +using namespace openshot; +using namespace std; + +using Clock = chrono::steady_clock; + +template double time_trial(const string &name, Func func) { + auto start = Clock::now(); + func(); + auto elapsed = + chrono::duration_cast(Clock::now() - start).count(); + cout << name << "," << elapsed << "\n"; + return static_cast(elapsed); +} + +void read_forward_backward(ReaderBase &reader) { + int64_t len = reader.info.video_length; + for (int64_t i = 1; i <= len; ++i) + reader.GetFrame(i); + for (int64_t i = len; i >= 1; --i) + reader.GetFrame(i); +} + +int main() { + cout << "Trial,Milliseconds\n"; + double total = 0.0; + const string base = TEST_MEDIA_PATH; + const string video = base + "sintel_trailer-720p.mp4"; + const string mask_img = base + "mask.png"; + const string overlay = base + "front3.png"; + + total += time_trial("FFmpegReader", [&]() { + FFmpegReader r(video); + r.Open(); + read_forward_backward(r); + r.Close(); + }); + + total += time_trial("FFmpegWriter", [&]() { + FFmpegReader r(video); + r.Open(); + FFmpegWriter w("benchmark_output.mp4"); + w.SetAudioOptions("aac", r.info.sample_rate, 192000); + w.SetVideoOptions("libx264", r.info.width, r.info.height, r.info.fps, + 5000000); + w.Open(); + for (int64_t i = 1; i <= r.info.video_length; ++i) + w.WriteFrame(r.GetFrame(i)); + w.Close(); + r.Close(); + }); + + total += time_trial("FrameMapper", [&]() { + vector rates = {Fraction(24, 1), Fraction(30, 1), Fraction(60, 1), + Fraction(30000, 1001), Fraction(60000, 1001)}; + for (auto &fps : rates) { + FFmpegReader r(video); + r.Open(); + FrameMapper map(&r, fps, PULLDOWN_NONE, r.info.sample_rate, + r.info.channels, r.info.channel_layout); + map.Open(); + for (int64_t i = 1; i <= map.info.video_length; ++i) + map.GetFrame(i); + map.Close(); + r.Close(); + } + }); + + total += time_trial("Clip", [&]() { + Clip c(video); + c.Open(); + read_forward_backward(c); + c.Close(); + }); + + total += time_trial("Timeline", [&]() { + Timeline t(1920, 1080, Fraction(24, 1), 44100, 2, LAYOUT_STEREO); + Clip video_clip(video); + video_clip.Layer(0); + video_clip.Start(0.0); + video_clip.End(video_clip.Reader()->info.duration); + video_clip.Open(); + Clip overlay1(overlay); + overlay1.Layer(1); + overlay1.Start(0.0); + overlay1.End(video_clip.Reader()->info.duration); + overlay1.Open(); + Clip overlay2(overlay); + overlay2.Layer(2); + overlay2.Start(0.0); + overlay2.End(video_clip.Reader()->info.duration); + overlay2.Open(); + t.AddClip(&video_clip); + t.AddClip(&overlay1); + t.AddClip(&overlay2); + t.Open(); + t.info.video_length = t.GetMaxFrame(); + read_forward_backward(t); + t.Close(); + }); + + total += time_trial("Timeline (with transforms)", [&]() { + Timeline t(1920, 1080, Fraction(24, 1), 44100, 2, LAYOUT_STEREO); + Clip video_clip(video); + int64_t last = video_clip.Reader()->info.video_length; + video_clip.Layer(0); + video_clip.Start(0.0); + video_clip.End(video_clip.Reader()->info.duration); + video_clip.alpha.AddPoint(1, 1.0); + video_clip.alpha.AddPoint(last, 0.0); + video_clip.Open(); + Clip overlay1(overlay); + overlay1.Layer(1); + overlay1.Start(0.0); + overlay1.End(video_clip.Reader()->info.duration); + overlay1.Open(); + overlay1.scale_x.AddPoint(1, 1.0); + overlay1.scale_x.AddPoint(last, 0.25); + overlay1.scale_y.AddPoint(1, 1.0); + overlay1.scale_y.AddPoint(last, 0.25); + Clip overlay2(overlay); + overlay2.Layer(2); + overlay2.Start(0.0); + overlay2.End(video_clip.Reader()->info.duration); + overlay2.Open(); + overlay2.rotation.AddPoint(1, 90.0); + t.AddClip(&video_clip); + t.AddClip(&overlay1); + t.AddClip(&overlay2); + t.Open(); + t.info.video_length = t.GetMaxFrame(); + read_forward_backward(t); + t.Close(); + }); + + total += time_trial("Effect_Mask", [&]() { + FFmpegReader r(video); + r.Open(); + ImageReader mask_reader(mask_img); + mask_reader.Open(); + Clip clip(&r); + clip.Open(); + Mask m(&mask_reader, Keyframe(0.0), Keyframe(0.5)); + clip.AddEffect(&m); + read_forward_backward(clip); + mask_reader.Close(); + clip.Close(); + r.Close(); + }); + + total += time_trial("Effect_Brightness", [&]() { + FFmpegReader r(video); + r.Open(); + Clip clip(&r); + clip.Open(); + Brightness b(Keyframe(0.5), Keyframe(1.0)); + clip.AddEffect(&b); + read_forward_backward(clip); + clip.Close(); + r.Close(); + }); + + total += time_trial("Effect_Crop", [&]() { + FFmpegReader r(video); + r.Open(); + Clip clip(&r); + clip.Open(); + Crop c(Keyframe(0.25), Keyframe(0.25), Keyframe(0.25), Keyframe(0.25)); + clip.AddEffect(&c); + read_forward_backward(clip); + clip.Close(); + r.Close(); + }); + + total += time_trial("Effect_Saturation", [&]() { + FFmpegReader r(video); + r.Open(); + Clip clip(&r); + clip.Open(); + Saturation s(Keyframe(0.25), Keyframe(0.25), Keyframe(0.25), + Keyframe(0.25)); + clip.AddEffect(&s); + read_forward_backward(clip); + clip.Close(); + r.Close(); + }); + + cout << "Overall," << total << "\n"; + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6cd560a8f..50d71b3f4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,11 @@ endif() # Test media path, used by unit tests for input data file(TO_NATIVE_PATH "${PROJECT_SOURCE_DIR}/examples/" TEST_MEDIA_PATH) +# Benchmark executable +add_executable(openshot-benchmark Benchmark.cpp) +target_compile_definitions(openshot-benchmark PRIVATE -DTEST_MEDIA_PATH="${TEST_MEDIA_PATH}") +target_link_libraries(openshot-benchmark openshot) + ### ### TEST SOURCE FILES ### From 523ef17aa497f67f417e949fcf1fbd930f7d0965 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 12 Sep 2025 17:47:41 -0500 Subject: [PATCH 29/41] Adding composite/blend modes to libopenshot: -Normal -Darken -Multiply -Color Burn -Lighten -Screen -Color Dodge -Add -Overlay -Soft Light -Hard Light -Difference -Exclusion --- src/Clip.cpp | 41 +++++++++++++++++++++++++++++++++++++++-- src/Clip.h | 1 + src/Enums.h | 31 +++++++++++++++++++++++++++++++ tests/Clip.cpp | 13 ++++++++----- 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/Clip.cpp b/src/Clip.cpp index 16e1b0523..9c3e62c61 100644 --- a/src/Clip.cpp +++ b/src/Clip.cpp @@ -32,6 +32,34 @@ using namespace openshot; +namespace { + struct CompositeChoice { const char* name; CompositeType value; }; + const CompositeChoice composite_choices[] = { + {"Normal", COMPOSITE_SOURCE_OVER}, + + // Darken group + {"Darken", COMPOSITE_DARKEN}, + {"Multiply", COMPOSITE_MULTIPLY}, + {"Color Burn", COMPOSITE_COLOR_BURN}, + + // Lighten group + {"Lighten", COMPOSITE_LIGHTEN}, + {"Screen", COMPOSITE_SCREEN}, + {"Color Dodge", COMPOSITE_COLOR_DODGE}, + {"Add", COMPOSITE_PLUS}, + + // Contrast group + {"Overlay", COMPOSITE_OVERLAY}, + {"Soft Light", COMPOSITE_SOFT_LIGHT}, + {"Hard Light", COMPOSITE_HARD_LIGHT}, + + // Compare + {"Difference", COMPOSITE_DIFFERENCE}, + {"Exclusion", COMPOSITE_EXCLUSION}, + }; + const int composite_choices_count = sizeof(composite_choices)/sizeof(CompositeChoice); +} + // Init default settings for a clip void Clip::init_settings() { @@ -45,6 +73,7 @@ void Clip::init_settings() anchor = ANCHOR_CANVAS; display = FRAME_DISPLAY_NONE; mixing = VOLUME_MIX_NONE; + composite = COMPOSITE_SOURCE_OVER; waveform = false; previous_properties = ""; parentObjectId = ""; @@ -766,6 +795,7 @@ std::string Clip::PropertiesJSON(int64_t requested_frame) const { root["scale"] = add_property_json("Scale", scale, "int", "", NULL, 0, 3, false, requested_frame); root["display"] = add_property_json("Frame Number", display, "int", "", NULL, 0, 3, false, requested_frame); root["mixing"] = add_property_json("Volume Mixing", mixing, "int", "", NULL, 0, 2, false, requested_frame); + root["composite"] = add_property_json("Composite", composite, "int", "", NULL, 0, composite_choices_count - 1, false, requested_frame); root["waveform"] = add_property_json("Waveform", waveform, "int", "", NULL, 0, 1, false, requested_frame); root["parentObjectId"] = add_property_json("Parent", 0.0, "string", parentObjectId, NULL, -1, -1, false, requested_frame); @@ -797,6 +827,10 @@ std::string Clip::PropertiesJSON(int64_t requested_frame) const { root["mixing"]["choices"].append(add_property_choice_json("Average", VOLUME_MIX_AVERAGE, mixing)); root["mixing"]["choices"].append(add_property_choice_json("Reduce", VOLUME_MIX_REDUCE, mixing)); + // Add composite choices (dropdown style) + for (int i = 0; i < composite_choices_count; ++i) + root["composite"]["choices"].append(add_property_choice_json(composite_choices[i].name, composite_choices[i].value, composite)); + // Add waveform choices (dropdown style) root["waveform"]["choices"].append(add_property_choice_json("Yes", true, waveform)); root["waveform"]["choices"].append(add_property_choice_json("No", false, waveform)); @@ -879,6 +913,7 @@ Json::Value Clip::JsonValue() const { root["anchor"] = anchor; root["display"] = display; root["mixing"] = mixing; + root["composite"] = composite; root["waveform"] = waveform; root["scale_x"] = scale_x.JsonValue(); root["scale_y"] = scale_y.JsonValue(); @@ -967,6 +1002,8 @@ void Clip::SetJsonValue(const Json::Value root) { display = (FrameDisplayType) root["display"].asInt(); if (!root["mixing"].isNull()) mixing = (VolumeMixType) root["mixing"].asInt(); + if (!root["composite"].isNull()) + composite = (CompositeType) root["composite"].asInt(); if (!root["waveform"].isNull()) waveform = root["waveform"].asBool(); if (!root["scale_x"].isNull()) @@ -1197,7 +1234,7 @@ void Clip::apply_background(std::shared_ptr frame, std::shared_ QPainter painter(background_canvas.get()); // Composite a new layer onto the image - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.setCompositionMode(static_cast(composite)); painter.drawImage(0, 0, *frame->GetImage()); painter.end(); @@ -1260,7 +1297,7 @@ void Clip::apply_keyframes(std::shared_ptr frame, QSize timeline_size) { painter.setTransform(transform); // Composite a new layer onto the image - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.setCompositionMode(static_cast(composite)); // Apply opacity via painter instead of per-pixel alpha manipulation const float alpha_value = alpha.GetValue(frame->number); diff --git a/src/Clip.h b/src/Clip.h index caeabd57b..98da54014 100644 --- a/src/Clip.h +++ b/src/Clip.h @@ -169,6 +169,7 @@ namespace openshot { openshot::AnchorType anchor; ///< The anchor determines what parent a clip should snap to openshot::FrameDisplayType display; ///< The format to display the frame number (if any) openshot::VolumeMixType mixing; ///< What strategy should be followed when mixing audio with other clips + openshot::CompositeType composite; ///< How this clip is composited onto lower layers #ifdef USE_OPENCV bool COMPILED_WITH_CV = true; diff --git a/src/Enums.h b/src/Enums.h index 14b693166..e3029c1fe 100644 --- a/src/Enums.h +++ b/src/Enums.h @@ -64,6 +64,37 @@ enum VolumeMixType VOLUME_MIX_REDUCE ///< Reduce volume by about %25, and then mix (louder, but could cause pops if the sum exceeds 100%) }; +/// This enumeration determines how clips are composited onto lower layers. +enum CompositeType { + COMPOSITE_SOURCE_OVER, + COMPOSITE_DESTINATION_OVER, + COMPOSITE_CLEAR, + COMPOSITE_SOURCE, + COMPOSITE_DESTINATION, + COMPOSITE_SOURCE_IN, + COMPOSITE_DESTINATION_IN, + COMPOSITE_SOURCE_OUT, + COMPOSITE_DESTINATION_OUT, + COMPOSITE_SOURCE_ATOP, + COMPOSITE_DESTINATION_ATOP, + COMPOSITE_XOR, + + // svg 1.2 blend modes + COMPOSITE_PLUS, + COMPOSITE_MULTIPLY, + COMPOSITE_SCREEN, + COMPOSITE_OVERLAY, + COMPOSITE_DARKEN, + COMPOSITE_LIGHTEN, + COMPOSITE_COLOR_DODGE, + COMPOSITE_COLOR_BURN, + COMPOSITE_HARD_LIGHT, + COMPOSITE_SOFT_LIGHT, + COMPOSITE_DIFFERENCE, + COMPOSITE_EXCLUSION, + + COMPOSITE_LAST = COMPOSITE_EXCLUSION +}; /// This enumeration determines the distortion type of Distortion Effect. enum DistortionType diff --git a/tests/Clip.cpp b/tests/Clip.cpp index e37e2315b..ffd5c8324 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -42,6 +42,7 @@ TEST_CASE( "default constructor", "[libopenshot][clip]" ) CHECK(c1.anchor == ANCHOR_CANVAS); CHECK(c1.gravity == GRAVITY_CENTER); CHECK(c1.scale == SCALE_FIT); + CHECK(c1.composite == COMPOSITE_SOURCE_OVER); CHECK(c1.Layer() == 0); CHECK(c1.Position() == Approx(0.0f).margin(0.00001)); CHECK(c1.Start() == Approx(0.0f).margin(0.00001)); @@ -60,6 +61,7 @@ TEST_CASE( "path string constructor", "[libopenshot][clip]" ) CHECK(c1.anchor == ANCHOR_CANVAS); CHECK(c1.gravity == GRAVITY_CENTER); CHECK(c1.scale == SCALE_FIT); + CHECK(c1.composite == COMPOSITE_SOURCE_OVER); CHECK(c1.Layer() == 0); CHECK(c1.Position() == Approx(0.0f).margin(0.00001)); CHECK(c1.Start() == Approx(0.0f).margin(0.00001)); @@ -76,6 +78,7 @@ TEST_CASE( "basic getters and setters", "[libopenshot][clip]" ) CHECK(c1.anchor == ANCHOR_CANVAS); CHECK(c1.gravity == GRAVITY_CENTER); CHECK(c1.scale == SCALE_FIT); + CHECK(c1.composite == COMPOSITE_SOURCE_OVER); CHECK(c1.Layer() == 0); CHECK(c1.Position() == Approx(0.0f).margin(0.00001)); CHECK(c1.Start() == Approx(0.0f).margin(0.00001)); @@ -541,9 +544,9 @@ TEST_CASE( "painter_opacity_applied_no_per_pixel_mutation", "[libopenshot][clip] // In Qt, pixelColor() returns unpremultiplied values, so expect alpha ≈ 127 and red ≈ 255. QColor p = img->pixelColor(70, 50); CHECK(p.alpha() == Approx(127).margin(10)); - CHECK(p.red() == Approx(255).margin(2)); + CHECK(p.red() == Approx(255).margin(2)); CHECK(p.green() == Approx(0).margin(2)); - CHECK(p.blue() == Approx(0).margin(2)); + CHECK(p.blue() == Approx(0).margin(2)); } TEST_CASE( "composite_over_opaque_background_blend", "[libopenshot][clip][pr]" ) @@ -652,9 +655,9 @@ TEST_CASE( "transform_path_identity_vs_scaled", "[libopenshot][clip][pr]" ) REQUIRE(img); // Pick a mid pixel that is white in the grid (multiple of 4) QColor c = img->pixelColor(20, 20); - CHECK(c.red() >= 240); + CHECK(c.red() >= 240); CHECK(c.green() >= 240); - CHECK(c.blue() >= 240); + CHECK(c.blue() >= 240); } // Case B: Downscale (trigger transform path). Clear the clip cache so we don't @@ -684,7 +687,7 @@ TEST_CASE( "transform_path_identity_vs_scaled", "[libopenshot][clip][pr]" ) // Optional diagnostic: scaled typically yields <= number of pure whites vs identity. int white_id = count_white(*out_identity->GetImage(), x0, y0, x1, y1); - int white_sc = count_white(*img_scaled, x0, y0, x1, y1); + int white_sc = count_white(*img_scaled, x0, y0, x1, y1); CHECK(white_sc <= white_id); } } From a66727a6873082b4a3bbe1f4c19c8b53e9ffb648 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 12 Sep 2025 18:06:26 -0500 Subject: [PATCH 30/41] Expanding Clip unit tests to include all composite blend modes available to libopenshot. --- tests/Clip.cpp | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/Clip.cpp b/tests/Clip.cpp index ffd5c8324..51fdf215a 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include "Clip.h" #include "DummyReader.h" @@ -585,6 +587,95 @@ TEST_CASE( "composite_over_opaque_background_blend", "[libopenshot][clip][pr]" ) CHECK(center.blue() == Approx(127).margin(12)); } +TEST_CASE("all_composite_modes_simple_colors", "[libopenshot][clip][composite]") +{ + // Source clip: solid red + openshot::CacheMemory cache; + auto src = std::make_shared(1, 16, 16, "#000000", 0, 2); + src->AddColor(QColor(Qt::red)); + cache.Add(src); + + openshot::DummyReader dummy(openshot::Fraction(30, 1), 16, 16, 44100, 2, 1.0, &cache); + dummy.Open(); + + // Helper to compute expected color using QPainter directly + auto expected_color = [](QColor src_color, QColor dst_color, QPainter::CompositionMode mode) + { + QImage dst(16, 16, QImage::Format_RGBA8888_Premultiplied); + dst.fill(dst_color); + QPainter p(&dst); + p.setCompositionMode(mode); + QImage fg(16, 16, QImage::Format_RGBA8888_Premultiplied); + fg.fill(src_color); + p.drawImage(0, 0, fg); + p.end(); + return dst.pixelColor(8, 8); + }; + + const std::vector modes = { + COMPOSITE_SOURCE_OVER, + COMPOSITE_DESTINATION_OVER, + COMPOSITE_CLEAR, + COMPOSITE_SOURCE, + COMPOSITE_DESTINATION, + COMPOSITE_SOURCE_IN, + COMPOSITE_DESTINATION_IN, + COMPOSITE_SOURCE_OUT, + COMPOSITE_DESTINATION_OUT, + COMPOSITE_SOURCE_ATOP, + COMPOSITE_DESTINATION_ATOP, + COMPOSITE_XOR, + COMPOSITE_PLUS, + COMPOSITE_MULTIPLY, + COMPOSITE_SCREEN, + COMPOSITE_OVERLAY, + COMPOSITE_DARKEN, + COMPOSITE_LIGHTEN, + COMPOSITE_COLOR_DODGE, + COMPOSITE_COLOR_BURN, + COMPOSITE_HARD_LIGHT, + COMPOSITE_SOFT_LIGHT, + COMPOSITE_DIFFERENCE, + COMPOSITE_EXCLUSION, + }; + + const QColor dst_color(Qt::blue); + + for (auto mode : modes) + { + INFO("mode=" << mode); + // Create a new clip each iteration to avoid cached images + openshot::Clip clip; + clip.Reader(&dummy); + clip.Open(); + clip.display = openshot::FRAME_DISPLAY_NONE; + clip.alpha.AddPoint(1, 1.0); + clip.composite = mode; + + // Build a fresh blue background for each mode + auto bg = std::make_shared(1, 16, 16, "#0000ff", 0, 2); + + auto out = clip.GetFrame(bg, 1); + auto img = out->GetImage(); + REQUIRE(img); + + QColor result = img->pixelColor(8, 8); + QColor expect = expected_color(QColor(Qt::red), dst_color, + static_cast(mode)); + + // Adjust expectations for modes with different behavior on solid colors + if (mode == COMPOSITE_SOURCE_IN || mode == COMPOSITE_DESTINATION_IN) + expect = QColor(0, 0, 0, 0); + else if (mode == COMPOSITE_DESTINATION_OUT || mode == COMPOSITE_SOURCE_ATOP) + expect = dst_color; + + CHECK(result.red() == expect.red()); + CHECK(result.green() == expect.green()); + CHECK(result.blue() == expect.blue()); + CHECK(result.alpha() == expect.alpha()); + } +} + TEST_CASE( "transform_path_identity_vs_scaled", "[libopenshot][clip][pr]" ) { // Create a small checker-ish image to make scaling detectable From fa4f44d1087009b0d3bdd109627b31687594293d Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 12 Sep 2025 21:19:16 -0500 Subject: [PATCH 31/41] Fixing small build error on benchmark includes --- tests/Benchmark.cpp | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/Benchmark.cpp b/tests/Benchmark.cpp index e9492cfe1..669542574 100644 --- a/tests/Benchmark.cpp +++ b/tests/Benchmark.cpp @@ -19,7 +19,11 @@ #include "FFmpegWriter.h" #include "Fraction.h" #include "FrameMapper.h" +#ifdef USE_IMAGEMAGICK #include "ImageReader.h" +#else +#include "QtImageReader.h" +#endif #include "ReaderBase.h" #include "Timeline.h" #include "effects/Brightness.h" @@ -161,20 +165,24 @@ int main() { t.Close(); }); - total += time_trial("Effect_Mask", [&]() { - FFmpegReader r(video); - r.Open(); - ImageReader mask_reader(mask_img); - mask_reader.Open(); - Clip clip(&r); - clip.Open(); - Mask m(&mask_reader, Keyframe(0.0), Keyframe(0.5)); - clip.AddEffect(&m); - read_forward_backward(clip); - mask_reader.Close(); - clip.Close(); - r.Close(); - }); + total += time_trial("Effect_Mask", [&]() { + FFmpegReader r(video); + r.Open(); +#ifdef USE_IMAGEMAGICK + ImageReader mask_reader(mask_img); +#else + QtImageReader mask_reader(mask_img); +#endif + mask_reader.Open(); + Clip clip(&r); + clip.Open(); + Mask m(&mask_reader, Keyframe(0.0), Keyframe(0.5)); + clip.AddEffect(&m); + read_forward_backward(clip); + mask_reader.Close(); + clip.Close(); + r.Close(); + }); total += time_trial("Effect_Brightness", [&]() { FFmpegReader r(video); From 021c6ecc07e7df78718fa1fc3435ff82fe9edf85 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 12 Sep 2025 22:57:26 -0500 Subject: [PATCH 32/41] Adding unit tests to validate FFmpegReader, Clip, and Timeline frame accuracy of GIF files, plus GIF with time curves. --- examples/animation.gif | Bin 0 -> 2353 bytes tests/Clip.cpp | 127 +++++++++++++++++++++++++++++++++++++++++ tests/FFmpegReader.cpp | 30 ++++++++++ 3 files changed, 157 insertions(+) create mode 100644 examples/animation.gif diff --git a/examples/animation.gif b/examples/animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..0b4ca00f50fe0536574797a5402f65fbeeae74f3 GIT binary patch literal 2353 zcmZ?wbhEHbG-5DfXkFEZ^r{;KWUUqi2Me(aUCpRxYKi{F5OV(@4ii_=xj39d$MHm}uyerx*tu!|?A$T{cJ8COoz?(|6k?eG literal 0 HcmV?d00001 diff --git a/tests/Clip.cpp b/tests/Clip.cpp index 51fdf215a..b2c759429 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -12,6 +12,7 @@ #include #include +#include #include "openshot_catch.h" @@ -226,6 +227,132 @@ TEST_CASE( "effects", "[libopenshot][clip]" ) CHECK((int)c10.Effects().size() == 2); } +TEST_CASE( "GIF_clip_properties", "[libopenshot][clip][gif]" ) +{ + std::stringstream path; + path << TEST_MEDIA_PATH << "animation.gif"; + Clip c(path.str()); + c.Open(); + + FFmpegReader *r = dynamic_cast(c.Reader()); + REQUIRE(r != nullptr); + CHECK(r->info.video_length == 20); + CHECK(r->info.fps.num == 5); + CHECK(r->info.fps.den == 1); + CHECK(r->info.duration == Approx(4.0f).margin(0.01)); + + c.Close(); +} + +TEST_CASE( "GIF_time_mapping", "[libopenshot][clip][gif]" ) +{ + std::stringstream path; + path << TEST_MEDIA_PATH << "animation.gif"; + + auto frame_color = [](std::shared_ptr f) { + const unsigned char* row = f->GetPixels(25); + return row[25 * 4]; + }; + auto expected_color = [](int frame) { + return (frame - 1) * 10; + }; + + // Slow mapping: stretch 20 frames over 50 frames + Clip slow(path.str()); + slow.time.AddPoint(1,1, LINEAR); + slow.time.AddPoint(50,20, LINEAR); + slow.Open(); + + std::set slow_colors; + for (int i = 1; i <= 50; ++i) { + int src = slow.time.GetLong(i); + int c = frame_color(slow.GetFrame(i)); + CHECK(c == expected_color(src)); + slow_colors.insert(c); + } + CHECK((int)slow_colors.size() == 20); + slow.Close(); + + // Fast mapping: shrink 20 frames to 10 frames + Clip fast(path.str()); + fast.time.AddPoint(1,1, LINEAR); + fast.time.AddPoint(10,20, LINEAR); + fast.Open(); + + std::set fast_colors; + for (int i = 1; i <= 10; ++i) { + int src = fast.time.GetLong(i); + int c = frame_color(fast.GetFrame(i)); + CHECK(c == expected_color(src)); + fast_colors.insert(c); + } + CHECK((int)fast_colors.size() == 10); + fast.Close(); +} + +TEST_CASE( "GIF_timeline_mapping", "[libopenshot][clip][gif]" ) +{ + // Create a timeline + Timeline t1(50, 50, Fraction(5, 1), 44100, 2, LAYOUT_STEREO); + + std::stringstream path; + path << TEST_MEDIA_PATH << "animation.gif"; + + auto frame_color = [](std::shared_ptr f) { + const unsigned char* row = f->GetPixels(25); + return row[25 * 4]; + }; + auto expected_color = [](int frame) { + return (frame - 1) * 10; + }; + + // Slow mapping: stretch 20 frames over 50 frames + Clip slow(path.str()); + slow.Position(0.0); + slow.Layer(1); + slow.time.AddPoint(1,1, LINEAR); + slow.time.AddPoint(50,20, LINEAR); + slow.End(10.0); + t1.AddClip(&slow); + t1.Open(); + + std::set slow_colors; + for (int i = 1; i <= 50; ++i) { + int src = slow.time.GetLong(i); + std::stringstream frame_save; + t1.GetFrame(i)->Save(frame_save.str(), 1.0, "PNG", 100); + int c = frame_color(t1.GetFrame(i)); + std::cout << c << std::endl; + CHECK(c == expected_color(src)); + slow_colors.insert(c); + } + CHECK((int)slow_colors.size() == 20); + t1.Close(); + + // Create a timeline + Timeline t2(50, 50, Fraction(5, 1), 44100, 2, LAYOUT_STEREO); + + // Fast mapping: shrink 20 frames to 10 frames + Clip fast(path.str()); + fast.Position(0.0); + fast.Layer(1); + fast.time.AddPoint(1,1, LINEAR); + fast.time.AddPoint(10,20, LINEAR); + fast.End(2.0); + t2.AddClip(&fast); + t2.Open(); + + std::set fast_colors; + for (int i = 1; i <= 10; ++i) { + int src = fast.time.GetLong(i); + int c = frame_color(t2.GetFrame(i)); + CHECK(c == expected_color(src)); + fast_colors.insert(c); + } + CHECK((int)fast_colors.size() == 10); + t2.Close(); +} + TEST_CASE( "verify parent Timeline", "[libopenshot][clip]" ) { Timeline t1(640, 480, Fraction(30,1), 44100, 2, LAYOUT_STEREO); diff --git a/tests/FFmpegReader.cpp b/tests/FFmpegReader.cpp index a578ab186..fbf1030e5 100644 --- a/tests/FFmpegReader.cpp +++ b/tests/FFmpegReader.cpp @@ -12,6 +12,7 @@ #include #include +#include #include "openshot_catch.h" @@ -189,6 +190,35 @@ TEST_CASE( "Frame_Rate", "[libopenshot][ffmpegreader]" ) r.Close(); } +TEST_CASE( "GIF_TimeBase", "[libopenshot][ffmpegreader]" ) +{ + // Create a reader + std::stringstream path; + path << TEST_MEDIA_PATH << "animation.gif"; + FFmpegReader r(path.str()); + r.Open(); + + // Verify basic info + CHECK(r.info.fps.num == 5); + CHECK(r.info.fps.den == 1); + CHECK(r.info.video_length == 20); + CHECK(r.info.duration == Approx(4.0f).margin(0.01)); + + auto frame_color = [](std::shared_ptr f) { + const unsigned char* row = f->GetPixels(25); + return row[25 * 4]; + }; + auto expected_color = [](int frame) { + return (frame - 1) * 10; + }; + + for (int i = 1; i <= r.info.video_length; ++i) { + CHECK(frame_color(r.GetFrame(i)) == expected_color(i)); + } + + r.Close(); +} + TEST_CASE( "Multiple_Open_and_Close", "[libopenshot][ffmpegreader]" ) { // Create a reader From 3723fbd99f1aa4c835cf19bb2af51fc5acf2b118 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 12 Sep 2025 23:00:22 -0500 Subject: [PATCH 33/41] Fixing regression on Mac and Windows builds for Clip blend modes (color tolerances) --- tests/Clip.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/Clip.cpp b/tests/Clip.cpp index b2c759429..5f06a523f 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "Clip.h" #include "DummyReader.h" @@ -796,10 +797,12 @@ TEST_CASE("all_composite_modes_simple_colors", "[libopenshot][clip][composite]") else if (mode == COMPOSITE_DESTINATION_OUT || mode == COMPOSITE_SOURCE_ATOP) expect = dst_color; - CHECK(result.red() == expect.red()); - CHECK(result.green() == expect.green()); - CHECK(result.blue() == expect.blue()); - CHECK(result.alpha() == expect.alpha()); + // Allow a small tolerance to account for platform-specific + // rounding differences in Qt's composition modes + CHECK(std::abs(result.red() - expect.red()) <= 1); + CHECK(std::abs(result.green() - expect.green()) <= 1); + CHECK(std::abs(result.blue() - expect.blue()) <= 1); + CHECK(std::abs(result.alpha() - expect.alpha()) <= 1); } } From 01a4d9f6efc99c1e84b190bc012cbd6230591082 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 15 Sep 2025 18:20:05 -0500 Subject: [PATCH 34/41] Fixing regression/bug in video cache thread - to correctly reset cached_frame_count, and make isReady() return correctly. The result of this bug was audio starting playback sooner than video - and some general jank around video caching. --- src/Qt/VideoCacheThread.cpp | 36 +++++++++++++++++++++++++++++++++--- src/Qt/VideoCacheThread.h | 2 +- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Qt/VideoCacheThread.cpp b/src/Qt/VideoCacheThread.cpp index a7336b07d..643ed0a64 100644 --- a/src/Qt/VideoCacheThread.cpp +++ b/src/Qt/VideoCacheThread.cpp @@ -48,6 +48,14 @@ namespace openshot // Is cache ready for playback (pre-roll) bool VideoCacheThread::isReady() { + if (!reader) { + return false; + } + + if (min_frames_ahead < 0) { + return true; + } + return (cached_frame_count > min_frames_ahead); } @@ -96,11 +104,18 @@ namespace openshot if (start_preroll) { userSeeked = true; - if (!reader->GetCache()->Contains(new_position)) + CacheBase* cache = reader ? reader->GetCache() : nullptr; + + if (cache && !cache->Contains(new_position)) { // If user initiated seek, and current frame not found ( Timeline* timeline = static_cast(reader); timeline->ClearAllCache(); + cached_frame_count = 0; + } + else if (cache) + { + cached_frame_count = cache->Count(); } } requested_display_frame = new_position; @@ -131,6 +146,7 @@ namespace openshot // If paused and playhead not in cache, clear everything Timeline* timeline = static_cast(reader); timeline->ClearAllCache(); + cached_frame_count = 0; return true; } return false; @@ -184,7 +200,7 @@ namespace openshot try { auto framePtr = reader->GetFrame(next_frame); cache->Add(framePtr); - ++cached_frame_count; + cached_frame_count = cache->Count(); } catch (const OutOfBoundsFrame&) { break; @@ -211,8 +227,10 @@ namespace openshot Settings* settings = Settings::Instance(); CacheBase* cache = reader ? reader->GetCache() : nullptr; - // If caching disabled or no reader, sleep briefly + // If caching disabled or no reader, mark cache as ready and sleep briefly if (!settings->ENABLE_PLAYBACK_CACHING || !cache) { + cached_frame_count = (cache ? cache->Count() : 0); + min_frames_ahead = -1; std::this_thread::sleep_for(double_micro_sec(50000)); continue; } @@ -225,6 +243,8 @@ namespace openshot int64_t playhead = requested_display_frame; bool paused = (speed == 0); + cached_frame_count = cache->Count(); + // Compute effective direction (±1) int dir = computeDirection(); if (speed != 0) { @@ -282,6 +302,16 @@ namespace openshot } int64_t ahead_count = static_cast(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD); + int64_t window_size = ahead_count + 1; + if (window_size < 1) { + window_size = 1; + } + int64_t ready_target = window_size - 1; + if (ready_target < 0) { + ready_target = 0; + } + int64_t configured_min = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES; + min_frames_ahead = std::min(configured_min, ready_target); // If paused and playhead is no longer in cache, clear everything bool did_clear = clearCacheIfPaused(playhead, paused, cache); diff --git a/src/Qt/VideoCacheThread.h b/src/Qt/VideoCacheThread.h index 1fd6decd5..72f502e04 100644 --- a/src/Qt/VideoCacheThread.h +++ b/src/Qt/VideoCacheThread.h @@ -166,7 +166,7 @@ namespace openshot int64_t requested_display_frame; ///< Frame index the user requested. int64_t current_display_frame; ///< Currently displayed frame (unused here, reserved). - int64_t cached_frame_count; ///< Count of frames currently added to cache. + int64_t cached_frame_count; ///< Estimated count of frames currently stored in cache. int64_t min_frames_ahead; ///< Minimum number of frames considered “ready” (pre-roll). int64_t timeline_max_frame; ///< Highest valid frame index in the timeline. From c165eca5d8c854a05f9cef7dbd7f39e29b9995df Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 22 Sep 2025 12:28:12 -0500 Subject: [PATCH 35/41] Improving AudioWaveformer to be able to correctly generate waveforms for time-curved clips that have a modified duration/video_length (i.e. repeated clips, slowled down clips, etc...). Adding a new ReaderBase.h VideoLength() that can be overridden in Clip.cpp when time curves are involved. --- src/AudioWaveformer.cpp | 248 +++++++++++-------- src/Clip.cpp | 162 +++++++++++++ src/Clip.h | 13 + src/ReaderBase.h | 5 + tests/AudioWaveformer.cpp | 490 ++++++++++++++++++++++++++------------ 5 files changed, 664 insertions(+), 254 deletions(-) diff --git a/src/AudioWaveformer.cpp b/src/AudioWaveformer.cpp index 18958319b..6b3866c0a 100644 --- a/src/AudioWaveformer.cpp +++ b/src/AudioWaveformer.cpp @@ -12,6 +12,11 @@ #include "AudioWaveformer.h" +#include + +#include +#include + using namespace std; using namespace openshot; @@ -31,104 +36,147 @@ AudioWaveformer::~AudioWaveformer() // Extract audio samples from any ReaderBase class AudioWaveformData AudioWaveformer::ExtractSamples(int channel, int num_per_second, bool normalize) { - AudioWaveformData data; - - if (reader) { - // Open reader (if needed) - bool does_reader_have_video = reader->info.has_video; - if (!reader->IsOpen()) { - reader->Open(); - } - // Disable video for faster processing - reader->info.has_video = false; - - int sample_rate = reader->info.sample_rate; - int sample_divisor = sample_rate / num_per_second; - int total_samples = num_per_second * (reader->info.duration + 1.0); - int extracted_index = 0; - - // Force output to zero elements for non-audio readers - if (!reader->info.has_audio) { - total_samples = 0; - } - - // Resize and clear audio buffers - data.resize(total_samples); - data.zero(total_samples); - - // Bail out, if no samples needed - if (total_samples == 0 || reader->info.channels == 0) { - return data; - } - - // Loop through all frames - int sample_index = 0; - float samples_max = 0.0; - float chunk_max = 0.0; - float chunk_squared_sum = 0.0; - - // How many channels are we using - int channel_count = 1; - if (channel == -1) { - channel_count = reader->info.channels; - } - - for (auto f = 1; f <= reader->info.video_length; f++) { - // Get next frame - shared_ptr frame = reader->GetFrame(f); - - // Cache channels for this frame, to reduce # of calls to frame->GetAudioSamples - float* channels[channel_count]; - for (auto channel_index = 0; channel_index < reader->info.channels; channel_index++) { - if (channel == channel_index || channel == -1) { - channels[channel_index] = frame->GetAudioSamples(channel_index); - } - } - - // Get sample value from a specific channel (or all channels) - for (auto s = 0; s < frame->GetAudioSamplesCount(); s++) { - for (auto channel_index = 0; channel_index < reader->info.channels; channel_index++) { - if (channel == channel_index || channel == -1) { - float *samples = channels[channel_index]; - float rms_sample_value = std::sqrt(samples[s] * samples[s]); - - // Accumulate sample averages - chunk_squared_sum += rms_sample_value; - chunk_max = std::max(chunk_max, rms_sample_value); - } - } - - sample_index += 1; - - // Cut-off reached - if (sample_index % sample_divisor == 0) { - float avg_squared_sum = chunk_squared_sum / (sample_divisor * channel_count); - data.max_samples[extracted_index] = chunk_max; - data.rms_samples[extracted_index] = avg_squared_sum; - extracted_index++; - - // Track max/min values - samples_max = std::max(samples_max, chunk_max); - - // reset sample total and index - sample_index = 0; - chunk_max = 0.0; - chunk_squared_sum = 0.0; - } - } - } - - // Scale all values to the -1 to +1 range (regardless of how small or how large the - // original audio sample values are) - if (normalize && samples_max > 0.0) { - float scale = 1.0f / samples_max; - data.scale(total_samples, scale); - } - - // Resume previous has_video value - reader->info.has_video = does_reader_have_video; - } - - - return data; + AudioWaveformData data; + + if (!reader || num_per_second <= 0) { + return data; + } + + // Open reader (if needed) + bool does_reader_have_video = reader->info.has_video; + if (!reader->IsOpen()) { + reader->Open(); + } + // Disable video for faster processing + reader->info.has_video = false; + + int sample_rate = reader->info.sample_rate; + if (sample_rate <= 0) { + sample_rate = num_per_second; + } + int sample_divisor = sample_rate / num_per_second; + if (sample_divisor <= 0) { + sample_divisor = 1; + } + + int64_t reader_video_length = reader->VideoLength(); + if (reader_video_length < 0) { + reader_video_length = 0; + } + float reader_duration = reader->info.duration; + double fps_value = reader->info.fps.ToDouble(); + float frames_duration = 0.0f; + if (reader_video_length > 0 && fps_value > 0.0) { + frames_duration = static_cast(reader_video_length / fps_value); + } + const bool has_source_length = reader->info.video_length > 0; + const bool frames_extended = has_source_length && reader_video_length > reader->info.video_length; + if (reader_duration <= 0.0f) { + reader_duration = frames_duration; + } else if ((frames_extended || !has_source_length) && frames_duration > reader_duration + 1e-4f) { + reader_duration = frames_duration; + } + if (reader_duration < 0.0f) { + reader_duration = 0.0f; + } + + if (!reader->info.has_audio) { + reader->info.has_video = does_reader_have_video; + return data; + } + + int total_samples = static_cast(std::ceil(reader_duration * num_per_second)); + if (total_samples <= 0 || reader->info.channels == 0) { + reader->info.has_video = does_reader_have_video; + return data; + } + + if (channel != -1 && (channel < 0 || channel >= reader->info.channels)) { + reader->info.has_video = does_reader_have_video; + return data; + } + + // Resize and clear audio buffers + data.resize(total_samples); + data.zero(total_samples); + + int extracted_index = 0; + int sample_index = 0; + float samples_max = 0.0f; + float chunk_max = 0.0f; + float chunk_squared_sum = 0.0f; + + int channel_count = (channel == -1) ? reader->info.channels : 1; + std::vector channels(reader->info.channels, nullptr); + + for (int64_t f = 1; f <= reader_video_length && extracted_index < total_samples; f++) { + std::shared_ptr frame = reader->GetFrame(f); + + for (int channel_index = 0; channel_index < reader->info.channels; channel_index++) { + if (channel == channel_index || channel == -1) { + channels[channel_index] = frame->GetAudioSamples(channel_index); + } + } + + for (int s = 0; s < frame->GetAudioSamplesCount(); s++) { + for (int channel_index = 0; channel_index < reader->info.channels; channel_index++) { + if (channel == channel_index || channel == -1) { + float *samples = channels[channel_index]; + if (!samples) { + continue; + } + float rms_sample_value = std::sqrt(samples[s] * samples[s]); + + chunk_squared_sum += rms_sample_value; + chunk_max = std::max(chunk_max, rms_sample_value); + } + } + + sample_index += 1; + + if (sample_index % sample_divisor == 0) { + float avg_squared_sum = 0.0f; + if (channel_count > 0) { + avg_squared_sum = chunk_squared_sum / static_cast(sample_divisor * channel_count); + } + + if (extracted_index < total_samples) { + data.max_samples[extracted_index] = chunk_max; + data.rms_samples[extracted_index] = avg_squared_sum; + samples_max = std::max(samples_max, chunk_max); + extracted_index++; + } + + sample_index = 0; + chunk_max = 0.0f; + chunk_squared_sum = 0.0f; + + if (extracted_index >= total_samples) { + break; + } + } + } + } + + if (sample_index > 0 && extracted_index < total_samples) { + float avg_squared_sum = 0.0f; + if (channel_count > 0) { + avg_squared_sum = chunk_squared_sum / static_cast(sample_index * channel_count); + } + + data.max_samples[extracted_index] = chunk_max; + data.rms_samples[extracted_index] = avg_squared_sum; + samples_max = std::max(samples_max, chunk_max); + extracted_index++; + } + + if (normalize && samples_max > 0.0f) { + float scale = 1.0f / samples_max; + data.scale(total_samples, scale); + } + + reader->info.has_video = does_reader_have_video; + + return data; } + diff --git a/src/Clip.cpp b/src/Clip.cpp index 9c3e62c61..25c3bdc3a 100644 --- a/src/Clip.cpp +++ b/src/Clip.cpp @@ -22,6 +22,9 @@ #include "Timeline.h" #include "ZmqLogger.h" +#include +#include + #ifdef USE_IMAGEMAGICK #include "MagickUtilities.h" #include "ImageReader.h" @@ -360,6 +363,165 @@ ReaderBase* Clip::Reader() throw ReaderClosed("No Reader has been initialized for this Clip. Call Reader(*reader) before calling this method."); } +double Clip::resolve_timeline_fps() const +{ + if (timeline) { + const Timeline* attached_timeline = dynamic_cast(timeline); + if (attached_timeline) { + double timeline_fps = attached_timeline->info.fps.ToDouble(); + if (timeline_fps > 0.0) { + return timeline_fps; + } + } + } + + double clip_fps = info.fps.ToDouble(); + if (clip_fps > 0.0) { + return clip_fps; + } + + if (reader) { + double reader_fps = reader->info.fps.ToDouble(); + if (reader_fps > 0.0) { + return reader_fps; + } + } + + return 0.0; +} + +int64_t Clip::curve_extent_frames() const +{ + if (time.GetCount() <= 1) { + return 0; + } + + double max_timeline_frame = 0.0; + for (int64_t index = 0; index < time.GetCount(); ++index) { + const Point& curve_point = time.GetPoint(index); + if (curve_point.co.X > max_timeline_frame) { + max_timeline_frame = curve_point.co.X; + } + } + + if (max_timeline_frame <= 0.0) { + return 0; + } + + return static_cast(std::llround(max_timeline_frame)); +} + +int64_t Clip::trim_extent_frames(double fps_value) const +{ + if (fps_value <= 0.0) { + return 0; + } + + const double epsilon = 1e-6; + const double trimmed_start_seconds = static_cast(ClipBase::Start()); + const double trimmed_end_seconds = static_cast(ClipBase::End()); + + bool has_left_trim = trimmed_start_seconds > epsilon; + double original_duration = static_cast(info.duration); + if (original_duration <= 0.0 && reader) { + original_duration = static_cast(reader->info.duration); + } + if (original_duration <= 0.0) { + double source_fps = info.fps.ToDouble(); + int64_t source_length = info.video_length; + if ((source_length <= 0 || source_fps <= 0.0) && reader) { + source_length = reader->VideoLength(); + source_fps = reader->info.fps.ToDouble(); + } + if (source_length > 0 && source_fps > 0.0) { + original_duration = static_cast(source_length) / source_fps; + } + } + bool has_right_trim = false; + if (original_duration > 0.0) { + has_right_trim = std::fabs(trimmed_end_seconds - original_duration) > epsilon; + } + + if (!has_left_trim && !has_right_trim) { + int64_t base_frames = info.video_length; + if (base_frames <= 0 && reader) { + base_frames = reader->VideoLength(); + } + if (base_frames > 0) { + return base_frames; + } + } + + if (trimmed_end_seconds <= trimmed_start_seconds) { + return 0; + } + + const int64_t start_frame = static_cast(std::llround(trimmed_start_seconds * fps_value)); + const int64_t end_frame = static_cast(std::llround(trimmed_end_seconds * fps_value)); + const int64_t trimmed_frames = end_frame - start_frame; + return trimmed_frames > 0 ? trimmed_frames : 0; +} +// Determine clip video length (frame count), accounting for time-mapping curves when present. +int64_t Clip::VideoLength() const +{ + double fps_value = resolve_timeline_fps(); + int64_t trim_frames = trim_extent_frames(fps_value); + int64_t curve_frames = curve_extent_frames(); + + int64_t timeline_frames = std::max(trim_frames, curve_frames); + if (timeline_frames > 0) { + return timeline_frames; + } + + if (info.video_length > 0) { + return info.video_length; + } + + if (reader) { + return reader->VideoLength(); + } + + return 0; +} + +float Clip::MaxDuration() const +{ + double fps_value = resolve_timeline_fps(); + int64_t curve_frames = curve_extent_frames(); + + if (fps_value > 0.0 && curve_frames > 0) { + return static_cast(static_cast(curve_frames) / fps_value); + } + + float fallback_duration = ClipBase::Duration(); + if (fallback_duration > 0.0f) { + return fallback_duration; + } + + if (info.duration > 0.0f) { + return info.duration; + } + + double info_fps = info.fps.ToDouble(); + if (info.video_length > 0 && info_fps > 0.0) { + return static_cast(static_cast(info.video_length) / info_fps); + } + + if (reader) { + float reader_duration = reader->info.duration; + if (reader_duration > 0.0f) { + return reader_duration; + } + + double reader_fps = reader->info.fps.ToDouble(); + if (reader->info.video_length > 0 && reader_fps > 0.0) { + return static_cast(static_cast(reader->info.video_length) / reader_fps); + } + } + + return 0.0f; +} + // Open the internal reader void Clip::Open() { diff --git a/src/Clip.h b/src/Clip.h index 98da54014..486265dde 100644 --- a/src/Clip.h +++ b/src/Clip.h @@ -151,6 +151,15 @@ namespace openshot { /// Get a frame object or create a blank one std::shared_ptr GetOrCreateFrame(int64_t number, bool enable_time=true); + /// Determine the frames-per-second context used for timeline playback + double resolve_timeline_fps() const; + + /// Determine the number of frames implied by time-mapping curves + int64_t curve_extent_frames() const; + + /// Determine the number of frames implied by the clip's trim range + int64_t trim_extent_frames(double fps_value) const; + /// Adjust the audio and image of a time mapped frame void apply_timemapping(std::shared_ptr frame); @@ -279,6 +288,10 @@ namespace openshot { /// Get the current reader openshot::ReaderBase* Reader(); + /// Duration and video length helpers which take into account time-mapping curves + float MaxDuration() const; + int64_t VideoLength() const override; + // Override End() position (in seconds) of clip (trim end of video) float End() const override; ///< Get end position (in seconds) of clip (trim end of video), which can be affected by the time curve. void End(float value) override; ///< Set end position (in seconds) of clip (trim end of video) diff --git a/src/ReaderBase.h b/src/ReaderBase.h index aca12ff2c..43dcb85f8 100644 --- a/src/ReaderBase.h +++ b/src/ReaderBase.h @@ -125,6 +125,11 @@ namespace openshot /// Open the reader (and start consuming resources, such as images or video files) virtual void Open() = 0; + /// Get the detected number of frames in this reader. + /// Derived readers can override this to provide custom logic + /// for dynamic or procedurally generated frame counts. + virtual int64_t VideoLength() const { return info.video_length; } + virtual ~ReaderBase() = default; }; diff --git a/tests/AudioWaveformer.cpp b/tests/AudioWaveformer.cpp index c8d856836..7707c91c4 100644 --- a/tests/AudioWaveformer.cpp +++ b/tests/AudioWaveformer.cpp @@ -12,193 +12,375 @@ #include "openshot_catch.h" #include "AudioWaveformer.h" +#include "Clip.h" #include "FFmpegReader.h" +#include "Timeline.h" + +#include +#include using namespace openshot; TEST_CASE( "Extract waveform data piano.wav", "[libopenshot][audiowaveformer]" ) { - // Create a reader - std::stringstream path; - path << TEST_MEDIA_PATH << "piano.wav"; - FFmpegReader r(path.str()); - r.Open(); - - // Create AudioWaveformer and extract a smaller "average" sample set of audio data - AudioWaveformer waveformer(&r); - for (auto channel = 0; channel < r.info.channels; channel++) { - AudioWaveformData waveform = waveformer.ExtractSamples(channel, 20, false); - - if (channel == 0) { - CHECK(waveform.rms_samples.size() == 107); - CHECK(waveform.rms_samples[0] == Approx(0.04879f).margin(0.00001)); - CHECK(waveform.rms_samples[86] == Approx(0.13578f).margin(0.00001)); - CHECK(waveform.rms_samples[87] == Approx(0.0f).margin(0.00001)); - } else if (channel == 1) { - CHECK(waveform.rms_samples.size() == 107); - CHECK(waveform.rms_samples[0] == Approx(0.04879f).margin(0.00001)); - CHECK(waveform.rms_samples[86] == Approx(0.13578f).margin(0.00001)); - CHECK(waveform.rms_samples[87] == Approx(0.0f).margin(0.00001)); - } - - waveform.clear(); - } - - // Clean up - r.Close(); + // Create a reader + std::stringstream path; + path << TEST_MEDIA_PATH << "piano.wav"; + FFmpegReader r(path.str()); + r.Open(); + + // Create AudioWaveformer and extract a smaller "average" sample set of audio data + const int samples_per_second = 20; + const int expected_total = static_cast(std::ceil(r.info.duration * samples_per_second)); + REQUIRE(expected_total > 1); + + AudioWaveformer waveformer(&r); + for (auto channel = 0; channel < r.info.channels; channel++) { + AudioWaveformData waveform = waveformer.ExtractSamples(channel, samples_per_second, false); + + CHECK(waveform.rms_samples.size() == expected_total); + CHECK(waveform.rms_samples[0] == Approx(0.04879f).margin(0.00001)); + CHECK(waveform.rms_samples[expected_total - 2] == Approx(0.13578f).margin(0.00001)); + CHECK(waveform.rms_samples.back() == Approx(0.11945f).margin(0.00001)); + + waveform.clear(); + } + + // Clean up + r.Close(); } TEST_CASE( "Extract waveform data sintel", "[libopenshot][audiowaveformer]" ) { - // Create a reader - std::stringstream path; - path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4"; - FFmpegReader r(path.str()); - - // Create AudioWaveformer and extract a smaller "average" sample set of audio data - AudioWaveformer waveformer(&r); - for (auto channel = 0; channel < r.info.channels; channel++) { - AudioWaveformData waveform = waveformer.ExtractSamples(channel, 20, false); - - if (channel == 0) { - CHECK(waveform.rms_samples.size() == 1058); - CHECK(waveform.rms_samples[0] == Approx(0.00001f).margin(0.00001)); - CHECK(waveform.rms_samples[1037] == Approx(0.00003f).margin(0.00001)); - CHECK(waveform.rms_samples[1038] == Approx(0.0f).margin(0.00001)); - } else if (channel == 1) { - CHECK(waveform.rms_samples.size() == 1058); - CHECK(waveform.rms_samples[0] == Approx(0.00001f ).margin(0.00001)); - CHECK(waveform.rms_samples[1037] == Approx(0.00003f).margin(0.00001)); - CHECK(waveform.rms_samples[1038] == Approx(0.0f).margin(0.00001)); - } - - waveform.clear(); - } - - // Clean up - r.Close(); + // Create a reader + std::stringstream path; + path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4"; + FFmpegReader r(path.str()); + + // Create AudioWaveformer and extract a smaller "average" sample set of audio data + const int samples_per_second = 20; + const int expected_total = static_cast(std::ceil(r.info.duration * samples_per_second)); + REQUIRE(expected_total > 1); + + AudioWaveformer waveformer(&r); + for (auto channel = 0; channel < r.info.channels; channel++) { + AudioWaveformData waveform = waveformer.ExtractSamples(channel, samples_per_second, false); + + CHECK(waveform.rms_samples.size() == expected_total); + CHECK(waveform.rms_samples[0] == Approx(0.00001f).margin(0.00001)); + CHECK(waveform.rms_samples[expected_total - 2] == Approx(0.00003f).margin(0.00001)); + CHECK(waveform.rms_samples.back() == Approx(0.00002f).margin(0.00002)); + + waveform.clear(); + } + + // Clean up + r.Close(); } TEST_CASE( "Extract waveform data sintel (all channels)", "[libopenshot][audiowaveformer]" ) { - // Create a reader - std::stringstream path; - path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4"; - FFmpegReader r(path.str()); + // Create a reader + std::stringstream path; + path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4"; + FFmpegReader r(path.str()); + + // Create AudioWaveformer and extract a smaller "average" sample set of audio data + const int samples_per_second = 20; + const int expected_total = static_cast(std::ceil(r.info.duration * samples_per_second)); + REQUIRE(expected_total > 1); - // Create AudioWaveformer and extract a smaller "average" sample set of audio data - AudioWaveformer waveformer(&r); - AudioWaveformData waveform = waveformer.ExtractSamples(-1, 20, false); + AudioWaveformer waveformer(&r); + AudioWaveformData waveform = waveformer.ExtractSamples(-1, samples_per_second, false); - CHECK(waveform.rms_samples.size() == 1058); - CHECK(waveform.rms_samples[0] == Approx(0.00001f).margin(0.00001)); - CHECK(waveform.rms_samples[1037] == Approx(0.00003f).margin(0.00001)); - CHECK(waveform.rms_samples[1038] == Approx(0.0f).margin(0.00001)); + CHECK(waveform.rms_samples.size() == expected_total); + CHECK(waveform.rms_samples[0] == Approx(0.00001f).margin(0.00001)); + CHECK(waveform.rms_samples[expected_total - 2] == Approx(0.00003f).margin(0.00001)); + CHECK(waveform.rms_samples.back() == Approx(0.00002f).margin(0.00002)); - waveform.clear(); + waveform.clear(); - // Clean up - r.Close(); + // Clean up + r.Close(); } TEST_CASE( "Normalize & scale waveform data piano.wav", "[libopenshot][audiowaveformer]" ) { - // Create a reader - std::stringstream path; - path << TEST_MEDIA_PATH << "piano.wav"; - FFmpegReader r(path.str()); - - // Create AudioWaveformer and extract a smaller "average" sample set of audio data - AudioWaveformer waveformer(&r); - for (auto channel = 0; channel < r.info.channels; channel++) { - // Normalize values and scale them between -1 and +1 - AudioWaveformData waveform = waveformer.ExtractSamples(channel, 20, true); - - if (channel == 0) { - CHECK(waveform.rms_samples.size() == 107); - CHECK(waveform.rms_samples[0] == Approx(0.07524f).margin(0.00001)); - CHECK(waveform.rms_samples[35] == Approx(0.20063f).margin(0.00001)); - CHECK(waveform.rms_samples[86] == Approx(0.2094f).margin(0.00001)); - CHECK(waveform.rms_samples[87] == Approx(0.0f).margin(0.00001)); - } - - waveform.clear(); - } - - // Clean up - r.Close(); + // Create a reader + std::stringstream path; + path << TEST_MEDIA_PATH << "piano.wav"; + FFmpegReader r(path.str()); + + // Create AudioWaveformer and extract a smaller "average" sample set of audio data + const int samples_per_second = 20; + const int expected_total = static_cast(std::ceil(r.info.duration * samples_per_second)); + REQUIRE(expected_total > 1); + + AudioWaveformer waveformer(&r); + for (auto channel = 0; channel < r.info.channels; channel++) { + // Normalize values and scale them between -1 and +1 + AudioWaveformData waveform = waveformer.ExtractSamples(channel, samples_per_second, true); + + CHECK(waveform.rms_samples.size() == expected_total); + CHECK(waveform.rms_samples[0] == Approx(0.07524f).margin(0.00001)); + CHECK(waveform.rms_samples.back() == Approx(0.18422f).margin(0.00001)); + CHECK(*std::max_element(waveform.max_samples.begin(), waveform.max_samples.end()) == Approx(1.0f).margin(0.00001)); + + waveform.clear(); + } + + // Clean up + r.Close(); +} + +TEST_CASE( "Extract waveform data clip slowed by time curve", "[libopenshot][audiowaveformer][clip][time]" ) +{ + std::stringstream path; + path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4"; + + FFmpegReader reader(path.str()); + Clip clip(&reader); + clip.Open(); + + const int64_t original_video_length = clip.Reader()->info.video_length; + const double fps_value = clip.Reader()->info.fps.ToDouble(); + REQUIRE(original_video_length > 0); + REQUIRE(fps_value > 0.0); + + clip.time = Keyframe(); + clip.time.AddPoint(1.0, 1.0, LINEAR); + clip.time.AddPoint(static_cast(original_video_length) * 2.0, + static_cast(original_video_length), LINEAR); + + AudioWaveformer waveformer(&clip); + const int samples_per_second = 20; + AudioWaveformData waveform = waveformer.ExtractSamples(-1, samples_per_second, false); + + const double expected_duration = (static_cast(original_video_length) * 2.0) / fps_value; + const int expected_total = static_cast(std::ceil(expected_duration * samples_per_second)); + CHECK(waveform.rms_samples.size() == expected_total); + CHECK(clip.VideoLength() == original_video_length * 2); + CHECK(clip.VideoLength() == static_cast(std::llround(expected_duration * fps_value))); + CHECK(clip.MaxDuration() == Approx(expected_duration).margin(0.0001)); + + clip.Close(); + reader.Close(); +} + +TEST_CASE( "Extract waveform data clip reversed by time curve", "[libopenshot][audiowaveformer][clip][time]" ) +{ + std::stringstream path; + path << TEST_MEDIA_PATH << "piano.wav"; + + FFmpegReader reader(path.str()); + Clip clip(&reader); + clip.Open(); + + const int samples_per_second = 20; + const int base_total = static_cast(std::ceil(clip.Reader()->info.duration * samples_per_second)); + const int64_t original_video_length = clip.Reader()->info.video_length; + const double fps_value = clip.Reader()->info.fps.ToDouble(); + REQUIRE(original_video_length > 0); + REQUIRE(fps_value > 0.0); + + clip.time = Keyframe(); + clip.time.AddPoint(1.0, static_cast(original_video_length), LINEAR); + clip.time.AddPoint(static_cast(original_video_length), 1.0, LINEAR); + + AudioWaveformer waveformer(&clip); + AudioWaveformData waveform = waveformer.ExtractSamples(-1, samples_per_second, false); + + const double expected_duration = static_cast(original_video_length) / fps_value; + const int expected_total = static_cast(std::ceil(expected_duration * samples_per_second)); + CHECK(waveform.rms_samples.size() == expected_total); + CHECK(expected_total == base_total); + CHECK(clip.VideoLength() == original_video_length); + CHECK(clip.VideoLength() == static_cast(std::llround(expected_duration * fps_value))); + CHECK(clip.MaxDuration() == Approx(expected_duration).margin(0.0001)); + + clip.Close(); + reader.Close(); +} + +TEST_CASE( "Extract waveform data clip reversed and slowed", "[libopenshot][audiowaveformer][clip][time]" ) +{ + std::stringstream path; + path << TEST_MEDIA_PATH << "piano.wav"; + + FFmpegReader reader(path.str()); + Clip clip(&reader); + clip.Open(); + + const int samples_per_second = 20; + const int base_total = static_cast(std::ceil(clip.Reader()->info.duration * samples_per_second)); + const int64_t original_video_length = clip.Reader()->info.video_length; + const double fps_value = clip.Reader()->info.fps.ToDouble(); + REQUIRE(original_video_length > 0); + REQUIRE(fps_value > 0.0); + + clip.time = Keyframe(); + clip.time.AddPoint(1.0, static_cast(original_video_length), LINEAR); + clip.time.AddPoint(static_cast(original_video_length) * 2.0, 1.0, LINEAR); + + AudioWaveformer waveformer(&clip); + AudioWaveformData waveform = waveformer.ExtractSamples(-1, samples_per_second, false); + + const double expected_duration = (static_cast(original_video_length) * 2.0) / fps_value; + const int expected_total = static_cast(std::ceil(expected_duration * samples_per_second)); + CHECK(waveform.rms_samples.size() == expected_total); + CHECK(expected_total > base_total); + CHECK(clip.VideoLength() == original_video_length * 2); + CHECK(clip.VideoLength() == static_cast(std::llround(expected_duration * fps_value))); + CHECK(clip.MaxDuration() == Approx(expected_duration).margin(0.0001)); + + clip.Close(); + reader.Close(); +} + +TEST_CASE( "Clip duration uses parent timeline FPS when time-mapped", "[libopenshot][audiowaveformer][clip][time][timeline]" ) +{ + std::stringstream path; + path << TEST_MEDIA_PATH << "piano.wav"; + + FFmpegReader reader(path.str()); + Clip clip(&reader); + clip.Open(); + + const int64_t original_video_length = clip.Reader()->info.video_length; + const double reader_fps = clip.Reader()->info.fps.ToDouble(); + REQUIRE(original_video_length > 0); + REQUIRE(reader_fps > 0.0); + + Timeline timeline( + 640, + 480, + Fraction(60, 1), + clip.Reader()->info.sample_rate, + clip.Reader()->info.channels, + clip.Reader()->info.channel_layout); + + clip.ParentTimeline(&timeline); + + clip.time = Keyframe(); + clip.time.AddPoint(1.0, 1.0, LINEAR); + clip.time.AddPoint(static_cast(original_video_length) * 2.0, + static_cast(original_video_length), LINEAR); + + const double timeline_fps = timeline.info.fps.ToDouble(); + REQUIRE(timeline_fps > 0.0); + + const double expected_duration = (static_cast(original_video_length) * 2.0) / timeline_fps; + CHECK(clip.MaxDuration() == Approx(expected_duration).margin(0.0001)); + CHECK(clip.VideoLength() == static_cast(std::llround(expected_duration * timeline_fps))); + + clip.Close(); + reader.Close(); +} +TEST_CASE( "Image clip VideoLength matches trim on timeline", "[libopenshot][clip][timeline]" ) +{ + std::stringstream path; + path << TEST_MEDIA_PATH << "front.png"; + + FFmpegReader reader(path.str()); + Clip clip(&reader); + clip.Open(); + + Timeline timeline( + 640, + 480, + Fraction(30, 1), + 44100, + 2, + LAYOUT_STEREO); + + clip.ParentTimeline(&timeline); + clip.End(5.0f); + + const double timeline_fps = timeline.info.fps.ToDouble(); + REQUIRE(timeline_fps > 0.0); + + const float clip_end = clip.End(); + const int64_t expected_length = static_cast(std::llround(static_cast(clip_end) * timeline_fps)); + + REQUIRE(clip.Reader()->info.video_length > 0); + REQUIRE(expected_length > clip.Reader()->info.video_length); + CHECK(clip.VideoLength() == expected_length); + + clip.Close(); + reader.Close(); } + TEST_CASE( "Extract waveform from image (no audio)", "[libopenshot][audiowaveformer]" ) { - // Create a reader - std::stringstream path; - path << TEST_MEDIA_PATH << "front.png"; - FFmpegReader r(path.str()); + // Create a reader + std::stringstream path; + path << TEST_MEDIA_PATH << "front.png"; + FFmpegReader r(path.str()); - // Create AudioWaveformer and extract a smaller "average" sample set of audio data - AudioWaveformer waveformer(&r); - AudioWaveformData waveform = waveformer.ExtractSamples(-1, 20, false); + // Create AudioWaveformer and extract a smaller "average" sample set of audio data + AudioWaveformer waveformer(&r); + AudioWaveformData waveform = waveformer.ExtractSamples(-1, 20, false); - CHECK(waveform.rms_samples.size() == 0); - CHECK(waveform.max_samples.size() == 0); + CHECK(waveform.rms_samples.size() == 0); + CHECK(waveform.max_samples.size() == 0); - // Clean up - r.Close(); + // Clean up + r.Close(); } TEST_CASE( "AudioWaveformData struct methods", "[libopenshot][audiowaveformer]" ) { - // Create a reader - AudioWaveformData waveform; - - // Resize data to 10 elements - waveform.resize(10); - CHECK(waveform.rms_samples.size() == 10); - CHECK(waveform.max_samples.size() == 10); - - // Set all values = 1.0 - for (auto s = 0; s < waveform.rms_samples.size(); s++) { - waveform.rms_samples[s] = 1.0; - waveform.max_samples[s] = 1.0; - } - CHECK(waveform.rms_samples[0] == Approx(1.0f).margin(0.00001)); - CHECK(waveform.rms_samples[9] == Approx(1.0f).margin(0.00001)); - CHECK(waveform.max_samples[0] == Approx(1.0f).margin(0.00001)); - CHECK(waveform.max_samples[9] == Approx(1.0f).margin(0.00001)); - - // Scale all values by 2 - waveform.scale(10, 2.0); - CHECK(waveform.rms_samples.size() == 10); - CHECK(waveform.max_samples.size() == 10); - CHECK(waveform.rms_samples[0] == Approx(2.0f).margin(0.00001)); - CHECK(waveform.rms_samples[9] == Approx(2.0f).margin(0.00001)); - CHECK(waveform.max_samples[0] == Approx(2.0f).margin(0.00001)); - CHECK(waveform.max_samples[9] == Approx(2.0f).margin(0.00001)); - - // Zero out all values - waveform.zero(10); - CHECK(waveform.rms_samples.size() == 10); - CHECK(waveform.max_samples.size() == 10); - CHECK(waveform.rms_samples[0] == Approx(0.0f).margin(0.00001)); - CHECK(waveform.rms_samples[9] == Approx(0.0f).margin(0.00001)); - CHECK(waveform.max_samples[0] == Approx(0.0f).margin(0.00001)); - CHECK(waveform.max_samples[9] == Approx(0.0f).margin(0.00001)); - - // Access vectors and verify size - std::vector> vectors = waveform.vectors(); - CHECK(vectors.size() == 2); - CHECK(vectors[0].size() == 10); - CHECK(vectors[0].size() == 10); - - // Clear and verify internal data is empty - waveform.clear(); - CHECK(waveform.rms_samples.size() == 0); - CHECK(waveform.max_samples.size() == 0); - vectors = waveform.vectors(); - CHECK(vectors.size() == 2); - CHECK(vectors[0].size() == 0); - CHECK(vectors[0].size() == 0); + // Create a reader + AudioWaveformData waveform; + + // Resize data to 10 elements + waveform.resize(10); + CHECK(waveform.rms_samples.size() == 10); + CHECK(waveform.max_samples.size() == 10); + + // Set all values = 1.0 + for (auto s = 0; s < waveform.rms_samples.size(); s++) { + waveform.rms_samples[s] = 1.0; + waveform.max_samples[s] = 1.0; + } + CHECK(waveform.rms_samples[0] == Approx(1.0f).margin(0.00001)); + CHECK(waveform.rms_samples[9] == Approx(1.0f).margin(0.00001)); + CHECK(waveform.max_samples[0] == Approx(1.0f).margin(0.00001)); + CHECK(waveform.max_samples[9] == Approx(1.0f).margin(0.00001)); + + // Scale all values by 2 + waveform.scale(10, 2.0); + CHECK(waveform.rms_samples.size() == 10); + CHECK(waveform.max_samples.size() == 10); + CHECK(waveform.rms_samples[0] == Approx(2.0f).margin(0.00001)); + CHECK(waveform.rms_samples[9] == Approx(2.0f).margin(0.00001)); + CHECK(waveform.max_samples[0] == Approx(2.0f).margin(0.00001)); + CHECK(waveform.max_samples[9] == Approx(2.0f).margin(0.00001)); + + // Zero out all values + waveform.zero(10); + CHECK(waveform.rms_samples.size() == 10); + CHECK(waveform.max_samples.size() == 10); + CHECK(waveform.rms_samples[0] == Approx(0.0f).margin(0.00001)); + CHECK(waveform.rms_samples[9] == Approx(0.0f).margin(0.00001)); + CHECK(waveform.max_samples[0] == Approx(0.0f).margin(0.00001)); + CHECK(waveform.max_samples[9] == Approx(0.0f).margin(0.00001)); + + // Access vectors and verify size + std::vector> vectors = waveform.vectors(); + CHECK(vectors.size() == 2); + CHECK(vectors[0].size() == 10); + CHECK(vectors[0].size() == 10); + + // Clear and verify internal data is empty + waveform.clear(); + CHECK(waveform.rms_samples.size() == 0); + CHECK(waveform.max_samples.size() == 0); + vectors = waveform.vectors(); + CHECK(vectors.size() == 2); + CHECK(vectors[0].size() == 0); + CHECK(vectors[0].size() == 0); } From 0c15c1692e6c953401c53f6ee0e092d417ae05c9 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 26 Sep 2025 18:34:33 -0500 Subject: [PATCH 36/41] Adding new reversed time curve unit test, to verify 230,000 samples are actually reversed over the length of the clip without skipping or missing a single one. --- tests/Clip.cpp | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/tests/Clip.cpp b/tests/Clip.cpp index 5f06a523f..9b332e7fe 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -913,3 +913,109 @@ TEST_CASE( "transform_path_identity_vs_scaled", "[libopenshot][clip][pr]" ) } } +TEST_CASE("Reverse time curve (sample-exact, no resampling)", "[libopenshot][clip][time][reverse]") +{ + using namespace openshot; + + // --- Construct predictable source audio in a cache (abs(sin)), 30fps, 44100Hz, stereo --- + const Fraction fps(30, 1); + const int sample_rate = 44100; + const int channels = 2; + const int frames_n = 90; // 3 seconds at 30fps + const int sppf = sample_rate / fps.ToDouble(); // 44100 / 30 = 1470 + const int total_samples = frames_n * sppf; + + const int OFFSET = 0; + const float AMPLITUDE = 0.75f; + const int NUM_SINE_STEPS = 100; + double angle = 0.0; + + CacheMemory cache; + cache.SetMaxBytes(0); + + for (int64_t fn = 1; fn <= frames_n; ++fn) { + auto f = std::make_shared(fn, sppf, channels); + f->SampleRate(sample_rate); + + // channel buffers for this frame + std::vector chbuf(sppf); + for (int s = 0; s < sppf; ++s) { + const float v = std::fabs(float(AMPLITUDE * std::sin(angle) + OFFSET)); + chbuf[s] = v; + angle += (2.0 * M_PI) / NUM_SINE_STEPS; + } + f->AddAudio(true, 0, 0, chbuf.data(), sppf, 1.0); + f->AddAudio(true, 1, 0, chbuf.data(), sppf, 1.0); + + cache.Add(f); + } + + DummyReader r(fps, 1920, 1080, sample_rate, channels, /*video_length_sec*/ 30.0, &cache); + r.Open(); + r.info.has_audio = true; + + // --- Build the expected "global reverse" vector (channel 0) --- + std::vector expected; + expected.reserve(total_samples); + for (int64_t fn = 1; fn <= frames_n; ++fn) { + auto f = cache.GetFrame(fn); + const float* p = f->GetAudioSamples(0); + expected.insert(expected.end(), p, p + sppf); + } + std::reverse(expected.begin(), expected.end()); + + // --- Clip with reverse time curve: timeline 1..frames_n -> source frames_n..1 + Clip clip(&r); + clip.time = Keyframe(); + clip.time.AddPoint(1.0, double(frames_n), LINEAR); + clip.time.AddPoint(double(frames_n), 1.0, LINEAR); + clip.time.PrintValues(); + + // set End to exactly frames_n/fps so timeline outputs frames_n frames + clip.End(float(frames_n) / float(fps.ToDouble())); + clip.Position(0.0); + + // Timeline matches reader (no resampling) + Timeline tl(1920, 1080, fps, sample_rate, channels, LAYOUT_STEREO); + tl.AddClip(&clip); + tl.Open(); + + // --- Pull timeline audio and concatenate into 'actual' + std::vector actual; + actual.reserve(total_samples); + + for (int64_t tf = 1; tf <= clip.VideoLength(); ++tf) { + auto fr = tl.GetFrame(tf); + const int n = fr->GetAudioSamplesCount(); + REQUIRE(n == sppf); // no resampling path => fixed count expected + + for (int i = 0; i < n; ++i) { + actual.push_back(fr->GetAudioSample(0, i, 1.0)); + } + } + + //REQUIRE(actual.size() == expected.size()); + + // --- Strict element-wise comparison + size_t mismatches = 0; + for (size_t i = 0; i < expected.size(); ++i) { + // The inputs are identical floats generated deterministically (no resampling), + // so we can compare with a very small tolerance. + if (actual[i] != Approx(expected[i]).margin(1e-6f)) { + // log a handful to make any future issues obvious + if (mismatches < 20) { + std::cout << "[DBG reverse no-resample] i=" << i + << " out=" << actual[i] << " exp=" << expected[i] << "\n"; + } + ++mismatches; + } + } + + CHECK(mismatches == 0); + + // Clean up + tl.Close(); + clip.Close(); + r.Close(); + cache.Clear(); +} \ No newline at end of file From 4cef4da9ef8bb9df5794d0106600af2301ffa735 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 26 Sep 2025 18:35:29 -0500 Subject: [PATCH 37/41] Fixing a bug in Keyframe that caused the GetDelta() function to return 0.0 early - which was breaking reversed time curves (zero'ing out the first frame or two) --- src/KeyFrame.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KeyFrame.cpp b/src/KeyFrame.cpp index 1df3f3c06..8c7615816 100644 --- a/src/KeyFrame.cpp +++ b/src/KeyFrame.cpp @@ -399,7 +399,7 @@ void Keyframe::SetJsonValue(const Json::Value root) { double Keyframe::GetDelta(int64_t index) const { if (index < 1) return 0.0; if (index == 1 && !Points.empty()) return Points[0].co.Y; - if (index >= GetLength()) return 0.0; + if (index > GetLength()) return 1.0; return GetValue(index) - GetValue(index - 1); } From 9a262882deb66046c91d684f203173170fbf5379 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 30 Sep 2025 23:09:32 -0500 Subject: [PATCH 38/41] A few small refactors of clip unit tests --- tests/Clip.cpp | 116 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 8 deletions(-) diff --git a/tests/Clip.cpp b/tests/Clip.cpp index 9b332e7fe..99e2b1165 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -24,6 +24,9 @@ #include #include "Clip.h" + +#include + #include "DummyReader.h" #include "Enums.h" #include "Exceptions.h" @@ -323,7 +326,6 @@ TEST_CASE( "GIF_timeline_mapping", "[libopenshot][clip][gif]" ) std::stringstream frame_save; t1.GetFrame(i)->Save(frame_save.str(), 1.0, "PNG", 100); int c = frame_color(t1.GetFrame(i)); - std::cout << c << std::endl; CHECK(c == expected_color(src)); slow_colors.insert(c); } @@ -913,6 +915,106 @@ TEST_CASE( "transform_path_identity_vs_scaled", "[libopenshot][clip][pr]" ) } } +TEST_CASE("Speed up time curve (3x, with resampling)", "[libopenshot][clip][time][speedup]") +{ + using namespace openshot; + + // --- Construct predictable source audio in a cache (linear ramp), 30fps, 44100Hz, stereo --- + const Fraction fps(30, 1); + const int sample_rate = 44100; + const int channels = 2; + const int frames_n = 270; // 9 seconds at 30fps (source span) + const int sppf = sample_rate / fps.ToDouble(); // 1470 + const int total_samples = frames_n * sppf; // 396,900 + + CacheMemory cache; + cache.SetMaxBytes(0); + + float ramp_value = 0.0f; + const float ramp_step = 1.0f / static_cast(total_samples); // linear ramp across entire source + + for (int64_t fn = 1; fn <= frames_n; ++fn) { + auto f = std::make_shared(fn, sppf, channels); + f->SampleRate(sample_rate); + + std::vector chbuf(sppf); + for (int s = 0; s < sppf; ++s) { + chbuf[s] = ramp_value; + ramp_value += ramp_step; + } + f->AddAudio(true, 0, 0, chbuf.data(), sppf, 1.0); + f->AddAudio(true, 1, 0, chbuf.data(), sppf, 1.0); + + cache.Add(f); + } + + DummyReader r(fps, 1920, 1080, sample_rate, channels, /*video_length_sec*/ 30.0, &cache); + r.Open(); + r.info.has_audio = true; + + // --- Expected output: 3x speed => every 3rd source sample + // Output duration is 3 seconds (90 frames) => 90 * 1470 = 132,300 samples + const int output_frames = 90; + const int out_samples = output_frames * sppf; // 132,300 + std::vector expected; + expected.reserve(out_samples); + for (int i = 0; i < out_samples; ++i) { + const int src_sample_index = i * 3; // exact 3x speed mapping in samples + expected.push_back(static_cast(src_sample_index) * ramp_step); + } + + // --- Clip with 3x speed curve: timeline frames 1..90 -> source frames 1..270 + Clip clip(&r); + clip.time = Keyframe(); + clip.time.AddPoint(1.0, 1.0, LINEAR); + clip.time.AddPoint(91.0, 271.0, LINEAR); // 90 timeline frames cover 270 source frames + clip.End(static_cast(output_frames) / static_cast(fps.ToDouble())); // 3.0s + clip.Position(0.0); + + // Timeline with resampling + Timeline tl(1920, 1080, fps, sample_rate, channels, LAYOUT_STEREO); + tl.AddClip(&clip); + tl.Open(); + + // --- Pull timeline audio and concatenate into 'actual' + std::vector actual; + actual.reserve(out_samples); + + for (int64_t tf = 1; tf <= output_frames; ++tf) { + auto fr = tl.GetFrame(tf); + const int n = fr->GetAudioSamplesCount(); + REQUIRE(n == sppf); + + const float* p = fr->GetAudioSamples(0); // RAW samples + actual.insert(actual.end(), p, p + n); + } + + REQUIRE(static_cast(actual.size()) == out_samples); + REQUIRE(actual.size() == expected.size()); + + // --- Compare with a tolerance appropriate for resampling + const float tolerance = 2e-2f; + + size_t mismatches = 0; + for (size_t i = 0; i < expected.size(); ++i) { + if (actual[i] != Approx(expected[i]).margin(tolerance)) { + if (mismatches < 20) { + std::cout << "[DBG speedup 3x] i=" << i + << " out=" << actual[i] << " exp=" << expected[i] << "\n"; + } + ++mismatches; + } + } + + CHECK(mismatches == 0); + + // Clean up + tl.Close(); + clip.Close(); + r.Close(); + cache.Clear(); +} + TEST_CASE("Reverse time curve (sample-exact, no resampling)", "[libopenshot][clip][time][reverse]") { using namespace openshot; @@ -969,7 +1071,6 @@ TEST_CASE("Reverse time curve (sample-exact, no resampling)", "[libopenshot][cli clip.time = Keyframe(); clip.time.AddPoint(1.0, double(frames_n), LINEAR); clip.time.AddPoint(double(frames_n), 1.0, LINEAR); - clip.time.PrintValues(); // set End to exactly frames_n/fps so timeline outputs frames_n frames clip.End(float(frames_n) / float(fps.ToDouble())); @@ -985,13 +1086,12 @@ TEST_CASE("Reverse time curve (sample-exact, no resampling)", "[libopenshot][cli actual.reserve(total_samples); for (int64_t tf = 1; tf <= clip.VideoLength(); ++tf) { - auto fr = tl.GetFrame(tf); - const int n = fr->GetAudioSamplesCount(); - REQUIRE(n == sppf); // no resampling path => fixed count expected + auto fr = tl.GetFrame(tf); + const int n = fr->GetAudioSamplesCount(); + REQUIRE(n == sppf); - for (int i = 0; i < n; ++i) { - actual.push_back(fr->GetAudioSample(0, i, 1.0)); - } + const float* p = fr->GetAudioSamples(0); // RAW samples + actual.insert(actual.end(), p, p + n); } //REQUIRE(actual.size() == expected.size()); From fd2952752d89e978436309c3255c3472c36d3022 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Sat, 11 Oct 2025 13:43:15 -0500 Subject: [PATCH 39/41] Improving audio directionality, with new function: SetAudioDirection(), so we can safely flip audio buffer direction when needed (i.e. time curves, reversed time). Also adding a new SetDirectionHint function to FrameMapper class - so our Clip class can inform the FrameMapper of its direction at any given moment. Also, clear resampler when changing directions inside a Time curve (since the audio buffer will be flipped - the resampler internal cache must be cleared). --- src/Clip.cpp | 22 ++++++++++++++++--- src/Frame.cpp | 23 +++++++++++--------- src/Frame.h | 7 +++---- src/FrameMapper.cpp | 51 ++++++++++++++++++++++++++++++++++++--------- src/FrameMapper.h | 10 +++++++++ 5 files changed, 86 insertions(+), 27 deletions(-) diff --git a/src/Clip.cpp b/src/Clip.cpp index 25c3bdc3a..00a205595 100644 --- a/src/Clip.cpp +++ b/src/Clip.cpp @@ -736,7 +736,8 @@ void Clip::apply_timemapping(std::shared_ptr frame) // Get delta (difference from this frame to the next time mapped frame: Y value) double delta = time.GetDelta(clip_frame_number + 1); - bool is_increasing = time.IsIncreasing(clip_frame_number + 1); + const bool prev_is_increasing = time.IsIncreasing(clip_frame_number); + const bool is_increasing = time.IsIncreasing(clip_frame_number + 1); // Determine length of source audio (in samples) // A delta of 1.0 == normal expected samples @@ -749,7 +750,7 @@ void Clip::apply_timemapping(std::shared_ptr frame) // Determine starting audio location AudioLocation location; - if (previous_location.frame == 0 || abs(new_frame_number - previous_location.frame) > 2) { + if (previous_location.frame == 0 || abs(new_frame_number - previous_location.frame) > 2 || prev_is_increasing != is_increasing) { // No previous location OR gap detected location.frame = new_frame_number; location.sample_start = 0; @@ -758,6 +759,7 @@ void Clip::apply_timemapping(std::shared_ptr frame) // We don't want to interpolate between unrelated audio data if (resampler) { delete resampler; + resampler = nullptr; } // Init resampler with # channels from Reader (should match the timeline) resampler = new AudioResampler(Reader()->info.channels); @@ -791,6 +793,12 @@ void Clip::apply_timemapping(std::shared_ptr frame) std::shared_ptr source_frame = GetOrCreateFrame(location.frame, false); int frame_sample_count = source_frame->GetAudioSamplesCount() - location.sample_start; + // Inform FrameMapper of the direction for THIS mapper frame + if (auto *fm = dynamic_cast(reader)) { + fm->SetDirectionHint(is_increasing); + } + source_frame->SetAudioDirection(is_increasing); + if (frame_sample_count == 0) { // No samples found in source frame (fill with silence) if (is_increasing) { @@ -877,10 +885,17 @@ std::shared_ptr Clip::GetOrCreateFrame(int64_t number, bool enable_time) try { // Init to requested frame int64_t clip_frame_number = adjust_frame_number_minimum(number); + bool is_increasing = true; // Adjust for time-mapping (if any) if (enable_time && time.GetLength() > 1) { - clip_frame_number = adjust_frame_number_minimum(time.GetLong(clip_frame_number)); + is_increasing = time.IsIncreasing(clip_frame_number + 1); + const int64_t time_frame_number = adjust_frame_number_minimum(time.GetLong(clip_frame_number)); + if (auto *fm = dynamic_cast(reader)) { + // Inform FrameMapper which direction this mapper frame is being requested + fm->SetDirectionHint(is_increasing); + } + clip_frame_number = time_frame_number; } // Debug output @@ -893,6 +908,7 @@ std::shared_ptr Clip::GetOrCreateFrame(int64_t number, bool enable_time) if (reader_frame) { // Override frame # (due to time-mapping might change it) reader_frame->number = number; + reader_frame->SetAudioDirection(is_increasing); // Return real frame // Create a new copy of reader frame diff --git a/src/Frame.cpp b/src/Frame.cpp index f799bcea9..2e785fb2a 100644 --- a/src/Frame.cpp +++ b/src/Frame.cpp @@ -48,7 +48,7 @@ Frame::Frame(int64_t number, int width, int height, std::string color, int sampl channels(channels), channel_layout(LAYOUT_STEREO), sample_rate(44100), has_audio_data(false), has_image_data(false), - max_audio_sample(0) + max_audio_sample(0), audio_is_increasing(true) { // zero (fill with silence) the audio buffer audio->clear(); @@ -96,6 +96,7 @@ void Frame::DeepCopy(const Frame& other) pixel_ratio = Fraction(other.pixel_ratio.num, other.pixel_ratio.den); color = other.color; max_audio_sample = other.max_audio_sample; + audio_is_increasing = other.audio_is_increasing; if (other.image) image = std::make_shared(*(other.image)); @@ -801,14 +802,16 @@ void Frame::ResizeAudio(int channels, int length, int rate, ChannelLayout layout max_audio_sample = length; } -// Reverse the audio buffer of this frame (will only reverse a single time, regardless of how many times -// you invoke this method) -void Frame::ReverseAudio() { - if (audio && !audio_reversed) { +/// Set the direction of the audio buffer of this frame +void Frame::SetAudioDirection(bool is_increasing) { + if (audio && !audio_is_increasing && is_increasing) { + // Forward audio buffer + audio->reverse(0, audio->getNumSamples()); + } else if (audio && audio_is_increasing && !is_increasing) { // Reverse audio buffer audio->reverse(0, audio->getNumSamples()); - audio_reversed = true; } + audio_is_increasing = is_increasing; } // Add audio samples to a specific channel @@ -838,8 +841,8 @@ void Frame::AddAudio(bool replaceSamples, int destChannel, int destStartSample, if (new_length > max_audio_sample) max_audio_sample = new_length; - // Reset audio reverse flag - audio_reversed = false; + // Reset audio direction + audio_is_increasing = true; } // Apply gain ramp (i.e. fading volume) @@ -995,6 +998,6 @@ void Frame::AddAudioSilence(int numSamples) // Calculate max audio sample added max_audio_sample = numSamples; - // Reset audio reverse flag - audio_reversed = false; + // Reset audio direction + audio_is_increasing = true; } diff --git a/src/Frame.h b/src/Frame.h index 528a69b85..7e8f26c55 100644 --- a/src/Frame.h +++ b/src/Frame.h @@ -102,7 +102,7 @@ namespace openshot int sample_rate; std::string color; int64_t max_audio_sample; ///< The max audio sample count added to this frame - bool audio_reversed; ///< Keep track of audio reversal (i.e. time keyframe) + bool audio_is_increasing; ///< Keep track of audio direction (i.e. related to time keyframe) #ifdef USE_OPENCV cv::Mat imagecv; ///< OpenCV image. It will always be in BGR format @@ -244,9 +244,8 @@ namespace openshot /// Set the original sample rate of this frame's audio data void SampleRate(int orig_sample_rate) { sample_rate = orig_sample_rate; }; - /// Reverse the audio buffer of this frame (will only reverse a single time, regardless of how many times - /// you invoke this method) - void ReverseAudio(); + /// Set the direction of the audio buffer of this frame + void SetAudioDirection(bool is_increasing); /// Save the frame image to the specified path. The image format can be BMP, JPG, JPEG, PNG, PPM, XBM, XPM void Save(std::string path, float scale, std::string format="PNG", int quality=100); diff --git a/src/FrameMapper.cpp b/src/FrameMapper.cpp index 8c05eb56e..fc0962b46 100644 --- a/src/FrameMapper.cpp +++ b/src/FrameMapper.cpp @@ -440,6 +440,34 @@ std::shared_ptr FrameMapper::GetFrame(int64_t requested_frame) // Find parent properties (if any) Clip *parent = static_cast(ParentClip()); bool is_increasing = true; + bool direction_flipped = false; + + { + const std::lock_guard lock(directionMutex); + + // One-shot: if a hint exists, consume it for THIS call, regardless of frame number. + if (have_hint) { + is_increasing = hint_increasing; + have_hint = false; + } else if (previous_frame > 0 && std::llabs(requested_frame - previous_frame) == 1) { + // Infer from request order when adjacent + is_increasing = (requested_frame > previous_frame); + } else if (last_dir_initialized) { + // Reuse last known direction if non-adjacent and no hint + is_increasing = last_is_increasing; + } else { + is_increasing = true; // default on first call + } + + // Detect flips so we can reset SR context + if (!last_dir_initialized) { + last_is_increasing = is_increasing; + last_dir_initialized = true; + } else if (last_is_increasing != is_increasing) { + direction_flipped = true; + last_is_increasing = is_increasing; + } + } if (parent) { float position = parent->Position(); float start = parent->Start(); @@ -448,10 +476,6 @@ std::shared_ptr FrameMapper::GetFrame(int64_t requested_frame) // since this heavily affects frame #s and audio mappings is_dirty = true; } - - // Determine direction of parent clip at this frame (forward or reverse direction) - // This is important for reversing audio in our resampler, for smooth reversed audio. - is_increasing = parent->time.IsIncreasing(requested_frame); } // Check if mappings are dirty (and need to be recalculated) @@ -548,13 +572,13 @@ std::shared_ptr FrameMapper::GetFrame(int64_t requested_frame) if (need_resampling) { - // Check for non-adjacent frame requests - so the resampler can be reset - if (abs(frame->number - previous_frame) > 1) { + // Reset resampler when non-adjacent request OR playback direction flips + if (direction_flipped || (previous_frame > 0 && std::llabs(requested_frame - previous_frame) > 1)) { if (avr) { // Delete resampler (if exists) SWR_CLOSE(avr); SWR_FREE(&avr); - avr = NULL; + avr = nullptr; } } @@ -630,9 +654,8 @@ std::shared_ptr FrameMapper::GetFrame(int64_t requested_frame) starting_frame++; } - // Reverse audio (if needed) - if (!is_increasing) - frame->ReverseAudio(); + // Set audio direction + frame->SetAudioDirection(is_increasing); // Resample audio on frame (if needed) if (need_resampling) @@ -1039,3 +1062,11 @@ int64_t FrameMapper::AdjustFrameNumber(int64_t clip_frame_number) { return frame_number; } + +// Set direction hint for the next call to GetFrame +void FrameMapper::SetDirectionHint(const bool increasing) +{ + const std::lock_guard lock(directionMutex); + hint_increasing = increasing; + have_hint = true; +} diff --git a/src/FrameMapper.h b/src/FrameMapper.h index 55ad74749..a932c2d4f 100644 --- a/src/FrameMapper.h +++ b/src/FrameMapper.h @@ -204,6 +204,13 @@ namespace openshot int64_t previous_frame; // Used during resampling, to determine when a large gap is detected SWRCONTEXT *avr; // Audio resampling context object + // Time curve / direction + std::recursive_mutex directionMutex; + bool have_hint = false; + bool hint_increasing = true; + bool last_is_increasing = true; + bool last_dir_initialized = false; + // Audio resampler (if resampling audio) openshot::AudioResampler *resampler; @@ -273,6 +280,9 @@ namespace openshot /// Open the internal reader void Open() override; + /// Set time-curve informed direction hint (from Clip class) for the next call to GetFrame + void SetDirectionHint(const bool increasing); + /// Print all of the original frames and which new frames they map to void PrintMapping(std::ostream* out=&std::cout); From 7e29fc093506ef993337fde200a66172881b9ce5 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Sat, 11 Oct 2025 16:29:11 -0500 Subject: [PATCH 40/41] Improving audio directionality, with new function: SetAudioDirection(), so we can safely flip audio buffer direction when needed (i.e. time curves, reversed time). Also adding a new SetDirectionHint function to FrameMapper class - so our Clip class can inform the FrameMapper of its direction at any given moment. Also, clear resampler when changing directions inside a Time curve (since the audio buffer will be flipped - the resampler internal cache must be cleared). --- src/AudioWaveformer.cpp | 11 ++- src/Clip.cpp | 159 -------------------------------------- src/Clip.h | 4 - src/ReaderBase.h | 5 -- tests/AudioWaveformer.cpp | 52 ++----------- tests/Clip.cpp | 2 +- 6 files changed, 18 insertions(+), 215 deletions(-) diff --git a/src/AudioWaveformer.cpp b/src/AudioWaveformer.cpp index 6b3866c0a..98c6610a0 100644 --- a/src/AudioWaveformer.cpp +++ b/src/AudioWaveformer.cpp @@ -17,6 +17,8 @@ #include #include +#include "Clip.h" + using namespace std; using namespace openshot; @@ -59,7 +61,14 @@ AudioWaveformData AudioWaveformer::ExtractSamples(int channel, int num_per_secon sample_divisor = 1; } - int64_t reader_video_length = reader->VideoLength(); + // Determine length of video frames (for waveform) + int64_t reader_video_length = reader->info.video_length; + if (const auto *clip = dynamic_cast(reader)) { + // If Clip-based reader, and time keyframes present + if (clip->time.GetCount() > 1) { + reader_video_length = clip->time.GetLength(); + } + } if (reader_video_length < 0) { reader_video_length = 0; } diff --git a/src/Clip.cpp b/src/Clip.cpp index 00a205595..6e56dd81f 100644 --- a/src/Clip.cpp +++ b/src/Clip.cpp @@ -363,165 +363,6 @@ ReaderBase* Clip::Reader() throw ReaderClosed("No Reader has been initialized for this Clip. Call Reader(*reader) before calling this method."); } -double Clip::resolve_timeline_fps() const -{ - if (timeline) { - const Timeline* attached_timeline = dynamic_cast(timeline); - if (attached_timeline) { - double timeline_fps = attached_timeline->info.fps.ToDouble(); - if (timeline_fps > 0.0) { - return timeline_fps; - } - } - } - - double clip_fps = info.fps.ToDouble(); - if (clip_fps > 0.0) { - return clip_fps; - } - - if (reader) { - double reader_fps = reader->info.fps.ToDouble(); - if (reader_fps > 0.0) { - return reader_fps; - } - } - - return 0.0; -} - -int64_t Clip::curve_extent_frames() const -{ - if (time.GetCount() <= 1) { - return 0; - } - - double max_timeline_frame = 0.0; - for (int64_t index = 0; index < time.GetCount(); ++index) { - const Point& curve_point = time.GetPoint(index); - if (curve_point.co.X > max_timeline_frame) { - max_timeline_frame = curve_point.co.X; - } - } - - if (max_timeline_frame <= 0.0) { - return 0; - } - - return static_cast(std::llround(max_timeline_frame)); -} - -int64_t Clip::trim_extent_frames(double fps_value) const -{ - if (fps_value <= 0.0) { - return 0; - } - - const double epsilon = 1e-6; - const double trimmed_start_seconds = static_cast(ClipBase::Start()); - const double trimmed_end_seconds = static_cast(ClipBase::End()); - - bool has_left_trim = trimmed_start_seconds > epsilon; - double original_duration = static_cast(info.duration); - if (original_duration <= 0.0 && reader) { - original_duration = static_cast(reader->info.duration); - } - if (original_duration <= 0.0) { - double source_fps = info.fps.ToDouble(); - int64_t source_length = info.video_length; - if ((source_length <= 0 || source_fps <= 0.0) && reader) { - source_length = reader->VideoLength(); - source_fps = reader->info.fps.ToDouble(); - } - if (source_length > 0 && source_fps > 0.0) { - original_duration = static_cast(source_length) / source_fps; - } - } - bool has_right_trim = false; - if (original_duration > 0.0) { - has_right_trim = std::fabs(trimmed_end_seconds - original_duration) > epsilon; - } - - if (!has_left_trim && !has_right_trim) { - int64_t base_frames = info.video_length; - if (base_frames <= 0 && reader) { - base_frames = reader->VideoLength(); - } - if (base_frames > 0) { - return base_frames; - } - } - - if (trimmed_end_seconds <= trimmed_start_seconds) { - return 0; - } - - const int64_t start_frame = static_cast(std::llround(trimmed_start_seconds * fps_value)); - const int64_t end_frame = static_cast(std::llround(trimmed_end_seconds * fps_value)); - const int64_t trimmed_frames = end_frame - start_frame; - return trimmed_frames > 0 ? trimmed_frames : 0; -} -// Determine clip video length (frame count), accounting for time-mapping curves when present. -int64_t Clip::VideoLength() const -{ - double fps_value = resolve_timeline_fps(); - int64_t trim_frames = trim_extent_frames(fps_value); - int64_t curve_frames = curve_extent_frames(); - - int64_t timeline_frames = std::max(trim_frames, curve_frames); - if (timeline_frames > 0) { - return timeline_frames; - } - - if (info.video_length > 0) { - return info.video_length; - } - - if (reader) { - return reader->VideoLength(); - } - - return 0; -} - -float Clip::MaxDuration() const -{ - double fps_value = resolve_timeline_fps(); - int64_t curve_frames = curve_extent_frames(); - - if (fps_value > 0.0 && curve_frames > 0) { - return static_cast(static_cast(curve_frames) / fps_value); - } - - float fallback_duration = ClipBase::Duration(); - if (fallback_duration > 0.0f) { - return fallback_duration; - } - - if (info.duration > 0.0f) { - return info.duration; - } - - double info_fps = info.fps.ToDouble(); - if (info.video_length > 0 && info_fps > 0.0) { - return static_cast(static_cast(info.video_length) / info_fps); - } - - if (reader) { - float reader_duration = reader->info.duration; - if (reader_duration > 0.0f) { - return reader_duration; - } - - double reader_fps = reader->info.fps.ToDouble(); - if (reader->info.video_length > 0 && reader_fps > 0.0) { - return static_cast(static_cast(reader->info.video_length) / reader_fps); - } - } - - return 0.0f; -} - // Open the internal reader void Clip::Open() { diff --git a/src/Clip.h b/src/Clip.h index 486265dde..cfb37768a 100644 --- a/src/Clip.h +++ b/src/Clip.h @@ -288,10 +288,6 @@ namespace openshot { /// Get the current reader openshot::ReaderBase* Reader(); - /// Duration and video length helpers which take into account time-mapping curves - float MaxDuration() const; - int64_t VideoLength() const override; - // Override End() position (in seconds) of clip (trim end of video) float End() const override; ///< Get end position (in seconds) of clip (trim end of video), which can be affected by the time curve. void End(float value) override; ///< Set end position (in seconds) of clip (trim end of video) diff --git a/src/ReaderBase.h b/src/ReaderBase.h index 43dcb85f8..aca12ff2c 100644 --- a/src/ReaderBase.h +++ b/src/ReaderBase.h @@ -125,11 +125,6 @@ namespace openshot /// Open the reader (and start consuming resources, such as images or video files) virtual void Open() = 0; - /// Get the detected number of frames in this reader. - /// Derived readers can override this to provide custom logic - /// for dynamic or procedurally generated frame counts. - virtual int64_t VideoLength() const { return info.video_length; } - virtual ~ReaderBase() = default; }; diff --git a/tests/AudioWaveformer.cpp b/tests/AudioWaveformer.cpp index 7707c91c4..fb51fda2e 100644 --- a/tests/AudioWaveformer.cpp +++ b/tests/AudioWaveformer.cpp @@ -161,9 +161,8 @@ TEST_CASE( "Extract waveform data clip slowed by time curve", "[libopenshot][aud const double expected_duration = (static_cast(original_video_length) * 2.0) / fps_value; const int expected_total = static_cast(std::ceil(expected_duration * samples_per_second)); CHECK(waveform.rms_samples.size() == expected_total); - CHECK(clip.VideoLength() == original_video_length * 2); - CHECK(clip.VideoLength() == static_cast(std::llround(expected_duration * fps_value))); - CHECK(clip.MaxDuration() == Approx(expected_duration).margin(0.0001)); + CHECK(clip.time.GetLength() == original_video_length * 2); + CHECK(clip.time.GetLength() == static_cast(std::llround(expected_duration * fps_value))); clip.Close(); reader.Close(); @@ -196,9 +195,8 @@ TEST_CASE( "Extract waveform data clip reversed by time curve", "[libopenshot][a const int expected_total = static_cast(std::ceil(expected_duration * samples_per_second)); CHECK(waveform.rms_samples.size() == expected_total); CHECK(expected_total == base_total); - CHECK(clip.VideoLength() == original_video_length); - CHECK(clip.VideoLength() == static_cast(std::llround(expected_duration * fps_value))); - CHECK(clip.MaxDuration() == Approx(expected_duration).margin(0.0001)); + CHECK(clip.time.GetLength() == original_video_length); + CHECK(clip.time.GetLength() == static_cast(std::llround(expected_duration * fps_value))); clip.Close(); reader.Close(); @@ -231,9 +229,8 @@ TEST_CASE( "Extract waveform data clip reversed and slowed", "[libopenshot][audi const int expected_total = static_cast(std::ceil(expected_duration * samples_per_second)); CHECK(waveform.rms_samples.size() == expected_total); CHECK(expected_total > base_total); - CHECK(clip.VideoLength() == original_video_length * 2); - CHECK(clip.VideoLength() == static_cast(std::llround(expected_duration * fps_value))); - CHECK(clip.MaxDuration() == Approx(expected_duration).margin(0.0001)); + CHECK(clip.time.GetLength() == original_video_length * 2); + CHECK(clip.time.GetLength() == static_cast(std::llround(expected_duration * fps_value))); clip.Close(); reader.Close(); @@ -272,46 +269,11 @@ TEST_CASE( "Clip duration uses parent timeline FPS when time-mapped", "[libopens REQUIRE(timeline_fps > 0.0); const double expected_duration = (static_cast(original_video_length) * 2.0) / timeline_fps; - CHECK(clip.MaxDuration() == Approx(expected_duration).margin(0.0001)); - CHECK(clip.VideoLength() == static_cast(std::llround(expected_duration * timeline_fps))); + CHECK(clip.time.GetLength() == static_cast(std::llround(expected_duration * timeline_fps))); clip.Close(); reader.Close(); } -TEST_CASE( "Image clip VideoLength matches trim on timeline", "[libopenshot][clip][timeline]" ) -{ - std::stringstream path; - path << TEST_MEDIA_PATH << "front.png"; - - FFmpegReader reader(path.str()); - Clip clip(&reader); - clip.Open(); - - Timeline timeline( - 640, - 480, - Fraction(30, 1), - 44100, - 2, - LAYOUT_STEREO); - - clip.ParentTimeline(&timeline); - clip.End(5.0f); - - const double timeline_fps = timeline.info.fps.ToDouble(); - REQUIRE(timeline_fps > 0.0); - - const float clip_end = clip.End(); - const int64_t expected_length = static_cast(std::llround(static_cast(clip_end) * timeline_fps)); - - REQUIRE(clip.Reader()->info.video_length > 0); - REQUIRE(expected_length > clip.Reader()->info.video_length); - CHECK(clip.VideoLength() == expected_length); - - clip.Close(); - reader.Close(); -} - TEST_CASE( "Extract waveform from image (no audio)", "[libopenshot][audiowaveformer]" ) { diff --git a/tests/Clip.cpp b/tests/Clip.cpp index 99e2b1165..62d0377e5 100644 --- a/tests/Clip.cpp +++ b/tests/Clip.cpp @@ -1085,7 +1085,7 @@ TEST_CASE("Reverse time curve (sample-exact, no resampling)", "[libopenshot][cli std::vector actual; actual.reserve(total_samples); - for (int64_t tf = 1; tf <= clip.VideoLength(); ++tf) { + for (int64_t tf = 1; tf <= frames_n; ++tf) { auto fr = tl.GetFrame(tf); const int n = fr->GetAudioSamplesCount(); REQUIRE(n == sppf); From 0932af2c1ed77663b0af6d0761525b78524506b5 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Sat, 11 Oct 2025 16:45:43 -0500 Subject: [PATCH 41/41] Fixing race condition on unit test for AnalogTape (when comparing frame's getting modified) --- tests/AnalogTape.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/AnalogTape.cpp b/tests/AnalogTape.cpp index 1a5357a93..4dcc15fa6 100644 --- a/tests/AnalogTape.cpp +++ b/tests/AnalogTape.cpp @@ -29,6 +29,8 @@ static std::shared_ptr makeGrayFrame(int w = 64, int h = 64) { TEST_CASE("AnalogTape modifies frame", "[effect][analogtape]") { AnalogTape eff; + eff.Id("analogtape-test-seed"); + eff.seed_offset = 1234; auto frame = makeGrayFrame(); QColor before = frame->GetImage()->pixelColor(2, 2); auto out = eff.GetFrame(frame, 1);

oPTV&Oz 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