From 37013cb9026c31e7b6892cc7cb8b79fc7a025396 Mon Sep 17 00:00:00 2001 From: Yggdrasil75 Date: Fri, 14 Nov 2025 13:08:35 -0500 Subject: [PATCH] spatial grid is annoying. also added frame storage --- tests/g2chromatic2.cpp | 32 ++- util/grid/grid22.hpp | 178 ++++++++++++--- util/output/aviwriter.hpp | 101 ++++++++- util/output/frame.hpp | 376 +++++++++++++++++++++++++++++++ util/output/video.hpp | 458 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 1105 insertions(+), 40 deletions(-) create mode 100644 util/output/frame.hpp create mode 100644 util/output/video.hpp diff --git a/tests/g2chromatic2.cpp b/tests/g2chromatic2.cpp index 3d21b60..fb82c02 100644 --- a/tests/g2chromatic2.cpp +++ b/tests/g2chromatic2.cpp @@ -9,10 +9,11 @@ #include "../util/output/aviwriter.hpp" #include "../util/output/bmpwriter.hpp" #include "../util/timing_decorator.cpp" +#include "../util/output/frame.hpp" struct AnimationConfig { - int width = 256; - int height = 256; + int width = 1024; + int height = 1024; int totalFrames = 480; float fps = 30.0f; int numSeeds = 8; @@ -149,22 +150,33 @@ int main() { AnimationConfig config; Grid2 grid = setup(config); - //grid.updateNeighborMap(); Preview(grid); std::vector> seeds = pickSeeds(grid,config); - std::vector> frames; + std::vector frames; // Change to vector of frame objects for (int i = 0; i < config.totalFrames; ++i){ std::cout << "Processing frame " << i + 1 << "/" << config.totalFrames << std::endl; expandPixel(grid,config,seeds); - int width; - int height; - std::vector frame; - grid.getGridAsBGR(width,height,frame); - frames.push_back(frame); + + frame outputFrame; + grid.getGridAsFrame(outputFrame, {'B', 'G', 'R'}); // Directly get as BGR frame + + // Alternative: Use the dedicated BGR method + // int width, height; + // grid.getGridAsBGRFrame(width, height, outputFrame); + + frames.push_back(outputFrame); + } + + // Use the frame-based AVIWriter overload + bool success = AVIWriter::saveAVI("output/chromatic_transformation.avi", frames, config.fps); + + if (success) { + std::cout << "Successfully saved AVI with " << frames.size() << " frames" << std::endl; + } else { + std::cout << "Failed to save AVI file!" << std::endl; } - exportavi(frames,config); FunctionTimer::printStats(FunctionTimer::Mode::ENHANCED); return 0; } \ No newline at end of file diff --git a/util/grid/grid22.hpp b/util/grid/grid22.hpp index b5e5750..d03f99b 100644 --- a/util/grid/grid22.hpp +++ b/util/grid/grid22.hpp @@ -2,6 +2,7 @@ #include "../vectorlogic/vec2.hpp" #include "../vectorlogic/vec4.hpp" #include "../timing_decorator.hpp" +#include "../output/frame.hpp" #include #include #ifndef GRID2_HPP @@ -53,7 +54,7 @@ public: ƨnoiƚiƨoꟼ.reserve(size); } - size_t size() { + size_t size() const { return Positions.size(); } @@ -102,8 +103,8 @@ public: class SpatialGrid { private: float cellSize; - std::unordered_map, Vec2::Hash> grid; public: + std::unordered_map, Vec2::Hash> grid; SpatialGrid(float cellSize = 2.0f) : cellSize(cellSize) {} Vec2 worldToGrid(const Vec2& worldPos) const { @@ -186,8 +187,8 @@ private: float neighborRadius = 1.0f; //TODO: spatial map - std::unordered_map spatialTable; - float spatSize = 2.0f; + SpatialGrid spatialGrid; + float spatialCellSize = 2.0f; public: //get position from id Vec2 getPositionID(size_t id) const { @@ -196,33 +197,51 @@ public: } //get id from position (optional radius, picks first found. radius of 0 becomes epsilon if none are found) - size_t getPositionVec(Vec2 pos, float radius = 0.0f) { - auto it = Positions.at(pos); - return it; + size_t getPositionVec(const Vec2& pos, float radius = 0.0f) { + if (radius == 0.0f) { + // Exact match - use spatial grid to find the cell + Vec2 gridPos = spatialGrid.worldToGrid(pos); + auto cellIt = spatialGrid.grid.find(gridPos); + if (cellIt != spatialGrid.grid.end()) { + for (size_t id : cellIt->second) { + if (Positions.at(id) == pos) { + return id; + } + } + } + throw std::out_of_range("Position not found"); + } else { + auto results = getPositionVecRegion(pos, radius); + if (!results.empty()) { + return results[0]; // Return first found + } + throw std::out_of_range("No positions found in radius"); + } } size_t getPositionVec(float x, float y, float radius = 0.0f) { return getPositionVec(Vec2(x,y), radius); } - size_t getIDFromSpatPOS(Vec2 pos) { - Vec2 spat2pos = (pos / spatSize).floor(); - reverselookupassistantclasscausecppisdumb spatids = spatialTable.at(spat2pos); - return spatids.at(pos); - } - //get all id in region - std::vector getPositionVecRegion(Vec2 pos, float radius = 1.0f) { + std::vector getPositionVecRegion(const Vec2& pos, float radius = 1.0f) { + TIME_FUNCTION; float searchRadius = (radius == 0.0f) ? std::numeric_limits::epsilon() : radius; - - float radiusSq = searchRadius*searchRadius; - std::vector posvec; - for (const auto& pair : Positions) { - if (pair.second.distanceSquared(pos) <= radiusSq) { - posvec.push_back(pair.first); + + // Get candidates from spatial grid + std::vector candidates = spatialGrid.queryRange(pos, searchRadius); + + // Fine-filter by exact distance + std::vector results; + float radiusSq = searchRadius * searchRadius; + + for (size_t id : candidates) { + if (Positions.at(id).distanceSquared(pos) <= radiusSq) { + results.push_back(id); } } - return posvec; + + return results; } //get color from id @@ -252,18 +271,27 @@ public: size_t id = Positions.set(pos); Colors[id] = color; Sizes[id] = size; - return id; + + // Add to spatial grid + spatialGrid.insert(id, pos); updateNeighborForID(id); + return id; } //set position by id - void setPosition(size_t id, const Vec2& position) { - Positions.at(id).move(position); + void setPosition(size_t id, const Vec2& newPosition) { + Vec2 oldPosition = Positions.at(id); + spatialGrid.update(id, oldPosition, newPosition); + Positions.at(id).move(newPosition); updateNeighborForID(id); } void setPosition(size_t id, float x, float y) { - Positions.at(id).move(Vec2(x,y)); + Vec2 newPos = Vec2(x,y); + Vec2 oldPos = Positions.at(id); + + spatialGrid.update(id, oldPos, newPos); + Positions.at(id).move(newPos); updateNeighborForID(id); } @@ -313,10 +341,12 @@ public: //remove object (should remove the id, the color, the position, and the size) size_t removeID(size_t id) { + Vec2 oldPosition = Positions.at(id); Positions.remove(id); Colors.erase(id); Sizes.erase(id); unassignedIDs.push_back(id); + spatialGrid.remove(id, oldPosition); updateNeighborForID(id); return id; } @@ -327,6 +357,7 @@ public: Colors.erase(id); Sizes.erase(id); unassignedIDs.push_back(id); + spatialGrid.remove(id, pos); updateNeighborForID(id); return id; } @@ -335,8 +366,9 @@ public: void bulkUpdatePositions(const std::unordered_map& newPositions) { TIME_FUNCTION; for (const auto& [id, newPos] : newPositions) { - auto it = Positions.at(id); - it.move(newPos); + Vec2 oldPosition = Positions.at(id); + Positions.at(id).move(newPos); + spatialGrid.update(id, oldPosition, newPos); } updateNeighborMap(); } @@ -379,12 +411,14 @@ public: Sizes.reserve(Sizes.size() + objects.size()); // Batch insertion + #pragma omp parallel for for (size_t i = 0; i < objects.size(); ++i) { const auto& [pos, color, size] = objects[i]; size_t id = Positions.set(pos); Colors[id] = color; Sizes[id] = size; + spatialGrid.insert(id,pos); } shrinkIfNeeded(); @@ -410,6 +444,7 @@ public: size_t id = Positions.set(poses[i]); Colors[id] = colors[i]; Sizes[id] = sizes[i]; + spatialGrid.insert(id,poses[i]); } shrinkIfNeeded(); @@ -526,6 +561,88 @@ public: getBoundingBox(minCorner, maxCorner); getGridRegionAsBGR(minCorner, maxCorner, width, height, bgrData); } + + // Get region as frame with customizable channels + void getGridRegionAsFrame(const Vec2& minCorner, const Vec2& maxCorner, + int& width, int& height, frame& outputFrame, + const std::vector& channels = {'R', 'G', 'B'}) const { + TIME_FUNCTION; + // Calculate dimensions + width = static_cast(maxCorner.x - minCorner.x); + height = static_cast(maxCorner.y - minCorner.y); + + if (width <= 0 || height <= 0) { + width = height = 0; + outputFrame.clear(); + return; + } + + // Initialize frame with specified channels + outputFrame.resize(width, height, channels); + // For each position in the grid, find the corresponding pixel + for (const auto& [id, pos] : Positions) { + if (pos.x >= minCorner.x && pos.x < maxCorner.x && + pos.y >= minCorner.y && pos.y < maxCorner.y) { + + // Calculate pixel coordinates + int pixelX = static_cast(pos.x - minCorner.x); + int pixelY = static_cast(pos.y - minCorner.y); + + // Ensure within bounds + if (pixelX >= 0 && pixelX < width && pixelY >= 0 && pixelY < height) { + // Get color + const Vec4& color = Colors.at(id); + + // Set pixel data based on requested channels + for (size_t channel_idx = 0; channel_idx < channels.size(); ++channel_idx) { + float value = 0.0f; + switch (channels[channel_idx]) { + case 'R': case 'r': value = color.r; break; + case 'G': case 'g': value = color.g; break; + case 'B': case 'b': value = color.b; break; + case 'A': case 'a': value = color.a; break; + case 'X': case 'x': value = pos.x - minCorner.x; break; // Normalized X + case 'Y': case 'y': value = pos.y - minCorner.y; break; // Normalized Y + case 'S': case 's': value = Sizes.at(id); break; // Size + case 'I': case 'i': value = static_cast(id) / Positions.size(); break; // Normalized ID + default: value = 0.0f; break; + } + outputFrame.at(pixelY, pixelX, channel_idx) = static_cast(value * 255); + } + } + } + } + } + + // Get full grid as frame + void getGridAsFrame(frame& outputFrame, const std::vector& channels = {'R', 'G', 'B'}) { + int width, height; + Vec2 minCorner, maxCorner; + getBoundingBox(minCorner, maxCorner); + getGridRegionAsFrame(minCorner, maxCorner, width, height, outputFrame, channels); + } + + // Get region as frame with common channel configurations + void getGridRegionAsRGBFrame(const Vec2& minCorner, const Vec2& maxCorner, + int& width, int& height, frame& outputFrame) { + getGridRegionAsFrame(minCorner, maxCorner, width, height, outputFrame, {'R', 'G', 'B'}); + } + + void getGridRegionAsBGRFrame(const Vec2& minCorner, const Vec2& maxCorner, + int& width, int& height, frame& outputFrame) { + getGridRegionAsFrame(minCorner, maxCorner, width, height, outputFrame, {'B', 'G', 'R'}); + } + + void getGridRegionAsRGBAFrame(const Vec2& minCorner, const Vec2& maxCorner, + int& width, int& height, frame& outputFrame) { + getGridRegionAsFrame(minCorner, maxCorner, width, height, outputFrame, {'R', 'G', 'B', 'A'}); + } + + void getGridRegionAsBGRAFrame(const Vec2& minCorner, const Vec2& maxCorner, + int& width, int& height, frame& outputFrame) { + getGridRegionAsFrame(minCorner, maxCorner, width, height, outputFrame, {'B', 'G', 'R', 'A'}); + } + //get bounding box void getBoundingBox(Vec2& minCorner, Vec2& maxCorner) { @@ -562,6 +679,8 @@ public: Positions.clear(); Colors.clear(); Sizes.clear(); + spatialGrid.clear(); + neighborMap.clear(); } // neighbor map @@ -573,9 +692,10 @@ public: for (const auto& [id1, pos1] : Positions) { std::vector neighbors; float radiusSq = neighborRadius * neighborRadius; + auto candidate_ids = spatialGrid.queryRange(pos1, neighborRadius); - for (const auto& [id2, pos2] : Positions) { - if (id1 != id2 && pos1.distanceSquared(pos2) <= radiusSq) { + for (size_t id2 : candidate_ids) { + if (id1 != id2 && Positions.at(id1).distanceSquared(Positions.at(id2)) <= radiusSq) { neighbors.push_back(id2); } } diff --git a/util/output/aviwriter.hpp b/util/output/aviwriter.hpp index 6d1728d..7295ffb 100644 --- a/util/output/aviwriter.hpp +++ b/util/output/aviwriter.hpp @@ -8,6 +8,8 @@ #include #include #include +#include "frame.hpp" +#include "video.hpp" class AVIWriter { private: @@ -110,7 +112,73 @@ private: } } + // Helper function to convert frame to RGB format + static std::vector frameToRGB(const frame& frm) { + if (frm.empty()) { + return {}; + } + + size_t width = frm.width(); + size_t height = frm.height(); + std::vector rgbData(width * height * 3); + + // Check if frame already has RGB channels + bool hasR = frm.has_channel('R') || frm.has_channel('r'); + bool hasG = frm.has_channel('G') || frm.has_channel('g'); + bool hasB = frm.has_channel('B') || frm.has_channel('b'); + + if (hasR && hasG && hasB) { + // Frame has RGB channels - extract them + std::vector rChannel = frm.has_channel('R') ? + frm.get_channel_data('R') : frm.get_channel_data('r'); + std::vector gChannel = frm.has_channel('G') ? + frm.get_channel_data('G') : frm.get_channel_data('g'); + std::vector bChannel = frm.has_channel('B') ? + frm.get_channel_data('B') : frm.get_channel_data('b'); + + // Convert to BGR format (required by AVI) + for (size_t i = 0; i < width * height; ++i) { + rgbData[i * 3 + 0] = bChannel[i]; // Blue + rgbData[i * 3 + 1] = gChannel[i]; // Green + rgbData[i * 3 + 2] = rChannel[i]; // Red + } + } else if (frm.channels_count() == 1) { + // Grayscale frame - convert to RGB + std::vector grayChannel = frm.get_channel_data(frm.channels()[0]); + + for (size_t i = 0; i < width * height; ++i) { + uint8_t gray = grayChannel[i]; + rgbData[i * 3 + 0] = gray; // Blue + rgbData[i * 3 + 1] = gray; // Green + rgbData[i * 3 + 2] = gray; // Red + } + } else if (frm.channels_count() == 3) { + // Assume the 3 channels are RGB (even if not named) + // Convert to BGR format + for (size_t y = 0; y < height; ++y) { + for (size_t x = 0; x < width; ++x) { + rgbData[(y * width + x) * 3 + 0] = frm.at(y, x, size_t(2)); // Blue + rgbData[(y * width + x) * 3 + 1] = frm.at(y, x, size_t(1)); // Green + rgbData[(y * width + x) * 3 + 2] = frm.at(y, x, size_t(0)); // Red + } + } + } else { + // Unsupported format - use first channel as grayscale + std::vector firstChannel = frm.get_channel_data(frm.channels()[0]); + + for (size_t i = 0; i < width * height; ++i) { + uint8_t gray = firstChannel[i]; + rgbData[i * 3 + 0] = gray; // Blue + rgbData[i * 3 + 1] = gray; // Green + rgbData[i * 3 + 2] = gray; // Red + } + } + + return rgbData; + } + public: + // Original method for vector of raw frame data static bool saveAVI(const std::string& filename, const std::vector>& frames, int width, int height, float fps = 30.0f) { @@ -120,7 +188,7 @@ public: // std::cout << "1" << "width: " << width << // "height: " << height << "frame count: " << fps << std::endl; - + // Validate frame sizes size_t expectedFrameSize = width * height * 3; for (const auto& frame : frames) { @@ -304,6 +372,37 @@ public: return true; } + // New overload for frame objects + static bool saveAVI(const std::string& filename, + const std::vector& frames, + float fps = 30.0f) { + if (frames.empty() || fps <= 0) { + return false; + } + + // Validate that all frames have the same dimensions + int width = static_cast(frames[0].width()); + int height = static_cast(frames[0].height()); + + for (const auto& frm : frames) { + if (frm.width() != static_cast(width) || + frm.height() != static_cast(height)) { + return false; + } + } + + // Convert frames to RGB format + std::vector> rgbFrames; + rgbFrames.reserve(frames.size()); + + for (const auto& frm : frames) { + rgbFrames.push_back(frameToRGB(frm)); + } + + // Use the existing implementation + return saveAVI(filename, rgbFrames, width, height, fps); + } + // Convenience function to save from individual frame files static bool saveAVIFromFrames(const std::string& filename, const std::vector& frameFiles, diff --git a/util/output/frame.hpp b/util/output/frame.hpp new file mode 100644 index 0000000..beba17a --- /dev/null +++ b/util/output/frame.hpp @@ -0,0 +1,376 @@ +#ifndef FRAME_HPP +#define FRAME_HPP + +#include +#include +#include +#include +#include +#include +#include + +class frame { +private: + std::vector data_; + size_t width_; + size_t height_; + std::vector channels_; + + void validate_dimensions() const { + size_t expected_size = width_ * height_ * channels_.size(); + if (data_.size() != expected_size) { + throw std::invalid_argument("Data size does not match dimensions"); + } + if (width_ == 0 || height_ == 0) { + throw std::invalid_argument("Dimensions must be positive"); + } + if (channels_.empty()) { + throw std::invalid_argument("Channels list cannot be empty"); + } + } +public: + // Default constructor + frame() : width_(0), height_(0) {} + + // Constructor with dimensions and channel names + frame(size_t width, size_t height, const std::vector& channels = {'\0'}) + : width_(width), height_(height), channels_(channels) { + if (width == 0 || height == 0) { + throw std::invalid_argument("Dimensions must be positive"); + } + if (channels.empty()) { + throw std::invalid_argument("Channels list cannot be empty"); + } + data_.resize(width * height * channels.size()); + } + + // Constructor with initializer list for channels + frame(size_t width, size_t height, std::initializer_list channels) + : width_(width), height_(height), channels_(channels) { + if (width == 0 || height == 0) { + throw std::invalid_argument("Dimensions must be positive"); + } + if (channels.size() == 0) { + throw std::invalid_argument("Channels list cannot be empty"); + } + data_.resize(width * height * channels.size()); + } + + // Constructor with existing data + frame(const std::vector& data, size_t width, size_t height, + const std::vector& channels = {'\0'}) + : data_(data), width_(width), height_(height), channels_(channels) { + validate_dimensions(); + } + + // Move constructor with data + frame(std::vector&& data, size_t width, size_t height, + const std::vector& channels = {'\0'}) + : data_(std::move(data)), width_(width), height_(height), channels_(channels) { + validate_dimensions(); + } + + // Copy constructor + frame(const frame&) = default; + + // Move constructor + frame(frame&&) = default; + + // Copy assignment + frame& operator=(const frame&) = default; + + // Move assignment + frame& operator=(frame&&) = default; + + // Accessors + size_t width() const noexcept { return width_; } + size_t height() const noexcept { return height_; } + const std::vector& channels() const noexcept { return channels_; } + size_t channels_count() const noexcept { return channels_.size(); } + size_t size() const noexcept { return data_.size(); } + size_t total_pixels() const noexcept { return width_ * height_; } + + // Data access + const std::vector& data() const noexcept { return data_; } + std::vector& data() noexcept { return data_; } + + // Raw pointer access (const and non-const) + const uint8_t* raw_data() const noexcept { return data_.data(); } + uint8_t* raw_data() noexcept { return data_.data(); } + + // Pixel access by channel index + uint8_t& at(size_t row, size_t col, size_t channel_idx) { + if (row >= height_ || col >= width_ || channel_idx >= channels_.size()) { + throw std::out_of_range("Pixel coordinates or channel index out of range"); + } + return data_[(row * width_ + col) * channels_.size() + channel_idx]; + } + + const uint8_t& at(size_t row, size_t col, size_t channel_idx) const { + if (row >= height_ || col >= width_ || channel_idx >= channels_.size()) { + throw std::out_of_range("Pixel coordinates or channel index out of range"); + } + return data_[(row * width_ + col) * channels_.size() + channel_idx]; + } + + // Pixel access by channel name (returns first occurrence) + uint8_t& at(size_t row, size_t col, char channel_name) { + return at(row, col, get_channel_index(channel_name)); + } + + const uint8_t& at(size_t row, size_t col, char channel_name) const { + return at(row, col, get_channel_index(channel_name)); + } + + // Get channel index by name (returns first occurrence) + size_t get_channel_index(char channel_name) const { + for (size_t i = 0; i < channels_.size(); ++i) { + if (channels_[i] == channel_name) { + return i; + } + } + throw std::out_of_range("Channel name not found: " + std::string(1, channel_name)); + } + + // Check if channel exists + bool has_channel(char channel_name) const { + for (char c : channels_) { + if (c == channel_name) { + return true; + } + } + return false; + } + + // Get all values for a specific channel across the image + std::vector get_channel_data(char channel_name) const { + size_t channel_idx = get_channel_index(channel_name); + std::vector result(total_pixels()); + size_t pixel_count = total_pixels(); + size_t channel_count = channels_.size(); + + for (size_t i = 0; i < pixel_count; ++i) { + result[i] = data_[i * channel_count + channel_idx]; + } + return result; + } + + // Set all values for a specific channel across the image + void set_channel_data(char channel_name, const std::vector& channel_data) { + if (channel_data.size() != total_pixels()) { + throw std::invalid_argument("Channel data size does not match image dimensions"); + } + + size_t channel_idx = get_channel_index(channel_name); + size_t pixel_count = total_pixels(); + size_t channel_count = channels_.size(); + + for (size_t i = 0; i < pixel_count; ++i) { + data_[i * channel_count + channel_idx] = channel_data[i]; + } + } + + // Check if frame is valid/initialized + bool empty() const noexcept { + return width_ == 0 || height_ == 0 || data_.empty(); + } + + // Resize the frame (clears existing data) + void resize(size_t width, size_t height, const std::vector& channels = {'\0'}) { + if (width == 0 || height == 0) { + throw std::invalid_argument("Dimensions must be positive"); + } + if (channels.empty()) { + throw std::invalid_argument("Channels list cannot be empty"); + } + width_ = width; + height_ = height; + channels_ = channels; + data_.resize(width * height * channels.size()); + } + + // Resize with initializer list for channels + void resize(size_t width, size_t height, std::initializer_list channels) { + resize(width, height, std::vector(channels)); + } + + // Change channel names (must maintain same number of channels) + void set_channels(const std::vector& new_channels) { + if (new_channels.size() != channels_.size()) { + throw std::invalid_argument("New channels count must match current channels count"); + } + if (new_channels.empty()) { + throw std::invalid_argument("Channels list cannot be empty"); + } + channels_ = new_channels; + } + + // Clear the frame + void clear() noexcept { + data_.clear(); + width_ = 0; + height_ = 0; + channels_.clear(); + } + + // Swap with another frame + void swap(frame& other) noexcept { + data_.swap(other.data_); + std::swap(width_, other.width_); + std::swap(height_, other.height_); + channels_.swap(other.channels_); + } + + // Create a deep copy + frame clone() const { + return frame(*this); + } + + // Get string representation of channels + std::string channels_string() const { + return std::string(channels_.begin(), channels_.end()); + } + + // RLE Compression - Compress the entire frame data + std::vector> compress_rle() const { + if (empty()) { + return {}; + } + + std::vector> compressed; + + if (data_.empty()) { + return compressed; + } + + uint8_t current_value = data_[0]; + uint32_t count = 1; + + for (size_t i = 1; i < data_.size(); ++i) { + if (data_[i] == current_value && count < UINT32_MAX) { + ++count; + } else { + compressed.emplace_back(current_value, count); + current_value = data_[i]; + count = 1; + } + } + + // Add the last run + compressed.emplace_back(current_value, count); + + return compressed; + } + + // RLE Compression for a specific channel + std::vector> compress_channel_rle(char channel_name) const { + if (empty()) { + return {}; + } + + std::vector channel_data = get_channel_data(channel_name); + std::vector> compressed; + + if (channel_data.empty()) { + return compressed; + } + + uint8_t current_value = channel_data[0]; + uint32_t count = 1; + + for (size_t i = 1; i < channel_data.size(); ++i) { + if (channel_data[i] == current_value && count < UINT32_MAX) { + ++count; + } else { + compressed.emplace_back(current_value, count); + current_value = channel_data[i]; + count = 1; + } + } + + // Add the last run + compressed.emplace_back(current_value, count); + + return compressed; + } + + // RLE Decompression - Decompress RLE data into this frame + void decompress_rle(const std::vector>& compressed_data) { + if (compressed_data.empty()) { + clear(); + return; + } + + // Calculate total size from compressed data + size_t total_size = 0; + for (const auto& run : compressed_data) { + total_size += run.second; + } + + // Resize data vector to accommodate decompressed data + data_.resize(total_size); + + // Decompress the data + size_t index = 0; + for (const auto& run : compressed_data) { + for (uint32_t i = 0; i < run.second; ++i) { + if (index < data_.size()) { + data_[index++] = run.first; + } + } + } + + // Note: After RLE decompression, width_, height_, and channels_ might not be valid + // The user should set these appropriately after decompression + } + + // Static method to create frame from RLE compressed data with known dimensions + static frame from_rle(const std::vector>& compressed_data, + size_t width, size_t height, + const std::vector& channels = {'\0'}) { + frame result; + result.decompress_rle(compressed_data); + + // Validate that decompressed data size matches expected dimensions + size_t expected_size = width * height * channels.size(); + if (result.data_.size() != expected_size) { + throw std::invalid_argument("Decompressed data size does not match provided dimensions"); + } + + result.width_ = width; + result.height_ = height; + result.channels_ = channels; + + return result; + } + + // Calculate compression ratio + double get_compression_ratio() const { + if (empty()) { + return 1.0; + } + + auto compressed = compress_rle(); + if (compressed.empty()) { + return 1.0; + } + + size_t original_size = data_.size(); + size_t compressed_size = compressed.size() * sizeof(std::pair); + + return static_cast(original_size) / compressed_size; + } + + // Get size of compressed data in bytes + size_t get_compressed_size() const { + auto compressed = compress_rle(); + return compressed.size() * sizeof(std::pair); + } + + // Check if compression would be beneficial (ratio > 1.0) + bool would_benefit_from_compression() const { + return get_compression_ratio() > 1.0; + } +}; + +#endif \ No newline at end of file diff --git a/util/output/video.hpp b/util/output/video.hpp new file mode 100644 index 0000000..147bee2 --- /dev/null +++ b/util/output/video.hpp @@ -0,0 +1,458 @@ +#ifndef VIDEO_HPP +#define VIDEO_HPP + +#include "frame.hpp" +#include +#include +#include +#include +#include +#include + +class video { +private: + std::vector>> compressed_frames_; + size_t width_; + size_t height_; + std::vector channels_; + double fps_; + bool use_differential_encoding_; + + // Compress frame using differential encoding + std::vector> compress_with_differential( + const frame& current_frame, const frame* previous_frame = nullptr) const { + + if (previous_frame == nullptr) { + // First frame - compress normally + return current_frame.compress_rle(); + } + + // Create differential frame + std::vector diff_data(current_frame.size()); + + const std::vector& current_data = current_frame.data(); + const std::vector& prev_data = previous_frame->data(); + + // Calculate difference between frames + for (size_t i = 0; i < current_data.size(); ++i) { + // Use modulo arithmetic to handle unsigned byte overflow + diff_data[i] = (current_data[i] - prev_data[i]) & 0xFF; + } + + // Create temporary frame for differential data + frame diff_frame(diff_data, width_, height_, channels_); + + // Compress the differential data + return diff_frame.compress_rle(); + } + + // Decompress differential frame + frame decompress_differential(const std::vector>& compressed_diff, + const frame& previous_frame) const { + + frame diff_frame; + diff_frame.decompress_rle(compressed_diff); + + // Reconstruct original frame from differential + std::vector reconstructed_data(diff_frame.size()); + const std::vector& diff_data = diff_frame.data(); + const std::vector& prev_data = previous_frame.data(); + + for (size_t i = 0; i < diff_data.size(); ++i) { + // Reverse the differential encoding + reconstructed_data[i] = (prev_data[i] + diff_data[i]) & 0xFF; + } + + return frame(reconstructed_data, width_, height_, channels_); + } + +public: + // Default constructor + video() : width_(0), height_(0), fps_(30.0), use_differential_encoding_(true) {} + + // Constructor with dimensions and settings + video(size_t width, size_t height, const std::vector& channels = {'\0'}, + double fps = 30.0, bool use_differential = true) + : width_(width), height_(height), channels_(channels), fps_(fps), + use_differential_encoding_(use_differential) { + + if (width == 0 || height == 0) { + throw std::invalid_argument("Dimensions must be positive"); + } + if (channels.empty()) { + throw std::invalid_argument("Channels list cannot be empty"); + } + if (fps <= 0) { + throw std::invalid_argument("FPS must be positive"); + } + } + + // Constructor with initializer list for channels + video(size_t width, size_t height, std::initializer_list channels, + double fps = 30.0, bool use_differential = true) + : video(width, height, std::vector(channels), fps, use_differential) {} + + // Accessors + size_t width() const noexcept { return width_; } + size_t height() const noexcept { return height_; } + const std::vector& channels() const noexcept { return channels_; } + double fps() const noexcept { return fps_; } + bool use_differential_encoding() const noexcept { return use_differential_encoding_; } + size_t frame_count() const noexcept { return compressed_frames_.size(); } + size_t channels_count() const noexcept { return channels_.size(); } + + // Check if video is empty + bool empty() const noexcept { + return compressed_frames_.empty() || width_ == 0 || height_ == 0; + } + + // Add a frame to the video sequence + void add_frame(const frame& new_frame) { + // Validate frame dimensions and channels + if (new_frame.width() != width_ || new_frame.height() != height_) { + throw std::invalid_argument("Frame dimensions must match video dimensions"); + } + if (new_frame.channels() != channels_) { + throw std::invalid_argument("Frame channels must match video channels"); + } + + if (compressed_frames_.empty() || !use_differential_encoding_) { + // First frame or differential encoding disabled - compress normally + compressed_frames_.push_back(new_frame.compress_rle()); + } else { + // Get the previous frame for differential encoding + frame prev_frame = get_frame(frame_count() - 1); + compressed_frames_.push_back(compress_with_differential(new_frame, &prev_frame)); + } + } + + // Add frame with move semantics + void add_frame(frame&& new_frame) { + add_frame(new_frame); // Just call the const version + } + + // Get a specific frame + frame get_frame(size_t index) const { + if (index >= compressed_frames_.size()) { + throw std::out_of_range("Frame index out of range"); + } + + if (index == 0 || !use_differential_encoding_) { + // First frame or no differential encoding - decompress normally + frame result; + result.decompress_rle(compressed_frames_[index]); + + // Set dimensions and channels + result.resize(width_, height_, channels_); + return result; + } else { + // Differential encoded frame - need previous frame to reconstruct + frame prev_frame = get_frame(index - 1); + return decompress_differential(compressed_frames_[index], prev_frame); + } + } + + // Get multiple frames as a sequence + std::vector get_frames(size_t start_index, size_t count) const { + if (start_index >= compressed_frames_.size()) { + throw std::out_of_range("Start index out of range"); + } + + count = std::min(count, compressed_frames_.size() - start_index); + std::vector frames; + frames.reserve(count); + + for (size_t i = start_index; i < start_index + count; ++i) { + frames.push_back(get_frame(i)); + } + + return frames; + } + + // Get all frames + std::vector get_all_frames() const { + return get_frames(0, compressed_frames_.size()); + } + + // Remove a frame + void remove_frame(size_t index) { + if (index >= compressed_frames_.size()) { + throw std::out_of_range("Frame index out of range"); + } + compressed_frames_.erase(compressed_frames_.begin() + index); + } + + // Clear all frames + void clear_frames() noexcept { + compressed_frames_.clear(); + } + + // Replace a frame + void replace_frame(size_t index, const frame& new_frame) { + if (index >= compressed_frames_.size()) { + throw std::out_of_range("Frame index out of range"); + } + + // Validate frame dimensions and channels + if (new_frame.width() != width_ || new_frame.height() != height_) { + throw std::invalid_argument("Frame dimensions must match video dimensions"); + } + if (new_frame.channels() != channels_) { + throw std::invalid_argument("Frame channels must match video channels"); + } + + if (index == 0 || !use_differential_encoding_) { + compressed_frames_[index] = new_frame.compress_rle(); + } else { + frame prev_frame = get_frame(index - 1); + compressed_frames_[index] = compress_with_differential(new_frame, &prev_frame); + } + + // If this isn't the last frame, we need to update the next frame's differential encoding + if (use_differential_encoding_ && index + 1 < compressed_frames_.size()) { + frame current_frame = get_frame(index); + frame next_frame_original = get_frame(index + 1); + compressed_frames_[index + 1] = compress_with_differential(next_frame_original, ¤t_frame); + } + } + + // Set FPS + void set_fps(double fps) { + if (fps <= 0) { + throw std::invalid_argument("FPS must be positive"); + } + fps_ = fps; + } + + // Enable/disable differential encoding + void set_differential_encoding(bool enabled) { + if (use_differential_encoding_ == enabled) { + return; // No change needed + } + + if (!compressed_frames_.empty() && enabled != use_differential_encoding_) { + // Need to recompress all frames with new encoding setting + auto original_frames = get_all_frames(); + clear_frames(); + use_differential_encoding_ = enabled; + + for (const auto& f : original_frames) { + add_frame(f); + } + } else { + use_differential_encoding_ = enabled; + } + } + + // Get video duration in seconds + double duration() const noexcept { + return compressed_frames_.size() / fps_; + } + + // Calculate total compressed size in bytes + size_t total_compressed_size() const noexcept { + size_t total = 0; + for (const auto& compressed_frame : compressed_frames_) { + total += compressed_frame.size() * sizeof(std::pair); + } + return total; + } + + // Calculate total uncompressed size in bytes + size_t total_uncompressed_size() const noexcept { + return compressed_frames_.size() * width_ * height_ * channels_.size(); + } + + // Calculate overall compression ratio + double overall_compression_ratio() const noexcept { + if (empty()) { + return 1.0; + } + size_t uncompressed = total_uncompressed_size(); + if (uncompressed == 0) { + return 1.0; + } + return static_cast(uncompressed) / total_compressed_size(); + } + + // Calculate average frame compression ratio + double average_frame_compression_ratio() const { + if (empty()) { + return 1.0; + } + + double total_ratio = 0.0; + for (size_t i = 0; i < compressed_frames_.size(); ++i) { + frame f = get_frame(i); + total_ratio += f.get_compression_ratio(); + } + + return total_ratio / compressed_frames_.size(); + } + + // Get compression statistics + struct compression_stats { + size_t total_frames; + size_t total_compressed_bytes; + size_t total_uncompressed_bytes; + double overall_ratio; + double average_frame_ratio; + double video_duration; + }; + + compression_stats get_compression_stats() const { + compression_stats stats; + stats.total_frames = compressed_frames_.size(); + stats.total_compressed_bytes = total_compressed_size(); + stats.total_uncompressed_bytes = total_uncompressed_size(); + stats.overall_ratio = overall_compression_ratio(); + stats.average_frame_ratio = average_frame_compression_ratio(); + stats.video_duration = duration(); + return stats; + } + + // Extract a sub-video + video subvideo(size_t start_frame, size_t frame_count) const { + if (start_frame >= compressed_frames_.size()) { + throw std::out_of_range("Start frame out of range"); + } + + frame_count = std::min(frame_count, compressed_frames_.size() - start_frame); + video result(width_, height_, channels_, fps_, use_differential_encoding_); + + for (size_t i = start_frame; i < start_frame + frame_count; ++i) { + result.compressed_frames_.push_back(compressed_frames_[i]); + } + + return result; + } + + // Append another video (must have same dimensions and channels) + void append_video(const video& other) { + if (other.width_ != width_ || other.height_ != height_ || other.channels_ != channels_) { + throw std::invalid_argument("Videos must have same dimensions and channels"); + } + + // If both use differential encoding, we can directly append compressed frames + if (use_differential_encoding_ && other.use_differential_encoding_) { + compressed_frames_.insert(compressed_frames_.end(), + other.compressed_frames_.begin(), + other.compressed_frames_.end()); + } else { + // Otherwise, we need to decompress and recompress + auto other_frames = other.get_all_frames(); + for (const auto& frame : other_frames) { + add_frame(frame); + } + } + } + + // Save/Load functionality (basic serialization) + std::vector serialize() const { + // Simple serialization format: + // [header][compressed_frame_data...] + // Header: width(4), height(4), channels_count(1), channels_data(n), fps(8), frame_count(4) + + std::vector result; + + // Header + auto add_uint32 = [&result](uint32_t value) { + for (int i = 0; i < 4; ++i) { + result.push_back((value >> (i * 8)) & 0xFF); + } + }; + + auto add_double = [&result](double value) { + const uint8_t* bytes = reinterpret_cast(&value); + for (size_t i = 0; i < sizeof(double); ++i) { + result.push_back(bytes[i]); + } + }; + + // Write header + add_uint32(static_cast(width_)); + add_uint32(static_cast(height_)); + result.push_back(static_cast(channels_.size())); + for (char c : channels_) { + result.push_back(static_cast(c)); + } + add_double(fps_); + result.push_back(use_differential_encoding_ ? 1 : 0); + add_uint32(static_cast(compressed_frames_.size())); + + // Write compressed frames + for (const auto& compressed_frame : compressed_frames_) { + add_uint32(static_cast(compressed_frame.size())); + for (const auto& run : compressed_frame) { + result.push_back(run.first); + add_uint32(run.second); + } + } + + return result; + } + + // Deserialize from byte data + static video deserialize(const std::vector& data) { + if (data.size() < 4 + 4 + 1 + 8 + 1 + 4) { // Minimum header size + throw std::invalid_argument("Invalid video data: too short"); + } + + size_t pos = 0; + auto read_uint32 = [&data, &pos]() { + if (pos + 4 > data.size()) throw std::invalid_argument("Unexpected end of data"); + uint32_t value = 0; + for (int i = 0; i < 4; ++i) { + value |= static_cast(data[pos++]) << (i * 8); + } + return value; + }; + + auto read_double = [&data, &pos]() { + if (pos + sizeof(double) > data.size()) throw std::invalid_argument("Unexpected end of data"); + double value; + uint8_t* bytes = reinterpret_cast(&value); + for (size_t i = 0; i < sizeof(double); ++i) { + bytes[i] = data[pos++]; + } + return value; + }; + + // Read header + uint32_t width = read_uint32(); + uint32_t height = read_uint32(); + uint8_t channels_count = data[pos++]; + + std::vector channels; + for (uint8_t i = 0; i < channels_count; ++i) { + if (pos >= data.size()) throw std::invalid_argument("Unexpected end of data"); + channels.push_back(static_cast(data[pos++])); + } + + double fps = read_double(); + bool use_diff = data[pos++] != 0; + uint32_t frame_count = read_uint32(); + + video result(width, height, channels, fps, use_diff); + + // Read compressed frames + for (uint32_t i = 0; i < frame_count; ++i) { + if (pos + 4 > data.size()) throw std::invalid_argument("Unexpected end of data"); + uint32_t runs_count = read_uint32(); + + std::vector> compressed_frame; + for (uint32_t j = 0; j < runs_count; ++j) { + if (pos + 5 > data.size()) throw std::invalid_argument("Unexpected end of data"); + uint8_t value = data[pos++]; + uint32_t count = read_uint32(); + compressed_frame.emplace_back(value, count); + } + + result.compressed_frames_.push_back(compressed_frame); + } + + return result; + } +}; + +#endif \ No newline at end of file