mirror of
https://github.com/ychalier/datamoshing.git
synced 2026-06-16 12:31:19 +02:00
removed optical-flow-transfer folder
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
# Optical Flow Transfer
|
||||
|
||||
Transfer optical flow from one video to an image.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You'll need a working installation of [Python 3](https://www.python.org/), and [FFmpeg](https://ffmpeg.org/). Make sure they are in PATH.
|
||||
|
||||
### Installation
|
||||
|
||||
Download or clone this repository:
|
||||
|
||||
```console
|
||||
git clone https://github.com/ychalier/datamoshing.git
|
||||
cd datamoshing/optical-flow-transfer/
|
||||
```
|
||||
|
||||
Install the requirements:
|
||||
|
||||
```console
|
||||
pip -m install requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Simply execute the main script:
|
||||
|
||||
```console
|
||||
python optical_flow_transfer.py <source-video> <source-image> <output-video>
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
[](https://drive.chalier.fr/protected/datamoshing/optical-flow-transfer-output.mp4)
|
||||
|
||||
I wrote some details [on my blog](https://chalier.fr/blog/datamoshing#opticalflowtransfer).
|
||||
|
||||
## Demo
|
||||
|
||||
Here is a [web demonstration](www/) where you may try different video sources (such as the webcam) and image sources to perform optical flow transfer.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 MiB |
@@ -1,110 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
import argparse
|
||||
import subprocess
|
||||
import cv2
|
||||
import numpy
|
||||
import tqdm
|
||||
import PIL.Image
|
||||
|
||||
|
||||
def transfer_optical_flow(video_path, image_path, frame_folder=".frames"):
|
||||
if os.path.isdir(frame_folder):
|
||||
shutil.rmtree(frame_folder)
|
||||
os.makedirs(frame_folder)
|
||||
video_capture = cv2.VideoCapture(video_path)
|
||||
_, video_first_frame = video_capture.read()
|
||||
prev_gray = cv2.cvtColor(video_first_frame, cv2.COLOR_BGR2GRAY)
|
||||
reference_frame = PIL.Image.open(image_path)
|
||||
frame_index = 0
|
||||
reference_frame.save(os.path.join(
|
||||
frame_folder,
|
||||
"%06d.jpg" % frame_index
|
||||
))
|
||||
image_frame = numpy.array(reference_frame)
|
||||
height, width, depth = image_frame.shape
|
||||
framerate = video_capture.get(cv2.CAP_PROP_FPS)
|
||||
frame_count = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
pbar = tqdm.tqdm(
|
||||
unit="frame",
|
||||
total=frame_count,
|
||||
desc="Computing optical flow"
|
||||
)
|
||||
base = numpy.zeros(height * width * depth, dtype=int)
|
||||
for l in range(height * width * depth):
|
||||
base[l] = l
|
||||
while video_capture.isOpened():
|
||||
pbar.update()
|
||||
frame_index += 1
|
||||
_, video_frame = video_capture.read()
|
||||
if video_frame is None:
|
||||
break
|
||||
gray = cv2.cvtColor(video_frame, cv2.COLOR_BGR2GRAY)
|
||||
flow = cv2.calcOpticalFlowFarneback(
|
||||
prev=prev_gray,
|
||||
next=gray,
|
||||
flow=None,
|
||||
pyr_scale=0.5,
|
||||
levels=3,
|
||||
winsize=15,
|
||||
iterations=3,
|
||||
poly_n=5,
|
||||
poly_sigma=1.2,
|
||||
flags=0
|
||||
).astype(int)
|
||||
flow_flat = numpy.repeat(
|
||||
flow[:, :, 1] * width * depth + flow[:, :, 0] * depth,
|
||||
depth
|
||||
)
|
||||
numpy.put(image_frame, base + flow_flat, image_frame.flat, mode="wrap")
|
||||
PIL.Image.fromarray(image_frame).save(os.path.join(
|
||||
frame_folder,
|
||||
"%06d.jpg" % frame_index
|
||||
))
|
||||
prev_gray = gray
|
||||
pbar.close()
|
||||
video_capture.release()
|
||||
return framerate
|
||||
|
||||
|
||||
def create_output_video(frame_folder, framerate, output_path=".frames"):
|
||||
subprocess.Popen([
|
||||
"ffmpeg",
|
||||
"-loglevel",
|
||||
"quiet",
|
||||
"-stats",
|
||||
"-hide_banner",
|
||||
"-framerate",
|
||||
"%.2f" % framerate,
|
||||
"-i",
|
||||
os.path.join(frame_folder, "%06d.jpg"),
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
output_path,
|
||||
"-y"
|
||||
]).wait()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("video_path", type=str)
|
||||
parser.add_argument("image_path", type=str)
|
||||
parser.add_argument("output_path", type=str)
|
||||
parser.add_argument("-f", "--frame-folder", type=str, default=".frames")
|
||||
args = parser.parse_args()
|
||||
framerate = transfer_optical_flow(
|
||||
args.video_path,
|
||||
args.image_path,
|
||||
args.frame_folder
|
||||
)
|
||||
create_output_video(
|
||||
args.frame_folder,
|
||||
framerate,
|
||||
args.output_path
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +0,0 @@
|
||||
tqdm
|
||||
Pillow
|
||||
python-opencv
|
||||
numpy
|
||||
@@ -1,106 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Optical Flow Transfer</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/ychalier/pifekit/pifekit.min.css" />
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/ychalier/pifekit/pifekit.min.js"></script>
|
||||
<script src="opencv.js"></script>
|
||||
<script src="master.js"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body class="dark-blue">
|
||||
<div class="container text-center">
|
||||
<h1>Optical Flow Transfer</h1>
|
||||
|
||||
<div class="xcolumns">
|
||||
|
||||
<div class="xcolumn-left">
|
||||
<div class="media-container" id="container-video">
|
||||
<button class="btn btn-primary" onclick="showModal('modal-load-video')" id="button-video-modal">Load video</button>
|
||||
</div>
|
||||
<div style="height: .4em"></div>
|
||||
<div class="media-container" id="container-image">
|
||||
<button class="btn btn-primary" onclick="showModal('modal-load-image')" id="button-image-modal">Load image</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="xcolumn-right">
|
||||
<div id="container-canvas" rowspan="2">
|
||||
<canvas id="canvas" width="256" height="256"></canvas>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button class="btn" id="button-reset" disabled>Reset</button>
|
||||
<button class="btn" id="button-download" disabled>Download</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<footer class="text-center">
|
||||
<p>
|
||||
More details on my <a href="https://chalier.fr/blog/datamoshing">blog</a> · Images from <a href="https://picsum.photos/">Lorem Picsum</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<div class="modal" id="modal-load-video">
|
||||
<span class="modal-overlay" onclick="closeModal('modal-load-video')"></span>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title h5">Load video</div>
|
||||
<div class="modal-subtitle text-gray">Load a video from a URL, a local file, or your webcam</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group pb-2">
|
||||
<label class="form-label" for="input-video-url">URL</label>
|
||||
<input class="form-input" type="url" id="input-video-url" placeholder="https://example.org/axis-cgi/mjpg/video.cgi" />
|
||||
</div>
|
||||
<div class="divider text-center" data-content="OR"></div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="input-video-file">File</label>
|
||||
<input class="form-input" type="file" id="input-video-file" accept="video/*" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" id="button-video-webcam">Webcam</button>
|
||||
<button class="btn btn-primary" id="button-video-load">Load</button>
|
||||
<button class="btn" onclick="closeModal('modal-load-video')">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-load-image">
|
||||
<span class="modal-overlay" onclick="closeModal('modal-load-image')"></span>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title h5">Load image</div>
|
||||
<div class="modal-subtitle text-gray">Load an image from a URL or a local file</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group pb-2">
|
||||
<label class="form-label" for="input-image-url">URL</label>
|
||||
<input class="form-input" type="url" id="input-image-url" placeholder="https://example.org/image.jpg" />
|
||||
</div>
|
||||
<div class="divider text-center" data-content="OR"></div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="input-image-file">File</label>
|
||||
<input class="form-input" type="file" id="input-image-file" accept="image/*" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" id="button-image-random">Random</button>
|
||||
<button class="btn btn-primary" id="button-image-load">Load</button>
|
||||
<button class="btn" onclick="closeModal('modal-load-image')">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,198 +0,0 @@
|
||||
navigator.getMedia = (navigator.getUserMedia ||
|
||||
navigator.webkitGetUserMedia ||
|
||||
navigator.mozGetUserMedia ||
|
||||
navigator.msGetUserMedia);
|
||||
|
||||
async function load_effect() {
|
||||
|
||||
var video_loaded = false;
|
||||
var image_loaded = false;
|
||||
|
||||
const size = 256;
|
||||
const width = size;
|
||||
const height = size;
|
||||
|
||||
const video_container = document.getElementById("container-video");
|
||||
const video = document.createElement("video");
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.style.objectFit = "cover";
|
||||
video.setAttribute("width", size);
|
||||
video.setAttribute("height", size);
|
||||
video.addEventListener("canplay", () => {
|
||||
if (!video_loaded) {
|
||||
console.log("Video is loaded");
|
||||
video_container.innerHTML = "";
|
||||
video_container.appendChild(video);
|
||||
if (image_loaded) start_effect();
|
||||
}
|
||||
video_loaded = true;
|
||||
}, false);
|
||||
|
||||
const image_container = document.getElementById("container-image");
|
||||
const image = new Image();
|
||||
const image_canvas = document.createElement("canvas");
|
||||
image_canvas.width = width;
|
||||
image_canvas.height = height;
|
||||
const image_canvas_context = image_canvas.getContext("2d");
|
||||
image.crossOrigin = "anonymous";
|
||||
image.addEventListener("load", () => {
|
||||
if (!image_loaded) {
|
||||
console.log("Image is loaded");
|
||||
image_container.innerHTML = "";
|
||||
image_container.appendChild(image_canvas);
|
||||
const target_ratio = width / height;
|
||||
const current_ratio = image.width / image.height;
|
||||
var cropped_width = image.width;
|
||||
var cropped_height = image.height;
|
||||
if (target_ratio > current_ratio) {
|
||||
// Crop vertically
|
||||
cropped_height = cropped_width / target_ratio;
|
||||
} else if (target_ratio < current_ratio) {
|
||||
// Crop horizontaly
|
||||
cropped_width = target_ratio * cropped_height;
|
||||
}
|
||||
image_canvas_context.drawImage(image, (image.width - cropped_width) / 2, (image.height - cropped_height) / 2, cropped_width, cropped_height, 0, 0, width, height);
|
||||
context.drawImage(image, (image.width - cropped_width) / 2, (image.height - cropped_height) / 2, cropped_width, cropped_height, 0, 0, width, height);
|
||||
if (video_loaded) start_effect();
|
||||
}
|
||||
image_loaded = true;
|
||||
}, false);
|
||||
|
||||
const canvas = document.getElementById("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext("2d");
|
||||
var image_data;
|
||||
var cap;
|
||||
|
||||
async function start_effect() {
|
||||
console.log("Both media loaded, starting the effect");
|
||||
|
||||
document.getElementById("button-reset").removeAttribute("disabled");
|
||||
document.getElementById("button-download").removeAttribute("disabled");
|
||||
|
||||
function takepicture() {
|
||||
let frame = new cv.Mat(height, width, cv.CV_8UC4);
|
||||
let frame_gray = new cv.Mat();
|
||||
cap.read(frame);
|
||||
cv.cvtColor(frame, frame_gray, cv.COLOR_RGBA2GRAY);
|
||||
return frame_gray;
|
||||
}
|
||||
|
||||
cap = new cv.VideoCapture(video);
|
||||
let reference = takepicture();
|
||||
image_data = context.getImageData(0, 0, width, height);
|
||||
while (true) {
|
||||
let current = takepicture();
|
||||
let flow = new cv.Mat();
|
||||
cv.calcOpticalFlowFarneback(reference, current, flow, 0.5, 3, 15, 3, 5, 1.2, 0);
|
||||
let image_data_copy = [...image_data.data];
|
||||
for (let i = 0; i < height; i++) {
|
||||
for (let j = 0; j < width; j++) {
|
||||
let k = (i * width + j);
|
||||
let ii = i + Math.round(flow.data32F[k * 2]);
|
||||
let jj = j + Math.round(flow.data32F[k * 2 + 1]);
|
||||
image_data.data[(ii * width + jj) * 4] = image_data_copy[(i * width + j) * 4];
|
||||
image_data.data[(ii * width + jj) * 4 + 1] = image_data_copy[(i * width + j) * 4 + 1];
|
||||
image_data.data[(ii * width + jj) * 4 + 2] = image_data_copy[(i * width + j) * 4 + 2];
|
||||
image_data.data[(ii * width + jj) * 4 + 3] = image_data_copy[(i * width + j) * 4 + 3];
|
||||
}
|
||||
}
|
||||
context.putImageData(image_data, 0, 0);
|
||||
await new Promise(x => requestAnimationFrame(x));
|
||||
current.copyTo(reference);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function load_video_webcam() {
|
||||
console.log("Loading video from the webcam");
|
||||
navigator.getMedia({ video: true, audio: false }, (stream) => {
|
||||
if (navigator.mozGetUserMedia) {
|
||||
video.srcObject = stream;
|
||||
} else {
|
||||
video.src = vendorURL.createObjectURL(stream);
|
||||
}
|
||||
video.play();
|
||||
}, (err) => {
|
||||
alert(err);
|
||||
console.error(err);
|
||||
document.getElementById("button-video-modal").disabled = false;
|
||||
});
|
||||
while (!video_loaded) { await new Promise(resolve => setTimeout(resolve, 10)); }
|
||||
}
|
||||
|
||||
async function load_video_url(video_url) {
|
||||
console.log("Loading video from a URL:", video_url);
|
||||
video.src = video_url;
|
||||
video.play();
|
||||
}
|
||||
|
||||
async function load_video_file(video_file) {
|
||||
console.log("Loading video from a file:", video_file);
|
||||
video.src = URL.createObjectURL(video_file);
|
||||
video.play();
|
||||
}
|
||||
|
||||
document.getElementById("button-video-webcam").addEventListener("click", () => {
|
||||
document.getElementById("button-video-modal").disabled = true;
|
||||
closeModal("modal-load-video");
|
||||
load_video_webcam();
|
||||
});
|
||||
|
||||
document.getElementById("button-video-load").addEventListener("click", () => {
|
||||
let video_url = document.getElementById("input-video-url").value;
|
||||
let video_files = document.getElementById("input-video-file").files;
|
||||
if (video_url != "") {
|
||||
load_video_url(video_url);
|
||||
} else if (video_files.length > 0) {
|
||||
load_video_file(video_files[0]);
|
||||
} else {
|
||||
alert("Please specify one source!");
|
||||
return;
|
||||
}
|
||||
document.getElementById("button-video-modal").disabled = true;
|
||||
closeModal("modal-load-video");
|
||||
});
|
||||
|
||||
document.getElementById("button-image-random").addEventListener("click", () => {
|
||||
console.log("Loading random image");
|
||||
image.src = `https://picsum.photos/${ width }/${ height }`;
|
||||
if (document.getElementById("button-image-modal")) {
|
||||
document.getElementById("button-image-modal").disabled = true;
|
||||
}
|
||||
closeModal("modal-load-image");
|
||||
});
|
||||
|
||||
document.getElementById("button-image-load").addEventListener("click", () => {
|
||||
let image_url = document.getElementById("input-image-url").value;
|
||||
let image_files = document.getElementById("input-image-file").files;
|
||||
if (image_url != "") {
|
||||
console.log("Loading image from a URL:", image_url);
|
||||
image.src = image_url;
|
||||
} else if (image_files.length > 0) {
|
||||
console.log("Loading image from a file:", image_files[0]);
|
||||
image.src = URL.createObjectURL(image_files[0]);
|
||||
} else {
|
||||
alert("Please specify one source!");
|
||||
return;
|
||||
}
|
||||
document.getElementById("button-image-modal").disabled = true;
|
||||
closeModal("modal-load-image");
|
||||
});
|
||||
|
||||
document.getElementById("button-reset").addEventListener("click", () => {
|
||||
image_data = image_canvas_context.getImageData(0, 0, width, height);
|
||||
});
|
||||
|
||||
document.getElementById("button-download").addEventListener("click", () => {
|
||||
const link = document.createElement("a");
|
||||
link.download = `${ Date.now() }.png`;
|
||||
link.href = canvas.toDataURL();
|
||||
link.click();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
window.addEventListener("load", load_effect);
|
||||
File diff suppressed because one or more lines are too long
@@ -1,78 +0,0 @@
|
||||
* {
|
||||
font-family: "Fira Code", monospace !important;
|
||||
color: white;
|
||||
--color-accent-0: hsl(192, 63%, 59%);
|
||||
--color-accent-1: hsl(192, 64%, 61%);
|
||||
--color-accent-2: hsl(192, 63%, 62%);
|
||||
--color-accent-3: hsl(192, 63%, 64%);
|
||||
--color-accent-4: hsl(192, 63%, 66%);
|
||||
--color-accent-5: hsl(192, 63%, 69%);
|
||||
--color-accent-6: hsla(192, 63%, 69%, 0.2);
|
||||
--color-accent-7: hsla(192, 63%, 69%, 0.95);
|
||||
--color-accent-8: hsl(192, 64%, 74%);
|
||||
--color-accent-9: hsl(192, 63%, 79%);
|
||||
--color-accent-a: hsl(192, 62%, 99%);
|
||||
--color-accent-b: hsl(192, 63%, 100%);
|
||||
--color-accent-c: hsl(192, 65%, 13%);
|
||||
--color-accent-d: hsl(192, 67%, 89%);
|
||||
--color-accent-e: hsl(192, 100%, 90%);
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 100vh;
|
||||
background: black;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--color-accent-5);
|
||||
}
|
||||
|
||||
table {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
#container-canvas canvas {
|
||||
width: 512px;
|
||||
height: 512px;
|
||||
}
|
||||
|
||||
.media-container {
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
border: .2em solid white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#container-canvas {
|
||||
box-sizing: content-box;
|
||||
width: 512px;
|
||||
height: 512px;
|
||||
border: .6em solid white;
|
||||
}
|
||||
|
||||
canvas, video, image {
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.xcolumns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.xcolumn-right, .xcolumn-left {
|
||||
margin: .4em;
|
||||
}
|
||||
Reference in New Issue
Block a user