| /* |
| * Copyright (c) 2026 The WebRTC project authors. All Rights Reserved. |
| * |
| * Use of this source code is governed by a BSD-style license |
| * that can be found in the LICENSE file in the root of the source |
| * tree. An additional intellectual property rights grant can be found |
| * in the file PATENTS. All contributing project authors may |
| * be found in the AUTHORS file in the root of the source tree. |
| */ |
| |
| #include "test/testsupport/pendulum_frame_generator.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <cstdint> |
| #include <cstdio> |
| #include <memory> |
| #include <numbers> |
| #include <optional> |
| #include <span> |
| #include <string> |
| #include <utility> |
| |
| #include "api/scoped_refptr.h" |
| #include "api/test/frame_generator_interface.h" |
| #include "api/video/resolution.h" |
| #include "rtc_base/checks.h" |
| #include "rtc_base/logging.h" |
| #include "test/frame_utils.h" |
| #include "test/testsupport/file_utils.h" |
| #include "test/testsupport/frame_reader.h" |
| |
| namespace webrtc { |
| namespace test { |
| namespace { |
| |
| scoped_refptr<I420Buffer> ReadI420BufferFromFile(const std::string& filepath, |
| int width, |
| int height) { |
| FILE* file = fopen(filepath.c_str(), "rb"); |
| if (file == nullptr) { |
| return nullptr; |
| } |
| scoped_refptr<I420Buffer> buffer = ReadI420Buffer(width, height, file); |
| fclose(file); |
| return buffer; |
| } |
| |
| class PendulumFrameReader : public FrameReader { |
| public: |
| explicit PendulumFrameReader( |
| std::unique_ptr<PendulumFrameGenerator> generator) |
| : generator_(std::move(generator)) {} |
| |
| scoped_refptr<I420Buffer> PullFrame() override { |
| RTC_CHECK(generator_); |
| ++frame_num_; |
| return generator_->NextI420Frame(); |
| } |
| |
| scoped_refptr<I420Buffer> PullFrame(int* frame_num) override { |
| *frame_num = frame_num_; |
| return PullFrame(); |
| } |
| |
| scoped_refptr<I420Buffer> ReadFrame(int frame_num) override { |
| return PullFrame(); |
| } |
| |
| scoped_refptr<I420Buffer> PullFrame(int* frame_num, |
| Resolution resolution, |
| Ratio framerate_scale) override { |
| generator_->ChangeResolution(resolution.width, resolution.height); |
| *frame_num = frame_num_; |
| return PullFrame(); |
| } |
| |
| scoped_refptr<I420Buffer> ReadFrame(int frame_num, |
| Resolution resolution) override { |
| generator_->ChangeResolution(resolution.width, resolution.height); |
| return PullFrame(); |
| } |
| |
| int num_frames() const override { return 1000000; } |
| |
| private: |
| std::unique_ptr<PendulumFrameGenerator> generator_; |
| int frame_num_ = 0; |
| }; |
| |
| } // namespace |
| |
| PendulumFrameGenerator::PendulumFrameGenerator(Config config) |
| : config_(config) { |
| Reset(); |
| LoadOrGenerateSourceImage(); |
| } |
| |
| void PendulumFrameGenerator::Reset() { |
| th1_ = 0.5; |
| th2_ = 0.5; |
| w1_ = 0.0; |
| w2_ = 0.0; |
| sim_time_ = 0.0; |
| zoom_phase_ = 0.0; |
| prng_state_ = 123456789; |
| } |
| |
| void PendulumFrameGenerator::ChangeResolution(size_t width, size_t height) { |
| config_.target_resolution.width = static_cast<int>(width); |
| config_.target_resolution.height = static_cast<int>(height); |
| } |
| |
| FrameGeneratorInterface::Resolution PendulumFrameGenerator::GetResolution() |
| const { |
| return {.width = static_cast<size_t>(config_.target_resolution.width), |
| .height = static_cast<size_t>(config_.target_resolution.height)}; |
| } |
| |
| void PendulumFrameGenerator::StepPhysics(double dt) { |
| constexpr double g = 9.81; |
| constexpr double gamma = 0.08; |
| constexpr double drive1_amp = 5.0; |
| constexpr double drive1_freq = 2.4; |
| constexpr double drive2_amp = 3.5; |
| constexpr double drive2_freq = 1.7; |
| |
| auto derivs = [&](double t1, double t2, double v1, double v2, double t, |
| double& dt1, double& dt2, double& dv1, double& dv2) { |
| double delta = t1 - t2; |
| double den = 2.0 - std::cos(2.0 * delta); |
| double num1 = -g * 3.0 * std::sin(t1) - g * std::sin(t1 - 2.0 * t2) - |
| 2.0 * std::sin(delta) * (v2 * v2 + v1 * v1 * std::cos(delta)); |
| double num2 = |
| 2.0 * std::sin(delta) * |
| (2.0 * v1 * v1 + 2.0 * g * std::cos(t1) + v2 * v2 * std::cos(delta)); |
| dt1 = v1; |
| dt2 = v2; |
| dv1 = (num1 / den) - gamma * v1 + drive1_amp * std::cos(drive1_freq * t); |
| dv2 = (num2 / den) - gamma * v2 + drive2_amp * std::cos(drive2_freq * t); |
| }; |
| |
| double k1_t1, k1_t2, k1_v1, k1_v2; |
| derivs(th1_, th2_, w1_, w2_, sim_time_, k1_t1, k1_t2, k1_v1, k1_v2); |
| |
| double k2_t1, k2_t2, k2_v1, k2_v2; |
| derivs(th1_ + 0.5 * dt * k1_t1, th2_ + 0.5 * dt * k1_t2, |
| w1_ + 0.5 * dt * k1_v1, w2_ + 0.5 * dt * k1_v2, sim_time_ + 0.5 * dt, |
| k2_t1, k2_t2, k2_v1, k2_v2); |
| |
| double k3_t1, k3_t2, k3_v1, k3_v2; |
| derivs(th1_ + 0.5 * dt * k2_t1, th2_ + 0.5 * dt * k2_t2, |
| w1_ + 0.5 * dt * k2_v1, w2_ + 0.5 * dt * k2_v2, sim_time_ + 0.5 * dt, |
| k3_t1, k3_t2, k3_v1, k3_v2); |
| |
| double k4_t1, k4_t2, k4_v1, k4_v2; |
| derivs(th1_ + dt * k3_t1, th2_ + dt * k3_t2, w1_ + dt * k3_v1, |
| w2_ + dt * k3_v2, sim_time_ + dt, k4_t1, k4_t2, k4_v1, k4_v2); |
| |
| th1_ += (dt / 6.0) * (k1_t1 + 2.0 * k2_t1 + 2.0 * k3_t1 + k4_t1); |
| th2_ += (dt / 6.0) * (k1_t2 + 2.0 * k2_t2 + 2.0 * k3_t2 + k4_t2); |
| w1_ += (dt / 6.0) * (k1_v1 + 2.0 * k2_v1 + 2.0 * k3_v1 + k4_v1); |
| w2_ += (dt / 6.0) * (k1_v2 + 2.0 * k2_v2 + 2.0 * k3_v2 + k4_v2); |
| sim_time_ += dt; |
| } |
| |
| void PendulumFrameGenerator::LoadOrGenerateSourceImage() { |
| const std::string& path = |
| config_.source_image_path.empty() |
| ? ResourcePath("difficult_photo_1850_1110", "yuv") |
| : config_.source_image_path; |
| |
| FrameGeneratorInterface::Resolution res = config_.source_resolution; |
| if (std::optional<webrtc::Resolution> parsed_res = |
| ParseResolutionFromFileName(path)) { |
| res.width = static_cast<size_t>(parsed_res->width); |
| res.height = static_cast<size_t>(parsed_res->height); |
| } |
| |
| source_image_ = ReadI420BufferFromFile(path, static_cast<int>(res.width), |
| static_cast<int>(res.height)); |
| |
| if (!source_image_) { |
| RTC_LOG(LS_WARNING) << "Failed to read " << path |
| << ", trying photo_1850_1110.yuv"; |
| const std::string fallback_path = ResourcePath("photo_1850_1110", "yuv"); |
| const std::optional<webrtc::Resolution> fallback_res = |
| ParseResolutionFromFileName(fallback_path); |
| const int fallback_width = fallback_res ? fallback_res->width : 1850; |
| const int fallback_height = fallback_res ? fallback_res->height : 1110; |
| source_image_ = |
| ReadI420BufferFromFile(fallback_path, fallback_width, fallback_height); |
| } |
| |
| if (!source_image_) { |
| RTC_LOG(LS_WARNING) |
| << "Generating synthetic test pattern for pendulum generator"; |
| const int w = res.width > 0 ? static_cast<int>(res.width) : 1850; |
| const int h = res.height > 0 ? static_cast<int>(res.height) : 1110; |
| source_image_ = I420Buffer::Create(w, h); |
| std::span<uint8_t> py(source_image_->MutableDataY(), |
| static_cast<size_t>(h * source_image_->StrideY())); |
| for (int y = 0; y < h; ++y) { |
| const int row = y * source_image_->StrideY(); |
| for (int x = 0; x < w; ++x) { |
| const int pattern = ((x / 16) ^ (y / 16)) & 1 ? 200 : 40; |
| const int fine_pattern = ((x / 2) ^ (y / 2)) & 1 ? 20 : -20; |
| py[row + x] = static_cast<uint8_t>( |
| std::clamp(pattern + fine_pattern + (x % 64), 16, 235)); |
| } |
| } |
| std::span<uint8_t> pu( |
| source_image_->MutableDataU(), |
| static_cast<size_t>((h / 2) * source_image_->StrideU())); |
| std::span<uint8_t> pv( |
| source_image_->MutableDataV(), |
| static_cast<size_t>((h / 2) * source_image_->StrideV())); |
| for (int y = 0; y < h / 2; ++y) { |
| const int row_u = y * source_image_->StrideU(); |
| const int row_v = y * source_image_->StrideV(); |
| for (int x = 0; x < w / 2; ++x) { |
| pu[row_u + x] = static_cast<uint8_t>(128 + ((x * 4) % 100) - 50); |
| pv[row_v + x] = static_cast<uint8_t>(128 + ((y * 4) % 100) - 50); |
| } |
| } |
| } |
| } |
| |
| void PendulumFrameGenerator::ApplyNoise(I420Buffer* buffer) { |
| const int width = buffer->width(); |
| const int height = buffer->height(); |
| const int stride_y = buffer->StrideY(); |
| std::span<uint8_t> plane(buffer->MutableDataY(), |
| static_cast<size_t>(stride_y * height)); |
| const int noise_range = 2 * config_.noise_level + 1; |
| |
| for (int y = 0; y < height; ++y) { |
| const int row_offset = y * stride_y; |
| for (int x = 0; x < width; ++x) { |
| prng_state_ ^= prng_state_ << 13; |
| prng_state_ ^= prng_state_ >> 17; |
| prng_state_ ^= prng_state_ << 5; |
| const int noise = |
| static_cast<int>(prng_state_ % noise_range) - config_.noise_level; |
| plane[row_offset + x] = static_cast<uint8_t>( |
| std::clamp(static_cast<int>(plane[row_offset + x]) + noise, 0, 255)); |
| } |
| } |
| } |
| |
| FrameGeneratorInterface::VideoFrameData PendulumFrameGenerator::NextFrame() { |
| return VideoFrameData(NextI420Frame(), std::nullopt); |
| } |
| |
| scoped_refptr<I420Buffer> PendulumFrameGenerator::NextI420Frame() { |
| RTC_CHECK(source_image_); |
| const double dt = 1.0 / std::max(1, config_.fps); |
| for (int i = 0; i < 4; ++i) { |
| StepPhysics(dt / 4.0); |
| } |
| |
| // Smoothly map angles to [0, 1] normalized coordinates |
| const double norm_x = 0.5 + 0.5 * std::clamp(std::sin(th1_), -1.0, 1.0); |
| const double norm_y = 0.5 + 0.5 * std::clamp(std::sin(th2_), -1.0, 1.0); |
| |
| // Dynamic zoom factor |
| zoom_phase_ += (2.0 * std::numbers::pi * config_.zoom_speed * dt) + |
| 0.02 * (std::abs(w1_) + std::abs(w2_)); |
| const double norm_z = 0.5 + 0.5 * std::sin(zoom_phase_); |
| double zoom = |
| config_.min_zoom + norm_z * (config_.max_zoom - config_.min_zoom); |
| zoom = std::max(1.0, zoom); |
| |
| const int src_w = source_image_->width(); |
| const int src_h = source_image_->height(); |
| const int tgt_w = config_.target_resolution.width; |
| const int tgt_h = config_.target_resolution.height; |
| |
| int max_crop_w, max_crop_h; |
| if (static_cast<int64_t>(src_w) * tgt_h > |
| static_cast<int64_t>(src_h) * tgt_w) { |
| max_crop_h = src_h; |
| max_crop_w = |
| static_cast<int>((static_cast<int64_t>(src_h) * tgt_w) / tgt_h); |
| } else { |
| max_crop_w = src_w; |
| max_crop_h = |
| static_cast<int>((static_cast<int64_t>(src_w) * tgt_h) / tgt_w); |
| } |
| |
| int crop_w = static_cast<int>(max_crop_w / zoom); |
| int crop_h = static_cast<int>(max_crop_h / zoom); |
| crop_w = std::clamp(crop_w & ~1, 2, src_w & ~1); |
| crop_h = std::clamp(crop_h & ~1, 2, src_h & ~1); |
| |
| const int max_offset_x = (src_w - crop_w) & ~1; |
| const int max_offset_y = (src_h - crop_h) & ~1; |
| |
| const int offset_x = |
| std::clamp(static_cast<int>(norm_x * max_offset_x) & ~1, 0, max_offset_x); |
| const int offset_y = |
| std::clamp(static_cast<int>(norm_y * max_offset_y) & ~1, 0, max_offset_y); |
| |
| scoped_refptr<I420Buffer> target_buffer = I420Buffer::Create(tgt_w, tgt_h); |
| target_buffer->CropAndScaleFrom(*source_image_, offset_x, offset_y, crop_w, |
| crop_h); |
| |
| if (config_.noise_level > 0) { |
| ApplyNoise(target_buffer.get()); |
| } |
| |
| return target_buffer; |
| } |
| |
| std::unique_ptr<PendulumFrameGenerator> CreatePendulumFrameGenerator( |
| const PendulumFrameGenerator::Config& config) { |
| return std::make_unique<PendulumFrameGenerator>(config); |
| } |
| |
| std::unique_ptr<FrameReader> CreatePendulumFrameReader( |
| const PendulumFrameGenerator::Config& config) { |
| return std::make_unique<PendulumFrameReader>( |
| CreatePendulumFrameGenerator(config)); |
| } |
| |
| } // namespace test |
| } // namespace webrtc |