spatial grid is annoying. also added frame storage
This commit is contained in:
@@ -9,10 +9,11 @@
|
|||||||
#include "../util/output/aviwriter.hpp"
|
#include "../util/output/aviwriter.hpp"
|
||||||
#include "../util/output/bmpwriter.hpp"
|
#include "../util/output/bmpwriter.hpp"
|
||||||
#include "../util/timing_decorator.cpp"
|
#include "../util/timing_decorator.cpp"
|
||||||
|
#include "../util/output/frame.hpp"
|
||||||
|
|
||||||
struct AnimationConfig {
|
struct AnimationConfig {
|
||||||
int width = 256;
|
int width = 1024;
|
||||||
int height = 256;
|
int height = 1024;
|
||||||
int totalFrames = 480;
|
int totalFrames = 480;
|
||||||
float fps = 30.0f;
|
float fps = 30.0f;
|
||||||
int numSeeds = 8;
|
int numSeeds = 8;
|
||||||
@@ -149,22 +150,33 @@ int main() {
|
|||||||
AnimationConfig config;
|
AnimationConfig config;
|
||||||
|
|
||||||
Grid2 grid = setup(config);
|
Grid2 grid = setup(config);
|
||||||
//grid.updateNeighborMap();
|
|
||||||
Preview(grid);
|
Preview(grid);
|
||||||
std::vector<std::tuple<size_t, Vec2, Vec4>> seeds = pickSeeds(grid,config);
|
std::vector<std::tuple<size_t, Vec2, Vec4>> seeds = pickSeeds(grid,config);
|
||||||
std::vector<std::vector<uint8_t>> frames;
|
std::vector<frame> frames; // Change to vector of frame objects
|
||||||
|
|
||||||
for (int i = 0; i < config.totalFrames; ++i){
|
for (int i = 0; i < config.totalFrames; ++i){
|
||||||
std::cout << "Processing frame " << i + 1 << "/" << config.totalFrames << std::endl;
|
std::cout << "Processing frame " << i + 1 << "/" << config.totalFrames << std::endl;
|
||||||
expandPixel(grid,config,seeds);
|
expandPixel(grid,config,seeds);
|
||||||
int width;
|
|
||||||
int height;
|
frame outputFrame;
|
||||||
std::vector<uint8_t> frame;
|
grid.getGridAsFrame(outputFrame, {'B', 'G', 'R'}); // Directly get as BGR frame
|
||||||
grid.getGridAsBGR(width,height,frame);
|
|
||||||
frames.push_back(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);
|
FunctionTimer::printStats(FunctionTimer::Mode::ENHANCED);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "../vectorlogic/vec2.hpp"
|
#include "../vectorlogic/vec2.hpp"
|
||||||
#include "../vectorlogic/vec4.hpp"
|
#include "../vectorlogic/vec4.hpp"
|
||||||
#include "../timing_decorator.hpp"
|
#include "../timing_decorator.hpp"
|
||||||
|
#include "../output/frame.hpp"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#ifndef GRID2_HPP
|
#ifndef GRID2_HPP
|
||||||
@@ -53,7 +54,7 @@ public:
|
|||||||
ƨnoiƚiƨoꟼ.reserve(size);
|
ƨnoiƚiƨoꟼ.reserve(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t size() {
|
size_t size() const {
|
||||||
return Positions.size();
|
return Positions.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +103,8 @@ public:
|
|||||||
class SpatialGrid {
|
class SpatialGrid {
|
||||||
private:
|
private:
|
||||||
float cellSize;
|
float cellSize;
|
||||||
std::unordered_map<Vec2, std::unordered_set<size_t>, Vec2::Hash> grid;
|
|
||||||
public:
|
public:
|
||||||
|
std::unordered_map<Vec2, std::unordered_set<size_t>, Vec2::Hash> grid;
|
||||||
SpatialGrid(float cellSize = 2.0f) : cellSize(cellSize) {}
|
SpatialGrid(float cellSize = 2.0f) : cellSize(cellSize) {}
|
||||||
|
|
||||||
Vec2 worldToGrid(const Vec2& worldPos) const {
|
Vec2 worldToGrid(const Vec2& worldPos) const {
|
||||||
@@ -186,8 +187,8 @@ private:
|
|||||||
float neighborRadius = 1.0f;
|
float neighborRadius = 1.0f;
|
||||||
|
|
||||||
//TODO: spatial map
|
//TODO: spatial map
|
||||||
std::unordered_map<Vec2, reverselookupassistantclasscausecppisdumb, Vec2::Hash> spatialTable;
|
SpatialGrid spatialGrid;
|
||||||
float spatSize = 2.0f;
|
float spatialCellSize = 2.0f;
|
||||||
public:
|
public:
|
||||||
//get position from id
|
//get position from id
|
||||||
Vec2 getPositionID(size_t id) const {
|
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)
|
//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) {
|
size_t getPositionVec(const Vec2& pos, float radius = 0.0f) {
|
||||||
auto it = Positions.at(pos);
|
if (radius == 0.0f) {
|
||||||
return it;
|
// 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) {
|
size_t getPositionVec(float x, float y, float radius = 0.0f) {
|
||||||
return getPositionVec(Vec2(x,y), radius);
|
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
|
//get all id in region
|
||||||
std::vector<size_t> getPositionVecRegion(Vec2 pos, float radius = 1.0f) {
|
std::vector<size_t> getPositionVecRegion(const Vec2& pos, float radius = 1.0f) {
|
||||||
|
TIME_FUNCTION;
|
||||||
float searchRadius = (radius == 0.0f) ? std::numeric_limits<float>::epsilon() : radius;
|
float searchRadius = (radius == 0.0f) ? std::numeric_limits<float>::epsilon() : radius;
|
||||||
|
|
||||||
float radiusSq = searchRadius*searchRadius;
|
// Get candidates from spatial grid
|
||||||
std::vector<size_t> posvec;
|
std::vector<size_t> candidates = spatialGrid.queryRange(pos, searchRadius);
|
||||||
for (const auto& pair : Positions) {
|
|
||||||
if (pair.second.distanceSquared(pos) <= radiusSq) {
|
// Fine-filter by exact distance
|
||||||
posvec.push_back(pair.first);
|
std::vector<size_t> 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
|
//get color from id
|
||||||
@@ -252,18 +271,27 @@ public:
|
|||||||
size_t id = Positions.set(pos);
|
size_t id = Positions.set(pos);
|
||||||
Colors[id] = color;
|
Colors[id] = color;
|
||||||
Sizes[id] = size;
|
Sizes[id] = size;
|
||||||
return id;
|
|
||||||
|
// Add to spatial grid
|
||||||
|
spatialGrid.insert(id, pos);
|
||||||
updateNeighborForID(id);
|
updateNeighborForID(id);
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
//set position by id
|
//set position by id
|
||||||
void setPosition(size_t id, const Vec2& position) {
|
void setPosition(size_t id, const Vec2& newPosition) {
|
||||||
Positions.at(id).move(position);
|
Vec2 oldPosition = Positions.at(id);
|
||||||
|
spatialGrid.update(id, oldPosition, newPosition);
|
||||||
|
Positions.at(id).move(newPosition);
|
||||||
updateNeighborForID(id);
|
updateNeighborForID(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPosition(size_t id, float x, float y) {
|
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);
|
updateNeighborForID(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,10 +341,12 @@ public:
|
|||||||
|
|
||||||
//remove object (should remove the id, the color, the position, and the size)
|
//remove object (should remove the id, the color, the position, and the size)
|
||||||
size_t removeID(size_t id) {
|
size_t removeID(size_t id) {
|
||||||
|
Vec2 oldPosition = Positions.at(id);
|
||||||
Positions.remove(id);
|
Positions.remove(id);
|
||||||
Colors.erase(id);
|
Colors.erase(id);
|
||||||
Sizes.erase(id);
|
Sizes.erase(id);
|
||||||
unassignedIDs.push_back(id);
|
unassignedIDs.push_back(id);
|
||||||
|
spatialGrid.remove(id, oldPosition);
|
||||||
updateNeighborForID(id);
|
updateNeighborForID(id);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -327,6 +357,7 @@ public:
|
|||||||
Colors.erase(id);
|
Colors.erase(id);
|
||||||
Sizes.erase(id);
|
Sizes.erase(id);
|
||||||
unassignedIDs.push_back(id);
|
unassignedIDs.push_back(id);
|
||||||
|
spatialGrid.remove(id, pos);
|
||||||
updateNeighborForID(id);
|
updateNeighborForID(id);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -335,8 +366,9 @@ public:
|
|||||||
void bulkUpdatePositions(const std::unordered_map<size_t, Vec2>& newPositions) {
|
void bulkUpdatePositions(const std::unordered_map<size_t, Vec2>& newPositions) {
|
||||||
TIME_FUNCTION;
|
TIME_FUNCTION;
|
||||||
for (const auto& [id, newPos] : newPositions) {
|
for (const auto& [id, newPos] : newPositions) {
|
||||||
auto it = Positions.at(id);
|
Vec2 oldPosition = Positions.at(id);
|
||||||
it.move(newPos);
|
Positions.at(id).move(newPos);
|
||||||
|
spatialGrid.update(id, oldPosition, newPos);
|
||||||
}
|
}
|
||||||
updateNeighborMap();
|
updateNeighborMap();
|
||||||
}
|
}
|
||||||
@@ -379,12 +411,14 @@ public:
|
|||||||
Sizes.reserve(Sizes.size() + objects.size());
|
Sizes.reserve(Sizes.size() + objects.size());
|
||||||
|
|
||||||
// Batch insertion
|
// Batch insertion
|
||||||
|
#pragma omp parallel for
|
||||||
for (size_t i = 0; i < objects.size(); ++i) {
|
for (size_t i = 0; i < objects.size(); ++i) {
|
||||||
const auto& [pos, color, size] = objects[i];
|
const auto& [pos, color, size] = objects[i];
|
||||||
size_t id = Positions.set(pos);
|
size_t id = Positions.set(pos);
|
||||||
|
|
||||||
Colors[id] = color;
|
Colors[id] = color;
|
||||||
Sizes[id] = size;
|
Sizes[id] = size;
|
||||||
|
spatialGrid.insert(id,pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
shrinkIfNeeded();
|
shrinkIfNeeded();
|
||||||
@@ -410,6 +444,7 @@ public:
|
|||||||
size_t id = Positions.set(poses[i]);
|
size_t id = Positions.set(poses[i]);
|
||||||
Colors[id] = colors[i];
|
Colors[id] = colors[i];
|
||||||
Sizes[id] = sizes[i];
|
Sizes[id] = sizes[i];
|
||||||
|
spatialGrid.insert(id,poses[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
shrinkIfNeeded();
|
shrinkIfNeeded();
|
||||||
@@ -527,6 +562,88 @@ public:
|
|||||||
getGridRegionAsBGR(minCorner, maxCorner, width, height, bgrData);
|
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<char>& channels = {'R', 'G', 'B'}) const {
|
||||||
|
TIME_FUNCTION;
|
||||||
|
// Calculate dimensions
|
||||||
|
width = static_cast<int>(maxCorner.x - minCorner.x);
|
||||||
|
height = static_cast<int>(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<int>(pos.x - minCorner.x);
|
||||||
|
int pixelY = static_cast<int>(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<float>(id) / Positions.size(); break; // Normalized ID
|
||||||
|
default: value = 0.0f; break;
|
||||||
|
}
|
||||||
|
outputFrame.at(pixelY, pixelX, channel_idx) = static_cast<uint8_t>(value * 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get full grid as frame
|
||||||
|
void getGridAsFrame(frame& outputFrame, const std::vector<char>& 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
|
//get bounding box
|
||||||
void getBoundingBox(Vec2& minCorner, Vec2& maxCorner) {
|
void getBoundingBox(Vec2& minCorner, Vec2& maxCorner) {
|
||||||
TIME_FUNCTION;
|
TIME_FUNCTION;
|
||||||
@@ -562,6 +679,8 @@ public:
|
|||||||
Positions.clear();
|
Positions.clear();
|
||||||
Colors.clear();
|
Colors.clear();
|
||||||
Sizes.clear();
|
Sizes.clear();
|
||||||
|
spatialGrid.clear();
|
||||||
|
neighborMap.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// neighbor map
|
// neighbor map
|
||||||
@@ -573,9 +692,10 @@ public:
|
|||||||
for (const auto& [id1, pos1] : Positions) {
|
for (const auto& [id1, pos1] : Positions) {
|
||||||
std::vector<size_t> neighbors;
|
std::vector<size_t> neighbors;
|
||||||
float radiusSq = neighborRadius * neighborRadius;
|
float radiusSq = neighborRadius * neighborRadius;
|
||||||
|
auto candidate_ids = spatialGrid.queryRange(pos1, neighborRadius);
|
||||||
|
|
||||||
for (const auto& [id2, pos2] : Positions) {
|
for (size_t id2 : candidate_ids) {
|
||||||
if (id1 != id2 && pos1.distanceSquared(pos2) <= radiusSq) {
|
if (id1 != id2 && Positions.at(id1).distanceSquared(Positions.at(id2)) <= radiusSq) {
|
||||||
neighbors.push_back(id2);
|
neighbors.push_back(id2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include "frame.hpp"
|
||||||
|
#include "video.hpp"
|
||||||
|
|
||||||
class AVIWriter {
|
class AVIWriter {
|
||||||
private:
|
private:
|
||||||
@@ -110,7 +112,73 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to convert frame to RGB format
|
||||||
|
static std::vector<uint8_t> frameToRGB(const frame& frm) {
|
||||||
|
if (frm.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t width = frm.width();
|
||||||
|
size_t height = frm.height();
|
||||||
|
std::vector<uint8_t> 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<uint8_t> rChannel = frm.has_channel('R') ?
|
||||||
|
frm.get_channel_data('R') : frm.get_channel_data('r');
|
||||||
|
std::vector<uint8_t> gChannel = frm.has_channel('G') ?
|
||||||
|
frm.get_channel_data('G') : frm.get_channel_data('g');
|
||||||
|
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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:
|
public:
|
||||||
|
// Original method for vector of raw frame data
|
||||||
static bool saveAVI(const std::string& filename,
|
static bool saveAVI(const std::string& filename,
|
||||||
const std::vector<std::vector<uint8_t>>& frames,
|
const std::vector<std::vector<uint8_t>>& frames,
|
||||||
int width, int height, float fps = 30.0f) {
|
int width, int height, float fps = 30.0f) {
|
||||||
@@ -304,6 +372,37 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New overload for frame objects
|
||||||
|
static bool saveAVI(const std::string& filename,
|
||||||
|
const std::vector<frame>& frames,
|
||||||
|
float fps = 30.0f) {
|
||||||
|
if (frames.empty() || fps <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that all frames have the same dimensions
|
||||||
|
int width = static_cast<int>(frames[0].width());
|
||||||
|
int height = static_cast<int>(frames[0].height());
|
||||||
|
|
||||||
|
for (const auto& frm : frames) {
|
||||||
|
if (frm.width() != static_cast<size_t>(width) ||
|
||||||
|
frm.height() != static_cast<size_t>(height)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert frames to RGB format
|
||||||
|
std::vector<std::vector<uint8_t>> 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
|
// Convenience function to save from individual frame files
|
||||||
static bool saveAVIFromFrames(const std::string& filename,
|
static bool saveAVIFromFrames(const std::string& filename,
|
||||||
const std::vector<std::string>& frameFiles,
|
const std::vector<std::string>& frameFiles,
|
||||||
|
|||||||
376
util/output/frame.hpp
Normal file
376
util/output/frame.hpp
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
#ifndef FRAME_HPP
|
||||||
|
#define FRAME_HPP
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
class frame {
|
||||||
|
private:
|
||||||
|
std::vector<uint8_t> data_;
|
||||||
|
size_t width_;
|
||||||
|
size_t height_;
|
||||||
|
std::vector<char> 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<char>& 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<char> 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<uint8_t>& data, size_t width, size_t height,
|
||||||
|
const std::vector<char>& channels = {'\0'})
|
||||||
|
: data_(data), width_(width), height_(height), channels_(channels) {
|
||||||
|
validate_dimensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move constructor with data
|
||||||
|
frame(std::vector<uint8_t>&& data, size_t width, size_t height,
|
||||||
|
const std::vector<char>& 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<char>& 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<uint8_t>& data() const noexcept { return data_; }
|
||||||
|
std::vector<uint8_t>& 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<uint8_t> get_channel_data(char channel_name) const {
|
||||||
|
size_t channel_idx = get_channel_index(channel_name);
|
||||||
|
std::vector<uint8_t> 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<uint8_t>& 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<char>& 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<char> channels) {
|
||||||
|
resize(width, height, std::vector<char>(channels));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change channel names (must maintain same number of channels)
|
||||||
|
void set_channels(const std::vector<char>& 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<std::pair<uint8_t, uint32_t>> compress_rle() const {
|
||||||
|
if (empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<uint8_t, uint32_t>> 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<std::pair<uint8_t, uint32_t>> compress_channel_rle(char channel_name) const {
|
||||||
|
if (empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> channel_data = get_channel_data(channel_name);
|
||||||
|
std::vector<std::pair<uint8_t, uint32_t>> 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<std::pair<uint8_t, uint32_t>>& 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<std::pair<uint8_t, uint32_t>>& compressed_data,
|
||||||
|
size_t width, size_t height,
|
||||||
|
const std::vector<char>& 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<uint8_t, uint32_t>);
|
||||||
|
|
||||||
|
return static_cast<double>(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<uint8_t, uint32_t>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if compression would be beneficial (ratio > 1.0)
|
||||||
|
bool would_benefit_from_compression() const {
|
||||||
|
return get_compression_ratio() > 1.0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
458
util/output/video.hpp
Normal file
458
util/output/video.hpp
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
#ifndef VIDEO_HPP
|
||||||
|
#define VIDEO_HPP
|
||||||
|
|
||||||
|
#include "frame.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <memory>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
class video {
|
||||||
|
private:
|
||||||
|
std::vector<std::vector<std::pair<uint8_t, uint32_t>>> compressed_frames_;
|
||||||
|
size_t width_;
|
||||||
|
size_t height_;
|
||||||
|
std::vector<char> channels_;
|
||||||
|
double fps_;
|
||||||
|
bool use_differential_encoding_;
|
||||||
|
|
||||||
|
// Compress frame using differential encoding
|
||||||
|
std::vector<std::pair<uint8_t, uint32_t>> 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<uint8_t> diff_data(current_frame.size());
|
||||||
|
|
||||||
|
const std::vector<uint8_t>& current_data = current_frame.data();
|
||||||
|
const std::vector<uint8_t>& 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<std::pair<uint8_t, uint32_t>>& compressed_diff,
|
||||||
|
const frame& previous_frame) const {
|
||||||
|
|
||||||
|
frame diff_frame;
|
||||||
|
diff_frame.decompress_rle(compressed_diff);
|
||||||
|
|
||||||
|
// Reconstruct original frame from differential
|
||||||
|
std::vector<uint8_t> reconstructed_data(diff_frame.size());
|
||||||
|
const std::vector<uint8_t>& diff_data = diff_frame.data();
|
||||||
|
const std::vector<uint8_t>& 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<char>& 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<char> channels,
|
||||||
|
double fps = 30.0, bool use_differential = true)
|
||||||
|
: video(width, height, std::vector<char>(channels), fps, use_differential) {}
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
size_t width() const noexcept { return width_; }
|
||||||
|
size_t height() const noexcept { return height_; }
|
||||||
|
const std::vector<char>& 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<frame> 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<frame> 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<frame> 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<uint8_t, uint32_t>);
|
||||||
|
}
|
||||||
|
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<double>(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<uint8_t> 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<uint8_t> 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<const uint8_t*>(&value);
|
||||||
|
for (size_t i = 0; i < sizeof(double); ++i) {
|
||||||
|
result.push_back(bytes[i]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write header
|
||||||
|
add_uint32(static_cast<uint32_t>(width_));
|
||||||
|
add_uint32(static_cast<uint32_t>(height_));
|
||||||
|
result.push_back(static_cast<uint8_t>(channels_.size()));
|
||||||
|
for (char c : channels_) {
|
||||||
|
result.push_back(static_cast<uint8_t>(c));
|
||||||
|
}
|
||||||
|
add_double(fps_);
|
||||||
|
result.push_back(use_differential_encoding_ ? 1 : 0);
|
||||||
|
add_uint32(static_cast<uint32_t>(compressed_frames_.size()));
|
||||||
|
|
||||||
|
// Write compressed frames
|
||||||
|
for (const auto& compressed_frame : compressed_frames_) {
|
||||||
|
add_uint32(static_cast<uint32_t>(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<uint8_t>& 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<uint32_t>(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<uint8_t*>(&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<char> 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<char>(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<std::pair<uint8_t, uint32_t>> 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
|
||||||
Reference in New Issue
Block a user