First pass at framerip
This commit is contained in:
commit
25af5c0079
|
|
@ -0,0 +1 @@
|
|||
build
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
project(video_frame_extractor LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# ── Source files ──────────────────────────────────────────────────────────────
|
||||
|
||||
add_executable(video_frame_extractor
|
||||
main.cpp
|
||||
frame_extractor.cpp
|
||||
tiff_writer.cpp
|
||||
platform_utils.cpp
|
||||
)
|
||||
|
||||
# ── FFmpeg (libav) ────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Strategy (in order of precedence):
|
||||
# 1. pkg-config — works out of the box on most Linux/macOS (Homebrew) setups.
|
||||
# 2. Manual paths via CMake cache variables FFMPEG_INCLUDE_DIR / FFMPEG_LIB_DIR.
|
||||
# Useful on Windows (vcpkg, manual builds, etc.).
|
||||
|
||||
find_package(PkgConfig QUIET)
|
||||
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec)
|
||||
pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat)
|
||||
pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil)
|
||||
pkg_check_modules(SWSCALE REQUIRED IMPORTED_TARGET libswscale)
|
||||
|
||||
target_link_libraries(video_frame_extractor PRIVATE
|
||||
PkgConfig::AVCODEC
|
||||
PkgConfig::AVFORMAT
|
||||
PkgConfig::AVUTIL
|
||||
PkgConfig::SWSCALE
|
||||
)
|
||||
else()
|
||||
# ── Fallback: manual path configuration ──────────────────────────────────
|
||||
# Set these variables when invoking CMake, e.g.:
|
||||
# cmake -DFFMPEG_INCLUDE_DIR=C:/ffmpeg/include \
|
||||
# -DFFMPEG_LIB_DIR=C:/ffmpeg/lib ..
|
||||
|
||||
set(FFMPEG_INCLUDE_DIR "" CACHE PATH "FFmpeg include directory")
|
||||
set(FFMPEG_LIB_DIR "" CACHE PATH "FFmpeg library directory")
|
||||
|
||||
if(NOT FFMPEG_INCLUDE_DIR OR NOT FFMPEG_LIB_DIR)
|
||||
message(FATAL_ERROR
|
||||
"pkg-config not found and FFMPEG_INCLUDE_DIR / FFMPEG_LIB_DIR are not set.\n"
|
||||
"Please install pkg-config or pass the FFmpeg paths manually.")
|
||||
endif()
|
||||
|
||||
target_include_directories(video_frame_extractor PRIVATE "${FFMPEG_INCLUDE_DIR}")
|
||||
|
||||
foreach(_lib avcodec avformat avutil swscale)
|
||||
find_library(_found_${_lib}
|
||||
NAMES ${_lib}
|
||||
PATHS "${FFMPEG_LIB_DIR}"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
if(NOT _found_${_lib})
|
||||
message(FATAL_ERROR "Could not find lib${_lib} in ${FFMPEG_LIB_DIR}")
|
||||
endif()
|
||||
target_link_libraries(video_frame_extractor PRIVATE "${_found_${_lib}}")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# ── Platform-specific adjustments ────────────────────────────────────────────
|
||||
|
||||
if(WIN32)
|
||||
# Suppress MSVC warnings about C runtime functions and enable Unicode APIs
|
||||
target_compile_definitions(video_frame_extractor PRIVATE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
UNICODE
|
||||
_UNICODE
|
||||
)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
# Silence deprecation warnings that FFmpeg headers can trigger on macOS
|
||||
target_compile_options(video_frame_extractor PRIVATE -Wno-deprecated-declarations)
|
||||
endif()
|
||||
|
||||
# ── Install ───────────────────────────────────────────────────────────────────
|
||||
|
||||
install(TARGETS video_frame_extractor RUNTIME DESTINATION bin)
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
# video_frame_extractor
|
||||
|
||||
Extracts every frame from a video file and writes them as uncompressed RGB
|
||||
TIFF images into a temporary directory. Compiles on **macOS**, **Linux**, and
|
||||
**Windows**.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Library | Purpose |
|
||||
|---------|---------|
|
||||
| `libavformat` | Container demuxing |
|
||||
| `libavcodec` | Video decoding |
|
||||
| `libavutil` | FFmpeg utilities |
|
||||
| `libswscale` | Pixel-format conversion |
|
||||
|
||||
All four are part of [FFmpeg](https://ffmpeg.org/). No other external libraries
|
||||
are required — TIFF files are written with a hand-rolled encoder.
|
||||
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
### macOS (Homebrew)
|
||||
|
||||
```bash
|
||||
brew install ffmpeg cmake
|
||||
|
||||
git clone <this-repo>
|
||||
cd video_frame_extractor
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Linux (apt)
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install ffmpeg libavcodec-dev libavformat-dev \
|
||||
libavutil-dev libswscale-dev cmake build-essential
|
||||
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Linux (dnf / Fedora)
|
||||
|
||||
```bash
|
||||
sudo dnf install ffmpeg-devel cmake gcc-c++
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Windows (vcpkg + MSVC)
|
||||
|
||||
1. Install [vcpkg](https://github.com/microsoft/vcpkg) and integrate it:
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
.\bootstrap-vcpkg.bat
|
||||
.\vcpkg integrate install
|
||||
.\vcpkg install ffmpeg:x64-windows
|
||||
```
|
||||
|
||||
2. Build:
|
||||
|
||||
```powershell
|
||||
cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake `
|
||||
-DVCPKG_TARGET_TRIPLET=x64-windows `
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
### Windows (manual FFmpeg build / pre-built binaries)
|
||||
|
||||
Download pre-built FFmpeg dev libs from https://github.com/BtbN/FFmpeg-Builds/releases
|
||||
and pass their paths to CMake:
|
||||
|
||||
```powershell
|
||||
cmake -B build `
|
||||
-DFFMPEG_INCLUDE_DIR="C:\ffmpeg\include" `
|
||||
-DFFMPEG_LIB_DIR="C:\ffmpeg\lib" `
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
video_frame_extractor <video_file>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
./build/video_frame_extractor sample.mp4
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Output directory: /tmp/vfe_frames_aBcDeF
|
||||
Video: sample.mp4
|
||||
Stream duration: 12.34s | Resolution: 1920x1080
|
||||
Extracting frames...
|
||||
Written frame 0 -> /tmp/vfe_frames_aBcDeF/frame_00000000.tif
|
||||
Written frame 100 -> /tmp/vfe_frames_aBcDeF/frame_00000100.tif
|
||||
...
|
||||
Done. Extracted 370 frame(s) to:
|
||||
/tmp/vfe_frames_aBcDeF
|
||||
```
|
||||
|
||||
Frames are named `frame_XXXXXXXX.tif` (zero-padded to 8 digits).
|
||||
|
||||
---
|
||||
|
||||
## TIFF format details
|
||||
|
||||
Each file is a **baseline TIFF 6.0** image:
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Colour space | RGB |
|
||||
| Bits per sample | 8 per channel (24-bit) |
|
||||
| Compression | None (1) |
|
||||
| Planar config | Chunky (interleaved) |
|
||||
| Byte order | Little-endian |
|
||||
| Strip layout | Single strip (full image) |
|
||||
|
||||
These files open correctly in Photoshop, GIMP, Preview, Windows Photo Viewer,
|
||||
ImageMagick, and any other TIFF-capable application.
|
||||
|
||||
---
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
video_frame_extractor/
|
||||
├── CMakeLists.txt # Cross-platform build script
|
||||
├── main.cpp # Entry point
|
||||
├── frame_extractor.h/.cpp # libav decoding + RGB conversion
|
||||
├── tiff_writer.h/.cpp # Dependency-free TIFF encoder
|
||||
└── platform_utils.h/.cpp # Temp-dir + path helpers (Win/POSIX)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT — do whatever you like with it.
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
#include "frame_extractor.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
#include <libswscale/swscale.h>
|
||||
}
|
||||
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
|
||||
// ── RAII wrappers ────────────────────────────────────────────────────────────
|
||||
|
||||
struct FormatContextGuard {
|
||||
AVFormatContext* ctx = nullptr;
|
||||
~FormatContextGuard() { if (ctx) avformat_close_input(&ctx); }
|
||||
};
|
||||
|
||||
struct CodecContextGuard {
|
||||
AVCodecContext* ctx = nullptr;
|
||||
~CodecContextGuard() { if (ctx) avcodec_free_context(&ctx); }
|
||||
};
|
||||
|
||||
struct FrameGuard {
|
||||
AVFrame* frame = nullptr;
|
||||
FrameGuard() : frame(av_frame_alloc()) {}
|
||||
~FrameGuard() { if (frame) av_frame_free(&frame); }
|
||||
};
|
||||
|
||||
struct PacketGuard {
|
||||
AVPacket* pkt = nullptr;
|
||||
PacketGuard() : pkt(av_packet_alloc()) {}
|
||||
~PacketGuard() { if (pkt) av_packet_free(&pkt); }
|
||||
};
|
||||
|
||||
struct SwsContextGuard {
|
||||
SwsContext* ctx = nullptr;
|
||||
~SwsContextGuard() { if (ctx) sws_freeContext(ctx); }
|
||||
};
|
||||
|
||||
// ── Impl ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
struct FrameExtractor::Impl {
|
||||
std::string path;
|
||||
|
||||
FormatContextGuard fmt;
|
||||
CodecContextGuard codec;
|
||||
|
||||
int videoStreamIndex = -1;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
double duration = 0.0;
|
||||
};
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
FrameExtractor::FrameExtractor(std::string path)
|
||||
: m_path(std::move(path))
|
||||
{
|
||||
d = new Impl();
|
||||
d->path = m_path;
|
||||
}
|
||||
|
||||
FrameExtractor::~FrameExtractor()
|
||||
{
|
||||
delete d;
|
||||
}
|
||||
|
||||
bool FrameExtractor::open()
|
||||
{
|
||||
// Open container
|
||||
if (avformat_open_input(&d->fmt.ctx, d->path.c_str(), nullptr, nullptr) < 0) {
|
||||
std::cerr << "[FrameExtractor] Cannot open file: " << d->path << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (avformat_find_stream_info(d->fmt.ctx, nullptr) < 0) {
|
||||
std::cerr << "[FrameExtractor] Cannot find stream info.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find best video stream
|
||||
const AVCodec* codec = nullptr;
|
||||
d->videoStreamIndex = av_find_best_stream(
|
||||
d->fmt.ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
|
||||
|
||||
if (d->videoStreamIndex < 0 || !codec) {
|
||||
std::cerr << "[FrameExtractor] No video stream found.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
AVStream* stream = d->fmt.ctx->streams[d->videoStreamIndex];
|
||||
|
||||
// Create and configure codec context
|
||||
d->codec.ctx = avcodec_alloc_context3(codec);
|
||||
if (!d->codec.ctx) {
|
||||
std::cerr << "[FrameExtractor] Cannot allocate codec context.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (avcodec_parameters_to_context(d->codec.ctx, stream->codecpar) < 0) {
|
||||
std::cerr << "[FrameExtractor] Cannot copy codec parameters.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (avcodec_open2(d->codec.ctx, codec, nullptr) < 0) {
|
||||
std::cerr << "[FrameExtractor] Cannot open codec.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
d->width = d->codec.ctx->width;
|
||||
d->height = d->codec.ctx->height;
|
||||
|
||||
// Duration in seconds
|
||||
if (d->fmt.ctx->duration != AV_NOPTS_VALUE) {
|
||||
d->duration = static_cast<double>(d->fmt.ctx->duration) / AV_TIME_BASE;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FrameExtractor::forEachFrame(FrameCallback cb)
|
||||
{
|
||||
if (d->videoStreamIndex < 0) {
|
||||
throw std::runtime_error("FrameExtractor::open() must succeed before forEachFrame()");
|
||||
}
|
||||
|
||||
const int W = d->width;
|
||||
const int H = d->height;
|
||||
const AVPixelFormat srcFmt = d->codec.ctx->pix_fmt;
|
||||
|
||||
// SwsContext for pixel-format conversion → RGB24
|
||||
SwsContextGuard sws;
|
||||
sws.ctx = sws_getContext(
|
||||
W, H, srcFmt,
|
||||
W, H, AV_PIX_FMT_RGB24,
|
||||
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
||||
|
||||
if (!sws.ctx) {
|
||||
throw std::runtime_error("sws_getContext failed");
|
||||
}
|
||||
|
||||
// Allocate a dedicated RGB frame (destination)
|
||||
FrameGuard rgbFrame;
|
||||
if (!rgbFrame.frame) throw std::runtime_error("av_frame_alloc failed");
|
||||
|
||||
const int bufSize = av_image_get_buffer_size(AV_PIX_FMT_RGB24, W, H, 1);
|
||||
std::vector<uint8_t> rgbBuffer(static_cast<size_t>(bufSize));
|
||||
|
||||
av_image_fill_arrays(
|
||||
rgbFrame.frame->data, rgbFrame.frame->linesize,
|
||||
rgbBuffer.data(), AV_PIX_FMT_RGB24, W, H, 1);
|
||||
|
||||
FrameGuard rawFrame;
|
||||
PacketGuard pkt;
|
||||
|
||||
FrameData fd;
|
||||
fd.width = W;
|
||||
fd.height = H;
|
||||
fd.pixels.resize(static_cast<size_t>(W) * H * 3);
|
||||
|
||||
bool keepGoing = true;
|
||||
|
||||
while (keepGoing && av_read_frame(d->fmt.ctx, pkt.pkt) >= 0) {
|
||||
if (pkt.pkt->stream_index != d->videoStreamIndex) {
|
||||
av_packet_unref(pkt.pkt);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (avcodec_send_packet(d->codec.ctx, pkt.pkt) < 0) {
|
||||
av_packet_unref(pkt.pkt);
|
||||
continue;
|
||||
}
|
||||
av_packet_unref(pkt.pkt);
|
||||
|
||||
int ret;
|
||||
while ((ret = avcodec_receive_frame(d->codec.ctx, rawFrame.frame)) >= 0) {
|
||||
// Convert to RGB24
|
||||
sws_scale(
|
||||
sws.ctx,
|
||||
rawFrame.frame->data, rawFrame.frame->linesize, 0, H,
|
||||
rgbFrame.frame->data, rgbFrame.frame->linesize);
|
||||
|
||||
// Copy into FrameData (packed, no stride padding)
|
||||
const uint8_t* src = rgbFrame.frame->data[0];
|
||||
const int srcStride = rgbFrame.frame->linesize[0];
|
||||
uint8_t* dst = fd.pixels.data();
|
||||
const int rowBytes = W * 3;
|
||||
|
||||
for (int y = 0; y < H; ++y) {
|
||||
std::copy(src, src + rowBytes, dst);
|
||||
src += srcStride;
|
||||
dst += rowBytes;
|
||||
}
|
||||
|
||||
fd.pts = rawFrame.frame->pts;
|
||||
|
||||
keepGoing = cb(fd);
|
||||
av_frame_unref(rawFrame.frame);
|
||||
|
||||
if (!keepGoing) break;
|
||||
}
|
||||
|
||||
if (ret != AVERROR(EAGAIN) && ret != AVERROR_EOF && ret < 0) {
|
||||
break; // real decode error
|
||||
}
|
||||
}
|
||||
|
||||
// Flush decoder
|
||||
if (keepGoing) {
|
||||
avcodec_send_packet(d->codec.ctx, nullptr);
|
||||
int ret;
|
||||
while ((ret = avcodec_receive_frame(d->codec.ctx, rawFrame.frame)) >= 0) {
|
||||
sws_scale(
|
||||
sws.ctx,
|
||||
rawFrame.frame->data, rawFrame.frame->linesize, 0, H,
|
||||
rgbFrame.frame->data, rgbFrame.frame->linesize);
|
||||
|
||||
const uint8_t* src = rgbFrame.frame->data[0];
|
||||
const int srcStride = rgbFrame.frame->linesize[0];
|
||||
uint8_t* dst = fd.pixels.data();
|
||||
const int rowBytes = W * 3;
|
||||
for (int y = 0; y < H; ++y) {
|
||||
std::copy(src, src + rowBytes, dst);
|
||||
src += srcStride;
|
||||
dst += rowBytes;
|
||||
}
|
||||
|
||||
fd.pts = rawFrame.frame->pts;
|
||||
av_frame_unref(rawFrame.frame);
|
||||
|
||||
if (!cb(fd)) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int FrameExtractor::width() const { return d->width; }
|
||||
int FrameExtractor::height() const { return d->height; }
|
||||
double FrameExtractor::durationSeconds() const { return d->duration; }
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Raw decoded frame: packed RGB24 pixels, top-to-bottom, no padding.
|
||||
struct FrameData {
|
||||
std::vector<uint8_t> pixels; // width * height * 3 bytes (RGB)
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int64_t pts = 0; // presentation timestamp (stream time-base units)
|
||||
};
|
||||
|
||||
// Callback invoked for every decoded frame.
|
||||
// Return true to continue, false to stop early.
|
||||
using FrameCallback = std::function<bool(const FrameData&)>;
|
||||
|
||||
/**
|
||||
* Opens a video file with libavformat/libavcodec, seeks to the first video
|
||||
* stream, and decodes every frame in display order, converting each to RGB24
|
||||
* via libswscale before handing it to the caller.
|
||||
*/
|
||||
class FrameExtractor {
|
||||
public:
|
||||
explicit FrameExtractor(std::string path);
|
||||
~FrameExtractor();
|
||||
|
||||
// Disallow copy
|
||||
FrameExtractor(const FrameExtractor&) = delete;
|
||||
FrameExtractor& operator=(const FrameExtractor&) = delete;
|
||||
|
||||
// Open the file and locate the video stream. Returns false on error.
|
||||
bool open();
|
||||
|
||||
// Decode every frame and invoke cb for each one.
|
||||
void forEachFrame(FrameCallback cb);
|
||||
|
||||
// Metadata accessors (valid after open())
|
||||
int width() const;
|
||||
int height() const;
|
||||
double durationSeconds() const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
Impl* d = nullptr;
|
||||
|
||||
std::string m_path;
|
||||
};
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* video_frame_extractor
|
||||
*
|
||||
* Extracts every frame from a video file and writes them as raw TIFF images.
|
||||
* Compiles on macOS, Linux, and Windows.
|
||||
*
|
||||
* Usage:
|
||||
* video_frame_extractor <video_file> [output_dir]
|
||||
*
|
||||
* If output_dir is omitted, a unique temporary directory is created and its
|
||||
* path is printed. If output_dir is provided it must already exist.
|
||||
*
|
||||
* Dependencies: FFmpeg (libavcodec, libavformat, libavutil, libswscale)
|
||||
*
|
||||
* Build:
|
||||
* See CMakeLists.txt
|
||||
*/
|
||||
|
||||
#include "frame_extractor.h"
|
||||
#include "tiff_writer.h"
|
||||
#include "platform_utils.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
if (argc < 2) {
|
||||
std::cerr << "Usage: " << argv[0] << " <video_file> [output_dir]\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string inputPath = argv[1];
|
||||
|
||||
// Determine output directory: use the supplied path or create a temp dir.
|
||||
std::string outputDir;
|
||||
if (argc >= 3) {
|
||||
outputDir = argv[2];
|
||||
if (!platform::directoryExists(outputDir)) {
|
||||
std::cerr << "Output directory does not exist: " << outputDir << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Output directory: " << outputDir << "\n";
|
||||
} else {
|
||||
if (!platform::createTempDirectory("vfe_frames_", outputDir)) {
|
||||
std::cerr << "Failed to create temporary directory.\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Output directory: " << outputDir << " (temporary)\n";
|
||||
}
|
||||
|
||||
// Open video
|
||||
FrameExtractor extractor(inputPath);
|
||||
if (!extractor.open()) {
|
||||
std::cerr << "Failed to open video: " << inputPath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Video: " << inputPath << "\n"
|
||||
<< "Stream duration: " << extractor.durationSeconds() << "s | "
|
||||
<< "Resolution: " << extractor.width() << "x" << extractor.height() << "\n"
|
||||
<< "Extracting frames...\n";
|
||||
|
||||
TiffWriter writer(outputDir);
|
||||
uint64_t frameCount = 0;
|
||||
|
||||
extractor.forEachFrame([&](const FrameData& frame) {
|
||||
const std::string filename = writer.write(frame, frameCount);
|
||||
if (filename.empty()) {
|
||||
std::cerr << "Warning: failed to write frame " << frameCount << "\n";
|
||||
} else if (frameCount % 100 == 0) {
|
||||
std::cout << " Written frame " << frameCount
|
||||
<< " -> " << filename << "\n";
|
||||
}
|
||||
++frameCount;
|
||||
return true; // return false to stop early
|
||||
});
|
||||
|
||||
std::cout << "Done. Extracted " << frameCount << " frame(s) to:\n"
|
||||
<< " " << outputDir << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
#include "platform_utils.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
// ── Windows ──────────────────────────────────────────────────────────────────
|
||||
#if defined(_WIN32)
|
||||
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <wchar.h>
|
||||
|
||||
namespace platform {
|
||||
|
||||
char pathSeparator() { return '\\'; }
|
||||
|
||||
bool directoryExists(const std::string& path)
|
||||
{
|
||||
const int wlen = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, nullptr, 0);
|
||||
if (wlen <= 0) return false;
|
||||
std::wstring wpath(static_cast<size_t>(wlen - 1), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, &wpath[0], wlen);
|
||||
|
||||
const DWORD attrs = GetFileAttributesW(wpath.c_str());
|
||||
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY);
|
||||
}
|
||||
|
||||
bool createTempDirectory(const std::string& prefix, std::string& outPath)
|
||||
{
|
||||
wchar_t tempBase[MAX_PATH + 1];
|
||||
if (GetTempPathW(MAX_PATH, tempBase) == 0) return false;
|
||||
|
||||
std::wstring wPrefix(prefix.begin(), prefix.end());
|
||||
|
||||
wchar_t tempFile[MAX_PATH + 1];
|
||||
if (GetTempFileNameW(tempBase, wPrefix.c_str(), 0, tempFile) == 0) return false;
|
||||
|
||||
DeleteFileW(tempFile);
|
||||
if (!CreateDirectoryW(tempFile, nullptr)) return false;
|
||||
|
||||
const int len = WideCharToMultiByte(CP_UTF8, 0, tempFile, -1, nullptr, 0, nullptr, nullptr);
|
||||
if (len <= 0) return false;
|
||||
outPath.resize(static_cast<size_t>(len - 1));
|
||||
WideCharToMultiByte(CP_UTF8, 0, tempFile, -1, &outPath[0], len, nullptr, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
|
||||
// ── POSIX (macOS + Linux) ─────────────────────────────────────────────────
|
||||
#else
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace platform {
|
||||
|
||||
char pathSeparator() { return '/'; }
|
||||
|
||||
bool directoryExists(const std::string& path)
|
||||
{
|
||||
struct stat st{};
|
||||
return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
|
||||
}
|
||||
|
||||
bool createTempDirectory(const std::string& prefix, std::string& outPath)
|
||||
{
|
||||
const char* tmpEnv = std::getenv("TMPDIR");
|
||||
std::string base = (tmpEnv && *tmpEnv) ? tmpEnv : "/tmp";
|
||||
|
||||
if (!base.empty() && base.back() == '/')
|
||||
base.pop_back();
|
||||
|
||||
// mkdtemp needs a mutable char*. In C++17, std::string::data() is
|
||||
// non-const and the buffer is null-terminated, so we can use it directly
|
||||
// without a separate std::vector<char> copy.
|
||||
std::string tmpl = base + "/" + prefix + "XXXXXX";
|
||||
|
||||
if (mkdtemp(tmpl.data()) == nullptr) return false;
|
||||
|
||||
outPath = tmpl;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace platform {
|
||||
|
||||
/**
|
||||
* Returns the OS-appropriate path separator character ('/' or '\\').
|
||||
*/
|
||||
char pathSeparator();
|
||||
|
||||
/**
|
||||
* Returns true if the given path exists and is a directory.
|
||||
*/
|
||||
bool directoryExists(const std::string& path);
|
||||
|
||||
/**
|
||||
* Create a uniquely-named subdirectory inside the system temporary directory.
|
||||
*
|
||||
* @param prefix Name prefix for the new directory.
|
||||
* @param outPath Receives the full path of the created directory on success.
|
||||
* @return True on success, false on failure.
|
||||
*/
|
||||
bool createTempDirectory(const std::string& prefix, std::string& outPath);
|
||||
|
||||
} // namespace platform
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Minimal, dependency-free TIFF writer.
|
||||
*
|
||||
* Produces uncompressed, baseline TIFF 6.0 files (little-endian).
|
||||
* Layout:
|
||||
* [8-byte header] [pixel data] [0–1 pad byte] [IFD entry count (2 bytes)]
|
||||
* [N × 12-byte IFD entries] [next-IFD offset = 0 (4 bytes)]
|
||||
* [BitsPerSample data: 3 × SHORT]
|
||||
*
|
||||
* Placing pixel data before the IFD keeps the strip offset a fixed constant
|
||||
* (always 8) and avoids a two-pass write. A padding byte is inserted when
|
||||
* imageSize is odd so the IFD starts on a word boundary, as the TIFF spec
|
||||
* requires.
|
||||
*/
|
||||
|
||||
#include "tiff_writer.h"
|
||||
#include "platform_utils.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
// ── TIFF helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t TIFF_SHORT = 3;
|
||||
constexpr uint16_t TIFF_LONG = 4;
|
||||
|
||||
constexpr uint16_t TAG_IMAGE_WIDTH = 256;
|
||||
constexpr uint16_t TAG_IMAGE_LENGTH = 257;
|
||||
constexpr uint16_t TAG_BITS_PER_SAMPLE = 258;
|
||||
constexpr uint16_t TAG_COMPRESSION = 259;
|
||||
constexpr uint16_t TAG_PHOTOMETRIC = 262;
|
||||
constexpr uint16_t TAG_STRIP_OFFSETS = 273;
|
||||
constexpr uint16_t TAG_SAMPLES_PER_PIXEL = 277;
|
||||
constexpr uint16_t TAG_ROWS_PER_STRIP = 278;
|
||||
constexpr uint16_t TAG_STRIP_BYTE_COUNTS = 279;
|
||||
constexpr uint16_t TAG_PLANAR_CONFIG = 284;
|
||||
|
||||
template <typename T>
|
||||
void writeLE(std::vector<uint8_t>& buf, size_t offset, T value)
|
||||
{
|
||||
static_assert(std::is_integral<T>::value, "Integral required");
|
||||
for (size_t i = 0; i < sizeof(T); ++i)
|
||||
buf[offset + i] = static_cast<uint8_t>((value >> (8 * i)) & 0xFF);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void appendLE(std::vector<uint8_t>& buf, T value)
|
||||
{
|
||||
const size_t pos = buf.size();
|
||||
buf.resize(pos + sizeof(T));
|
||||
writeLE(buf, pos, value);
|
||||
}
|
||||
|
||||
void appendIFDEntry(std::vector<uint8_t>& buf,
|
||||
uint16_t tag, uint16_t type,
|
||||
uint32_t count, uint32_t valueOrOffset)
|
||||
{
|
||||
appendLE<uint16_t>(buf, tag);
|
||||
appendLE<uint16_t>(buf, type);
|
||||
appendLE<uint32_t>(buf, count);
|
||||
appendLE<uint32_t>(buf, valueOrOffset);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── TiffWriter ────────────────────────────────────────────────────────────────
|
||||
|
||||
TiffWriter::TiffWriter(const std::string& outputDir)
|
||||
: m_dir(outputDir)
|
||||
{}
|
||||
|
||||
std::string TiffWriter::write(const FrameData& frame, uint64_t frameIndex)
|
||||
{
|
||||
if (frame.pixels.empty() || frame.width <= 0 || frame.height <= 0)
|
||||
return "";
|
||||
|
||||
std::ostringstream nameStream;
|
||||
nameStream << "frame_" << std::setw(8) << std::setfill('0') << frameIndex << ".tif";
|
||||
const std::string filePath = m_dir + platform::pathSeparator() + nameStream.str();
|
||||
|
||||
// ── Offsets ───────────────────────────────────────────────────────────
|
||||
|
||||
const uint32_t W = static_cast<uint32_t>(frame.width);
|
||||
const uint32_t H = static_cast<uint32_t>(frame.height);
|
||||
const uint32_t imageSize = W * 3u * H;
|
||||
|
||||
// The TIFF spec requires IFDs to begin on a word (2-byte) boundary.
|
||||
// Insert a 1-byte pad when imageSize is odd.
|
||||
const uint32_t padBytes = imageSize % 2u;
|
||||
|
||||
constexpr uint32_t HEADER_SIZE = 8u;
|
||||
constexpr uint16_t NUM_ENTRIES = 10u; // exactly the entries written below
|
||||
constexpr uint32_t IFD_SIZE = 2u + NUM_ENTRIES * 12u + 4u;
|
||||
|
||||
const uint32_t pixelOffset = HEADER_SIZE;
|
||||
const uint32_t ifdOffset = pixelOffset + imageSize + padBytes;
|
||||
const uint32_t bpsOffset = ifdOffset + IFD_SIZE; // 3 × SHORT after IFD
|
||||
|
||||
// ── Assemble in memory ────────────────────────────────────────────────
|
||||
|
||||
std::vector<uint8_t> tiff;
|
||||
tiff.reserve(bpsOffset + 6u);
|
||||
|
||||
// Header
|
||||
appendLE<uint8_t> (tiff, 0x49); // 'I' – little-endian
|
||||
appendLE<uint8_t> (tiff, 0x49); // 'I'
|
||||
appendLE<uint16_t>(tiff, 42u); // TIFF magic number
|
||||
appendLE<uint32_t>(tiff, ifdOffset); // offset of first IFD
|
||||
|
||||
// Pixel data
|
||||
tiff.insert(tiff.end(), frame.pixels.begin(), frame.pixels.end());
|
||||
|
||||
// Alignment pad (0 or 1 byte)
|
||||
if (padBytes)
|
||||
appendLE<uint8_t>(tiff, 0u);
|
||||
|
||||
// IFD entry count (must match NUM_ENTRIES exactly)
|
||||
appendLE<uint16_t>(tiff, NUM_ENTRIES);
|
||||
|
||||
// IFD entries — ascending tag order required by the spec
|
||||
appendIFDEntry(tiff, TAG_IMAGE_WIDTH, TIFF_LONG, 1u, W);
|
||||
appendIFDEntry(tiff, TAG_IMAGE_LENGTH, TIFF_LONG, 1u, H);
|
||||
appendIFDEntry(tiff, TAG_BITS_PER_SAMPLE, TIFF_SHORT, 3u, bpsOffset); // → extra data
|
||||
appendIFDEntry(tiff, TAG_COMPRESSION, TIFF_SHORT, 1u, 1u); // no compression
|
||||
appendIFDEntry(tiff, TAG_PHOTOMETRIC, TIFF_SHORT, 1u, 2u); // RGB
|
||||
appendIFDEntry(tiff, TAG_STRIP_OFFSETS, TIFF_LONG, 1u, pixelOffset);
|
||||
appendIFDEntry(tiff, TAG_SAMPLES_PER_PIXEL, TIFF_SHORT, 1u, 3u);
|
||||
appendIFDEntry(tiff, TAG_ROWS_PER_STRIP, TIFF_LONG, 1u, H); // single strip
|
||||
appendIFDEntry(tiff, TAG_STRIP_BYTE_COUNTS, TIFF_LONG, 1u, imageSize);
|
||||
appendIFDEntry(tiff, TAG_PLANAR_CONFIG, TIFF_SHORT, 1u, 1u); // chunky / interleaved
|
||||
|
||||
// Next-IFD offset = 0 (this is the only IFD)
|
||||
appendLE<uint32_t>(tiff, 0u);
|
||||
|
||||
// Extra data: BitsPerSample values (8, 8, 8)
|
||||
appendLE<uint16_t>(tiff, 8u); // R
|
||||
appendLE<uint16_t>(tiff, 8u); // G
|
||||
appendLE<uint16_t>(tiff, 8u); // B
|
||||
|
||||
// ── Write to disk ─────────────────────────────────────────────────────
|
||||
|
||||
std::ofstream out(filePath, std::ios::binary);
|
||||
if (!out) return "";
|
||||
out.write(reinterpret_cast<const char*>(tiff.data()),
|
||||
static_cast<std::streamsize>(tiff.size()));
|
||||
if (!out) return "";
|
||||
return filePath;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include "frame_extractor.h"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Writes FrameData objects as uncompressed RGB TIFF files.
|
||||
* No external library required — the TIFF structure is written by hand.
|
||||
* This produces baseline TIFF 6.0 files that every TIFF reader understands.
|
||||
*/
|
||||
class TiffWriter {
|
||||
public:
|
||||
explicit TiffWriter(const std::string& outputDir);
|
||||
|
||||
/**
|
||||
* Write a single frame.
|
||||
* @param frame Source pixel data (RGB24, packed).
|
||||
* @param frameIndex Zero-based frame number used to build the filename.
|
||||
* @return Full path of the written file, or "" on error.
|
||||
*/
|
||||
std::string write(const FrameData& frame, uint64_t frameIndex);
|
||||
|
||||
private:
|
||||
std::string m_dir;
|
||||
};
|
||||
Loading…
Reference in New Issue