diff --git a/.gitignore b/.gitignore
index 3bb56d4..afd8bc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# Created by https://www.gitignore.io/api/osx,python,pycharm,windows,visualstudio,visualstudiocode
# Edit at https://www.gitignore.io/?templates=osx,python,pycharm,windows,visualstudio,visualstudiocode
+./scripts
### OSX ###
# General
diff --git a/README.md b/README.md
index 4b3104d..471e5f7 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,11 @@
-[![Build status](https://github.com/rspy/rspy/workflows/build/badge.svg?branch=master&event=push)](https://github.com/rspy/rspy/actions?query=workflow%3Abuild)
-[![Python Version](https://img.shields.io/pypi/pyversions/rspy.svg)](https://pypi.org/project/rspy/)
+[![Python Version](https://img.shields.io/badge/python-3.8-blue
+)](https://img.shields.io/badge/python-3.9-pink) [![Python Version](https://img.shields.io/badge/python-3.9-pink
+)](https://img.shields.io/badge/python-3.8-blue) [![Python Version](https://img.shields.io/badge/python-3.10-green
+)](https://img.shields.io/badge/python-3.10-green)
[![Dependencies Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen.svg)](https://github.com/rspy/rspy/pulls?utf8=%E2%9C%93&q=is%3Apr%20author%3Aapp%2Fdependabot)
-
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Security: bandit](https://img.shields.io/badge/security-bandit-green.svg)](https://github.com/PyCQA/bandit)
[![Pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/rspy/rspy/blob/master/.pre-commit-config.yaml)
@@ -14,9 +15,13 @@
![Coverage Report](assets/images/coverage.svg)
The core implementation of Fast Rolling Shutter Correction in the Wild, TPAMI 2023 and Towards Nonlinear-Motion-Aware and Occlusion-Robust Rolling Shutter Correction, ICCV 2023.
-
+| **3GS** | **Gpark** |
+| :---------------------------: | :-------------------------------: |
+| ![3gs](assets/images/3gs.gif) | ![gpark](assets/images/gpark.gif) |
+
+
## Very first steps
### Initialize your code
@@ -88,55 +93,9 @@ Building a new version of the application contains steps:
- Create a `GitHub release`.
- And... publish 🙂 `poetry publish --build`
-## 🎯 What's next
-
-Well, that's up to you 💪🏻. I can only recommend the packages and articles that helped me.
-
-- [`Typer`](https://github.com/tiangolo/typer) is great for creating CLI applications.
-- [`Rich`](https://github.com/willmcgugan/rich) makes it easy to add beautiful formatting in the terminal.
-- [`Pydantic`](https://github.com/samuelcolvin/pydantic/) – data validation and settings management using Python type hinting.
-- [`Loguru`](https://github.com/Delgan/loguru) makes logging (stupidly) simple.
-- [`tqdm`](https://github.com/tqdm/tqdm) – fast, extensible progress bar for Python and CLI.
-- [`IceCream`](https://github.com/gruns/icecream) is a little library for sweet and creamy debugging.
-- [`orjson`](https://github.com/ijl/orjson) – ultra fast JSON parsing library.
-- [`Returns`](https://github.com/dry-python/returns) makes you function's output meaningful, typed, and safe!
-- [`Hydra`](https://github.com/facebookresearch/hydra) is a framework for elegantly configuring complex applications.
-- [`FastAPI`](https://github.com/tiangolo/fastapi) is a type-driven asynchronous web framework.
-
-Articles:
-
-- [Open Source Guides](https://opensource.guide/).
-- [A handy guide to financial support for open source](https://github.com/nayafia/lemonade-stand)
-- [GitHub Actions Documentation](https://help.github.com/en/actions).
-- Maybe you would like to add [gitmoji](https://gitmoji.carloscuesta.me/) to commit names. This is really funny. 😄
-
## 🚀 Features
-
-### Development features
-
-- Supports for `Python 3.8` and higher.
-- [`Poetry`](https://python-poetry.org/) as the dependencies manager. See configuration in [`pyproject.toml`](https://github.com/rspy/rspy/blob/master/pyproject.toml) and [`setup.cfg`](https://github.com/rspy/rspy/blob/master/setup.cfg).
-- Automatic codestyle with [`black`](https://github.com/psf/black), [`isort`](https://github.com/timothycrosley/isort) and [`pyupgrade`](https://github.com/asottile/pyupgrade).
-- Ready-to-use [`pre-commit`](https://pre-commit.com/) hooks with code-formatting.
-- Type checks with [`mypy`](https://mypy.readthedocs.io); docstring checks with [`darglint`](https://github.com/terrencepreilly/darglint); security checks with [`safety`](https://github.com/pyupio/safety) and [`bandit`](https://github.com/PyCQA/bandit)
-- Testing with [`pytest`](https://docs.pytest.org/en/latest/).
-- Ready-to-use [`.editorconfig`](https://github.com/rspy/rspy/blob/master/.editorconfig), [`.dockerignore`](https://github.com/rspy/rspy/blob/master/.dockerignore), and [`.gitignore`](https://github.com/rspy/rspy/blob/master/.gitignore). You don't have to worry about those things.
-
-### Deployment features
-
-- `GitHub` integration: issue and pr templates.
-- `Github Actions` with predefined [build workflow](https://github.com/rspy/rspy/blob/master/.github/workflows/build.yml) as the default CI/CD.
-- Everything is already set up for security checks, codestyle checks, code formatting, testing, linting, docker builds, etc with [`Makefile`](https://github.com/rspy/rspy/blob/master/Makefile#L89). More details in [makefile-usage](#makefile-usage).
-- [Dockerfile](https://github.com/rspy/rspy/blob/master/docker/Dockerfile) for your package.
-- Always up-to-date dependencies with [`@dependabot`](https://dependabot.com/). You will only [enable it](https://docs.github.com/en/github/administering-a-repository/enabling-and-disabling-version-updates#enabling-github-dependabot-version-updates).
-- Automatic drafts of new releases with [`Release Drafter`](https://github.com/marketplace/actions/release-drafter). You may see the list of labels in [`release-drafter.yml`](https://github.com/rspy/rspy/blob/master/.github/release-drafter.yml). Works perfectly with [Semantic Versions](https://semver.org/) specification.
-
-### Open source community features
-
-- Ready-to-use [Pull Requests templates](https://github.com/rspy/rspy/blob/master/.github/PULL_REQUEST_TEMPLATE.md) and several [Issue templates](https://github.com/rspy/rspy/tree/master/.github/ISSUE_TEMPLATE).
-- Files such as: `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `SECURITY.md` are generated automatically.
-- [`Stale bot`](https://github.com/apps/stale) that closes abandoned issues after a period of inactivity. (You will only [need to setup free plan](https://github.com/marketplace/stale)). Configuration is [here](https://github.com/rspy/rspy/blob/master/.github/.stale.yml).
-- [Semantic Versions](https://semver.org/) specification with [`Release Drafter`](https://github.com/marketplace/actions/release-drafter).
+- A light weight library for rolling shutter correction which is easy to use.
+- Support linear, qudratic and cubic motion model.
## Installation
@@ -144,15 +103,7 @@ Articles:
pip install -U rspy
```
-or install with `Poetry`
-
-```bash
-poetry add rspy
-```
-
-
-
-### Makefile usage
+### Usage
[`Makefile`](https://github.com/rspy/rspy/blob/master/Makefile) contains a lot of functions for faster development.
@@ -357,8 +308,8 @@ We use [`Release Drafter`](https://github.com/marketplace/actions/release-drafte
### List of labels and corresponding titles
-| **Label** | **Title in Releases** |
-| :-----------------------------------: | :---------------------: |
+| **Label** | **Title in Releases** |
+| :-----------------------------------: | :--------------------: |
| `enhancement`, `feature` | 🚀 Features |
| `bug`, `refactoring`, `bugfix`, `fix` | 🔧 Fixes & Refactoring |
| `build`, `ci`, `testing` | 📦 Build System & CI/CD |
diff --git a/assets/images/3gs.gif b/assets/images/3gs.gif
new file mode 100644
index 0000000..cafdfe2
Binary files /dev/null and b/assets/images/3gs.gif differ
diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg
index 0644a48..e5db27c 100644
--- a/assets/images/coverage.svg
+++ b/assets/images/coverage.svg
@@ -9,13 +9,13 @@
-
+
coverage
coverage
- 26%
- 26%
+ 100%
+ 100%
diff --git a/assets/images/gpark.gif b/assets/images/gpark.gif
new file mode 100644
index 0000000..8a51ed7
Binary files /dev/null and b/assets/images/gpark.gif differ
diff --git a/rspy/__init__.py b/rspy/__init__.py
index 784771c..c9443f8 100644
--- a/rspy/__init__.py
+++ b/rspy/__init__.py
@@ -1,9 +1,11 @@
# type: ignore[attr-defined]
"""The core implementation of Fast Rolling Shutter Correction in the Wild, TPAMI 2023 and Towards Nonlinear-Motion-Aware and Occlusion-Robust Rolling Shutter Correction, ICCV 2023."""
-import sys
from importlib import metadata as importlib_metadata
+from .solver import cubic_flow, linear_flow, quadratic_flow
+from .utils import feats_sampling
+
def get_version() -> str:
try:
@@ -13,3 +15,5 @@ def get_version() -> str:
version: str = get_version()
+
+__all__ = ["linear_flow", "quadratic_flow", "cubic_flow", "feats_sampling", "version"]
diff --git a/rspy/demo.py b/rspy/demo.py
new file mode 100644
index 0000000..aeeb910
--- /dev/null
+++ b/rspy/demo.py
@@ -0,0 +1,52 @@
+import argparse
+from pathlib import Path
+
+import torch
+from mmflow.apis import inference_model, init_model
+from mmflow.datasets import visualize_flow
+from PIL import Image
+from torchvision import transforms
+from torchvision.utils import save_image
+
+from rspy.solver import cubic_flow, linear_flow, quadratic_flow
+from rspy.utils import feats_sampling
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--input", type=str, help="input file or directory")
+parser.add_argument("--output", type=str, help="output directory")
+parser.add_argument("--model", type=str, default="linear", help="linear | quadratic | cubic")
+parser.add_argument("--gamma", type=float, default=0.9, help="the readout reatio")
+parser.add_argument("--tau", type=int, default=0, help="the timestamp warping to")
+parser.add_argument("--fconfig", type=str, default="raft_8x2_100k_mixed_368x768", help="mmflow config file")
+parser.add_argument("--device", type=str, default="cuda:0", help="cpu | cuda:0")
+args = parser.parse_args()
+
+
+def main():
+ assert args.model in ["linear", "quadratic", "cubic"]
+ input, output = Path(args.input), Path(args.output)
+ image_paths = sorted(list(input.iterdir()))
+
+ # init a optical-flow model
+ config_file, checkpoint_file = f"{args.fconfig}.py", f"{args.fconfig}.pth"
+ model = init_model(config_file, checkpoint_file, device=args.device)
+
+ # * inference flow
+ num = {"linear": 2, "quadratic": 3, "cubic": 4}[args.model]
+ flows = inference_model(model, image_paths[: num - 1], image_paths[1:num]) # list of numpy.ndarray
+ torch_flows = [torch.from_numpy(flow).unsqueeze(0).to(args.device) for flow in flows] # * list (1,h,w,2)
+ for i, flow in enumerate(flows):
+ visualize_flow(flow, output / f"{i:04d}.png")
+
+ # * rolling shutter correctin
+ solver = eval(f"{args.model}_flow")
+ F0tau = solver(*torch_flows[:num], args.gamma, args.tau) # * (1,h,w,2)
+
+ # * warp image
+ rs_path = image_paths[num // 2]
+ tsfm = transforms.Compose([transforms.ToTensor()])
+ rs_image = tsfm(Image.open(rs_path).convert("RGB")).unsqueeze(0).to(args.device) # * (1,3,h,w)
+ rsc_image = feats_sampling(rs_image, -F0tau)
+
+ # * save image
+ save_image(rsc_image, output / f"rsc_{rs_path.stem}.png")
diff --git a/rspy/demo/rs_00093.jpg b/rspy/demo/rs_00093.jpg
new file mode 100755
index 0000000..e8c698f
Binary files /dev/null and b/rspy/demo/rs_00093.jpg differ
diff --git a/rspy/demo/rs_00094.jpg b/rspy/demo/rs_00094.jpg
new file mode 100755
index 0000000..4add0d3
Binary files /dev/null and b/rspy/demo/rs_00094.jpg differ
diff --git a/rspy/demo/rs_00095.jpg b/rspy/demo/rs_00095.jpg
new file mode 100755
index 0000000..c192999
Binary files /dev/null and b/rspy/demo/rs_00095.jpg differ
diff --git a/rspy/demo/rs_00096.jpg b/rspy/demo/rs_00096.jpg
new file mode 100755
index 0000000..350993d
Binary files /dev/null and b/rspy/demo/rs_00096.jpg differ
diff --git a/rspy/example.py b/rspy/example.py
deleted file mode 100644
index 3df5ba1..0000000
--- a/rspy/example.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Example of code."""
-
-
-def hello(name: str) -> str:
- """Just an greetings example.
-
- Args:
- name (str): Name to greet.
-
- Returns:
- str: greeting message
-
- Examples:
- .. code:: python
-
- >>> hello("Roman")
- 'Hello Roman!'
- """
- return f"Hello {name}!"
diff --git a/rspy/solver.py b/rspy/solver.py
new file mode 100644
index 0000000..9580eab
--- /dev/null
+++ b/rspy/solver.py
@@ -0,0 +1,119 @@
+import torch
+from einops import rearrange
+
+
+def linear_flow(F01: torch.Tensor, gamma: float, tau: float) -> torch.Tensor:
+ """solve the linear motion matrix and predict the correction feild.
+ Args:
+ F01: torch.Tensor (b,h,w,2), flow 0 -> 1
+ gamma: float, the readout reatio
+ tau: float, the timestamp warping to
+ Returns:
+ torch.Tensor: the correction feild to tau.
+ """
+ h, w = F01.shape[1:3]
+ t01 = 1 + gamma / h * F01[:, :, :, 1] # * (b, h, w)
+
+ # solve the linear motion matrix
+ M = F01 / rearrange(t01, "b h w -> b h w 1") # * (b, h, w, 2)
+
+ # predict the correction feild
+ grid_y, _ = torch.meshgrid(
+ torch.arange(0, h, device=F01.device, requires_grad=False),
+ torch.arange(0, w, device=F01.device, requires_grad=False),
+ ) # * (h, w)
+
+ t0tau = tau - gamma / h * grid_y # * (h, w)
+ F0tau = rearrange(t0tau, "h w -> h w 1") * M # * (b, h, w, 2)
+
+ return F0tau
+
+
+def quadratic_flow(F0n1: torch.Tensor, F01: torch.Tensor, gamma: float, tau: float) -> torch.Tensor:
+ """solve the quadratic motion matrix and predict the correction feild.
+ Args:
+ F0n1: torch.Tensor (b,h,w,2), flow 0 -> -1
+ F01: torch.Tensor (b,h,w,2), flow 0 -> 1
+ gamma (float): the readout reatio
+ tau (float): the timestamp warping to
+ Returns:
+ torch.Tensor: the correction feild to tau.
+ """
+ h, w = F0n1.shape[1:3]
+ t0n1 = -1 + gamma / h * F0n1[:, :, :, 1] # * (b, h, w)
+ t01 = 1 + gamma / h * F01[:, :, :, 1] # * (b, h, w)
+
+ # solve the quadratic motion matrix
+ A = rearrange(
+ torch.stack([t0n1, 0.5 * t0n1 ** 2, t01, 0.5 * t01 ** 2], dim=-1),
+ "b h w (m n) -> b h w m n",
+ m=2,
+ n=2,
+ ) # * (b, h, w, 2, 2)
+
+ B = torch.stack([F0n1, F01], dim=-2) # * (b, h, w, 2, 2)
+ M = torch.linalg.solve(A, B) # * (b, h, w, 2, 2)
+
+ # predict the correction feild
+ grid_y, _ = torch.meshgrid(
+ torch.arange(0, h, device=F0n1.device, requires_grad=False),
+ torch.arange(0, w, device=F0n1.device, requires_grad=False),
+ )
+ t0tau = tau - gamma / h * grid_y # * (h, w)
+
+ Atau = rearrange(torch.stack([t0tau, 0.5 * t0tau ** 2], dim=-1), "h w m -> h w 1 m") # * (h, w, 1, 2)
+ F0tau = rearrange(Atau @ M, "b h w 1 n -> b h w n") # * (b, h, w, 2)
+
+ return F0tau
+
+
+def cubic_flow(F0n2: torch.Tensor, F0n1: torch.Tensor, F01: torch.Tensor, gamma: float, tau: float) -> torch.Tensor:
+ """solve the cubic motion matrix and predict the correction feild.
+ Args:
+ F0n1: torch.Tensor (b,h,w,2): flow 0 -> -1
+ F01: torch.Tensor (b,h,w,2): flow 0 -> 1
+ F02: torch.Tensor (b,h,w,2): flow 0 -> 2
+ gamma: (float): the readout reatio
+ tau: (float): the timestamp warping to
+ Returns:
+ torch.Tensor: the correction feild to tau.
+ """
+ h, w = F0n1.shape[1:3]
+ t0n2 = -2 + gamma / h * F0n2[:, :, :, 1]
+ t0n1 = -1 + gamma / h * F0n1[:, :, :, 1]
+ t01 = 1 + gamma / h * F01[:, :, :, 1]
+
+ # solve the quadratic motion matrix
+ A = rearrange(
+ torch.stack(
+ [
+ t0n2,
+ 0.5 * t0n2 ** 2,
+ 1 / 6 * t0n2 ** 3,
+ t0n1,
+ 0.5 * t0n1 ** 2,
+ 1 / 6 * t0n1 ** 3,
+ t01,
+ 0.5 * t01 ** 2,
+ 1 / 6 * t01 ** 3,
+ ],
+ dim=-1,
+ ),
+ "b h w (m n) -> b h w m n",
+ m=3,
+ n=3,
+ ) # * (b, h, w, 3, 3)
+ B = torch.stack([F0n2, F0n1, F01], dim=-2) # * (b, h, w, 3, 2)
+ M = torch.linalg.solve(A, B) # * (b, h, w, 3, 2)
+
+ # predict the correction feild
+ grid_y, _ = torch.meshgrid(
+ torch.arange(0, h, device=F0n1.device, requires_grad=False),
+ torch.arange(0, w, device=F0n1.device, requires_grad=False),
+ )
+ t0tau = tau - gamma / h * grid_y # * (h, w)
+
+ Atau = rearrange(torch.stack([t0tau, 0.5 * t0tau ** 2, 1 / 6 * t0tau ** 3], dim=-1), "h w m -> h w 1 m")
+ F0tau = rearrange(Atau @ M, "b h w 1 n -> b h w n") # * (b, h, w, 2)
+
+ return F0tau
diff --git a/rspy/utils.py b/rspy/utils.py
new file mode 100644
index 0000000..945a5c5
--- /dev/null
+++ b/rspy/utils.py
@@ -0,0 +1,41 @@
+import numpy as np
+import torch
+import torch.nn.functional as F
+
+
+def feats_sampling(
+ x,
+ flow,
+ interpolation="bilinear",
+ padding_mode="zeros",
+ align_corners=True,
+):
+ """return warped images with flows in shape(B, C, H, W)
+ Args:
+ x: shape(B, C, H, W)
+ flow: shape(B, H, W, 2)
+ """
+ if x.size()[-2:] != flow.size()[1:3]:
+ raise ValueError(
+ f"The spatial sizes of input ({x.size()[-2:]}) and " f"flow ({flow.size()[1:3]}) are not the same."
+ )
+ h, w = x.shape[-2:]
+
+ # create mesh grid
+ grid_y, grid_x = torch.meshgrid(torch.arange(0, h), torch.arange(0, w))
+ grid = torch.stack((grid_x, grid_y), 2).type_as(x) #! (h, w, 2)
+ grid.requires_grad = False
+ grid_flow = grid + flow
+
+ # scale grid_flow to [-1,1]
+ grid_flow_x = 2.0 * grid_flow[:, :, :, 0] / max(w - 1, 1) - 1.0
+ grid_flow_y = 2.0 * grid_flow[:, :, :, 1] / max(h - 1, 1) - 1.0
+ grid_flow = torch.stack((grid_flow_x, grid_flow_y), dim=3)
+ output = F.grid_sample(
+ x,
+ grid_flow,
+ mode=interpolation,
+ padding_mode=padding_mode,
+ align_corners=align_corners,
+ )
+ return output
diff --git a/scripts/GPark_QRST_seq_cropped.mp4 b/scripts/GPark_QRST_seq_cropped.mp4
new file mode 100644
index 0000000..fb57726
Binary files /dev/null and b/scripts/GPark_QRST_seq_cropped.mp4 differ
diff --git a/scripts/GPark_rs_seq_cropped.mp4 b/scripts/GPark_rs_seq_cropped.mp4
new file mode 100644
index 0000000..2888b0a
Binary files /dev/null and b/scripts/GPark_rs_seq_cropped.mp4 differ
diff --git a/scripts/run.sh b/scripts/run.sh
new file mode 100644
index 0000000..ca13889
--- /dev/null
+++ b/scripts/run.sh
@@ -0,0 +1,29 @@
+# python video_to_gif.py \
+# --rsv=/home/qdl/Desktop/PJLAB/pj/QRST/RSC/videos/3GS_rs_seq.mp4 \
+# --rscv=/home/qdl/Desktop/PJLAB/pj/QRST/RSC/videos/3GS_QRST_seq.mp4 \
+# --gif=3gs.gif \
+# --step=1 \
+# --fps=10 \
+# --rs_delay=2 \
+# --start=50
+
+# * GPark
+# python video_crop.py \
+# --input=/home/qdl/Desktop/PJLAB/pj/QRST/RSC/videos/GPark_QRST_seq.mp4 \
+# --output=GPark_QRST_seq_cropped.mp4 \
+# --ratio=0.025
+
+# python video_crop.py \
+# --input=/home/qdl/Desktop/PJLAB/pj/QRST/RSC/videos/GPark_rs_seq.mp4 \
+# --output=GPark_rs_seq_cropped.mp4 \
+# --ratio=0.025
+
+python video_to_gif.py \
+ --rsv=GPark_rs_seq_cropped.mp4 \
+ --rscv=GPark_QRST_seq_cropped.mp4 \
+ --gif=gpark.gif \
+ --step=1 \
+ --fps=0.5 \
+ --rs_delay=2 \
+ --start=25 \
+ --end=-10
diff --git a/scripts/video_crop.py b/scripts/video_crop.py
new file mode 100644
index 0000000..a3ce28f
--- /dev/null
+++ b/scripts/video_crop.py
@@ -0,0 +1,33 @@
+# use opencv read video and center crop
+import argparse
+
+import cv2 as cv
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--input", type=str, default="video.mp4", help="video path")
+parser.add_argument("--ratio", type=float, default=0.01, help="crop ratio")
+parser.add_argument("--output", type=str, default="video.mp4", help="output path")
+
+args = parser.parse_args()
+
+if __name__ == "__main__":
+ video = cv.VideoCapture(args.input)
+ FPS = video.get(cv.CAP_PROP_FPS)
+ frames = []
+ while True:
+ ret, frame = video.read()
+ if ret:
+ h, w = frame.shape[:2]
+ h_crop = int(h * args.ratio)
+ w_crop = int(w * args.ratio)
+ frame = frame[:, w_crop : w - w_crop * 2]
+ frame = cv.resize(frame, (640, 480))
+ frames.append(frame)
+ else:
+ break
+ video.release()
+
+ writer = cv.VideoWriter(args.output, cv.VideoWriter_fourcc(*"mp4v"), FPS, (frames[0].shape[1], frames[0].shape[0]))
+ for frame in frames:
+ writer.write(frame)
+ writer.release()
diff --git a/scripts/video_to_gif.py b/scripts/video_to_gif.py
new file mode 100644
index 0000000..17b9056
--- /dev/null
+++ b/scripts/video_to_gif.py
@@ -0,0 +1,43 @@
+# use opencv read video and convert to gif
+import argparse
+
+import cv2 as cv
+import imageio
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--rsv", type=str, default="video.mp4", help="RS video path")
+parser.add_argument("--rscv", type=str, default="video.mp4", help="RSC video path")
+parser.add_argument("--gif", type=str, default="video.gif", help="gif path")
+parser.add_argument("--fps", type=float, default=8, help="gif fps")
+parser.add_argument("--start", type=int, default=0, help="start frame")
+parser.add_argument("--end", type=int, default=-1, help="end frame")
+parser.add_argument("--rs_delay", type=int, default=0, help="RS delay frame")
+parser.add_argument("--step", type=int, default=10, help="step frame")
+parser.add_argument("--downsample", type=int, default=2, help="downsample")
+
+args = parser.parse_args()
+
+if __name__ == "__main__":
+ gif_path = args.gif
+ fps = args.fps
+
+ rs_video = cv.VideoCapture(args.rsv)
+ rsc_video = cv.VideoCapture(args.rscv)
+ frames = []
+ resz = lambda x: cv.resize(x, (x.shape[1] // args.downsample, x.shape[0] // args.downsample))
+
+ for i in range(args.rs_delay):
+ rs_video.read()
+
+ while True:
+ ret_rs, frame_rs = rs_video.read()
+ ret_rsc, frame_rsc = rsc_video.read()
+ if ret_rs and ret_rsc:
+ frame_rs = resz(cv.cvtColor(frame_rs, cv.COLOR_BGR2RGB))
+ frame_rsc = resz(cv.cvtColor(frame_rsc, cv.COLOR_BGR2RGB))
+ frame = cv.hconcat([frame_rs, frame_rsc])
+ frames.append(frame)
+ else:
+ break
+
+ imageio.mimsave(gif_path, frames[args.start : args.end : args.step], "GIF", duration=1.0 / fps)