diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..34291a8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + pull_request: + +jobs: + rust: + name: Rust + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Check formatting + run: cargo fmt --check + + - name: Run tests + run: cargo test diff --git a/Cargo.toml b/Cargo.toml index 4daa7ac..6a38c21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,12 @@ name = "zigvid" version = "0.1.0" edition = "2024" +description = "Compute zigzag persistence barcodes for binary image sequences and videos." +license = "MIT" +repository = "https://github.com/Landa233/zigvid" +readme = "README.md" +keywords = ["topology", "barcode", "video", "image-processing"] +categories = ["algorithms", "science"] [lib] name = "zigvid" @@ -10,7 +16,6 @@ crate-type = ["rlib", "cdylib"] [features] python = ["pyo3"] - [dependencies] image = "0.25.6" ndarray = "0.16.1" @@ -26,7 +31,6 @@ criterion = "0.5" pyo3 = { version = "0.28.3", optional = true, features = ["extension-module"] } numpy = { version = "0.28.0" } - [dev-dependencies] csv = "1" serde = { version = "1", features = ["derive"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5a4b125 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Landa233 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7f133d --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# Zigvid + +Zigvid computes H0 and H1 zigzag persistence barcodes for binary videos. It implements the pipeline of [1], providing a highly parallelisable graph-based approach that avoids constructing cubical complexes and scales to high-resolution, high-frame-rate data. The barcode decomposition is based on the algorithm presented in [2]. + +The package provides a Rust library for building formigrams and computing barcodes, together with optional Python bindings built using PyO3 and maturin. + +## Features + +- Compute H0 and H1 zigzag persistence barcodes from image frame paths. +- Configure foreground color, 4- or 8-connectivity, and union/intersection frame composition. +- Use the Rust API directly or build a Python extension module with maturin. +- Parallelise frame-level work with a configurable thread count. + +## Repository Status + +This repository is being prepared for public release. The core Rust API is functional, while packaging and convenience features are still under active development. + +### Roadmap +These features will be added: +- Support NumPy arrays as direct input for Python users. +- Provide pre-built wheels for installation via `pip`. + +## Rust Usage + +Add Zigvid as a path dependency while developing locally: + +```toml +[dependencies] +zigvid = { path = "../zigvid" } +``` + +Compute H0 and H1 barcodes from a sequence of binary image frames: + +```rust +use zigvid::{ + binary_image::foreground_info::{ + BinaryColor, CompositionType, Connectivity, ForegroundInfo, + }, + binary_video::compute_barcode::{compute_zigzag_barcode_h0, compute_zigzag_barcode_h1}, +}; + +let frames = vec![ + "frames/frame_00000.png", + "frames/frame_00001.png", + "frames/frame_00002.png", +]; + +let foreground = ForegroundInfo { + color: BinaryColor::White, + connectivity: Connectivity::Eight, + composition_type: CompositionType::Union, + padding: None, +}; + +let number_of_cores = 4; + +let h0_barcode = compute_zigzag_barcode_h0(&frames, foreground, number_of_cores); + +// Uses Alexander duality under the hood +let h1_barcode = compute_zigzag_barcode_h1(&frames, foreground, number_of_cores); + +println!("H0 barcode: {h0_barcode:?}"); +println!("H1 barcode: {h1_barcode:?}"); +``` + +For H1, pass the same `ForegroundInfo`; `compute_zigzag_barcode_h1` applies the required foreground/background inversion internally. + +## Rust Development + +Run the self-contained example: + +```sh +cargo run --example minimal_barcode +``` + +`minimal_barcode` writes temporary test frames before computing barcodes, so it does not require local generated data or benchmark files. + +## Python Usage + +The Python package is built from the same Rust crate with maturin. From the repository root, create or activate a virtual environment, then install the extension in editable mode: + +```sh +python -m pip install maturin +maturin develop +``` + +Then import `zigvid` from Python: + +```python +import zigvid + +barcode = zigvid.compute_barcode_from_paths( + [ + "frames/frame_00000.png", + "frames/frame_00001.png", + "frames/frame_00002.png", + ], + foreground_color="white", + foreground_connectivity="8", + foreground_composition_type="union", + padding=None, + num_threads=-1, + dim=None, +) + +print(barcode) +``` + +`dim=None` computes both supported dimensions. Use `dim=0` for H0 only or `dim=1` for H1 only. + +The return value is a dictionary containing `h0` and/or `h1`. Each dimension contains NumPy arrays grouped by interval endpoint type: + +- `oo`: open-open intervals +- `oc`: open-closed intervals +- `co`: closed-open intervals +- `cc`: closed-closed intervals + +Each array has shape `(n, 2)` and stores birth/death indices. + +## Python Options + +`compute_barcode_from_paths` accepts: + +- `paths`: ordered image frame paths. +- `foreground_color`: `"white"` or `"black"`. +- `foreground_connectivity`: `"4"` or `"8"`. +- `foreground_composition_type`: `"union"` or `"intersection"`. +- `padding`: `None`, `"none"`, `"white"`, or `"black"`. +- `num_threads`: `-1` to use available parallelism, or a positive integer. +- `dim`: `None`, `0`, or `1`. If `None`, both H0 and H1 are computed. + +Build a wheel instead of installing in editable mode: + +```sh +maturin build --release +``` + + +## References + +[1] D. Lanners, +*From Frames to Features: Scalable Zigzag Persistence for Binary Video*, +2026. +[[arXiv]](https://arxiv.org/abs/...) + +[2] T. K. Dey and T. Hou, +*Computing Zigzag Persistence on Graphs in Near-Linear Time*, +Proceedings of the 38th International Symposium on Computational Geometry (SoCG), 2022. +[[DOI]](https://doi.org/10.4230/LIPIcs.SoCG.2022.31) + +## License + +Zigvid is licensed under the MIT License. See [LICENSE](LICENSE). diff --git a/examples/benchmark_top_complexity_scaling.rs b/examples/benchmark_top_complexity_scaling.rs index 8b5c5b8..7177d90 100644 --- a/examples/benchmark_top_complexity_scaling.rs +++ b/examples/benchmark_top_complexity_scaling.rs @@ -44,8 +44,9 @@ fn main() -> Result<(), Box> { let pool = ThreadPoolBuilder::new().num_threads(6).build()?; - let num_discs = vec![14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,]; + let num_discs = vec![ + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + ]; let mut rows = Vec::new(); @@ -60,7 +61,7 @@ fn main() -> Result<(), Box> { for run in 1..=repeats { println!(" Run {run}/{repeats}"); - // benches/python/benchmark_videos/top_complexity_scaling/discs_100/256x256_300frames/frames/frame_00000.png + // benches/python/benchmark_videos/top_complexity_scaling/discs_100/256x256_300frames/frames/frame_00000.png let paths: Vec = (0..300) .map(|i| { @@ -84,8 +85,7 @@ fn main() -> Result<(), Box> { let formigram_start = Instant::now(); - let mut h0_formigram = - generate_formigram_from_video(&paths, foreground_info, 1); + let mut h0_formigram = generate_formigram_from_video(&paths, foreground_info, 1); // let mut h1_formigram = // generate_formigram_from_video(&paths, inverted_foreground_info, 1); @@ -104,12 +104,12 @@ fn main() -> Result<(), Box> { total_times.push(total_time); println!("Formigram time: {:.6}s", formigram_time); - println!("Formigram multiplicities: {:?}", h0_formigram.calculate_total_merge_split_mult()); - + println!( + "Formigram multiplicities: {:?}", + h0_formigram.calculate_total_merge_split_mult() + ); } - - let mean_formigram_time = mean(&formigram_times); let std_formigram_time = std(&formigram_times); @@ -123,7 +123,7 @@ fn main() -> Result<(), Box> { rows.push(BenchmarkRow { num_discs: discs, run: run_idx + 1, - threads:1, + threads: 1, formigram_time: formigram_times[run_idx], barcode_time: barcode_times[run_idx], total_time: total_times[run_idx], diff --git a/examples/minimal_barcode.rs b/examples/minimal_barcode.rs new file mode 100644 index 0000000..8c75fe4 --- /dev/null +++ b/examples/minimal_barcode.rs @@ -0,0 +1,55 @@ +use image::{GrayImage, Luma}; +use tempfile::tempdir; +use zigvid::{ + binary_image::foreground_info::{BinaryColor, CompositionType, Connectivity, ForegroundInfo}, + binary_video::compute_barcode::{compute_zigzag_barcode_h0, compute_zigzag_barcode_h1}, +}; + +fn write_frame(path: impl AsRef, pixels: &[(u32, u32)]) -> image::ImageResult<()> { + let mut image = GrayImage::new(5, 5); + + for &(x, y) in pixels { + image.put_pixel(x, y, Luma([255])); + } + + image.save(path) +} + +fn main() -> Result<(), Box> { + let temp_dir = tempdir()?; + + let frames = [ + ("frame_00000.png", vec![(2, 2)]), + ("frame_00001.png", vec![(1, 2), (2, 2), (3, 2)]), + ( + "frame_00002.png", + vec![(1, 1), (1, 2), (2, 2), (3, 2), (3, 3)], + ), + ]; + + let frame_paths = frames + .iter() + .map(|(name, pixels)| { + let path = temp_dir.path().join(name); + write_frame(&path, pixels)?; + Ok(path) + }) + .collect::>>()?; + + let foreground = ForegroundInfo { + color: BinaryColor::White, + connectivity: Connectivity::Eight, + composition_type: CompositionType::Union, + padding: None, + }; + + let number_of_cores = 1; + + let h0_barcode = compute_zigzag_barcode_h0(&frame_paths, foreground, number_of_cores); + let h1_barcode = compute_zigzag_barcode_h1(&frame_paths, foreground, number_of_cores); + + println!("H0 barcode: {h0_barcode:?}"); + println!("H1 barcode: {h1_barcode:?}"); + + Ok(()) +} diff --git a/pyproject.toml b/pyproject.toml index 214e4fc..e898f2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,17 @@ build-backend = "maturin" [project] name = "zigvid" version = "0.1.0" +description = "Python bindings for computing zigzag persistence barcodes from binary image sequences." +readme = "README.md" +license = "MIT" +authors = [{ name = "Landa233" }] requires-python = ">=3.9" +[project.urls] +Homepage = "https://github.com/Landa233/zigvid" +Repository = "https://github.com/Landa233/zigvid" +Issues = "https://github.com/Landa233/zigvid/issues" + [tool.maturin] bindings = "pyo3" python-source = "python"