Last active
April 7, 2022 18:16
-
-
Save Moondarker/430bfac0b48b8f29d6b3633ce97f17b0 to your computer and use it in GitHub Desktop.
Barebones Node.JS + ffmpeg multithreaded image processing scripts used for my r/place zoomed in timelapses
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
~~used for name only~~ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
rem Last stage tool after rplace_crop.js | |
ffmpeg -r 30 -start_number 0 -i out/crop/frame_%03d.png -c:v libx264 -vf fps=60,scale=1024:1024:flags=neighbor -pix_fmt yuv420p result.mp4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Was used to create r/place timelapses | |
Compiles stuff prepared by rplace_subsector_timestamper.js in one video | |
Raw data source: https://place.thatguyalex.com/ | |
*/ | |
const { spawn } = require('child_process') | |
const fs = require('fs') | |
function processSubsector (sector, subsector) { | |
return new Promise(resolve => { | |
const files = fs.readdirSync(`out/${sector}/${subsector}`) | |
fs.writeFileSync(`out/${sector}/${sector}-${subsector}_filelist.txt`, files.sort((a, b) => parseInt(a.match(/(\d+)/)[0]) - parseInt(b.match(/(\d+)/)[0])).map(f => `file '${subsector}/${f}'`).join('\n')) | |
const ffmpeg = spawn('ffmpeg', ['-n', '-r', '30', '-f', 'concat', '-i', `out/${sector}/${sector}-${subsector}_filelist.txt`, '-c:v', 'libx264', '-vf', 'fps=60,scale=1000:1064:flags=neighbor', '-pix_fmt', 'yuv420p', `out/results/${sector}-${subsector}_timestamped.mp4`]) | |
ffmpeg.stdout.on('data', msg => {}) | |
ffmpeg.stderr.on('data', msg => {}) | |
ffmpeg.on('close', code => { | |
resolve(code) | |
}) | |
}) | |
} | |
async function main () { | |
for (let sector = 1; sector <= 16; sector += 1) { | |
for (let subsector = 1; subsector <= 16; subsector += 1) { | |
const retCode = await processSubsector(sector, subsector) | |
if (retCode !== 0 && retCode !== 1) console.log(`Return code ${retCode}`) | |
} | |
console.log(`Sector ${sector} done`) | |
} | |
} | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Was used to create r/place timelapses | |
Crops (line 13, argument goes as follows: width:height:x:y) and numbers images incrementally | |
Can be used after rplace_merge.js or separately | |
Raw data source: https://place.thatguyalex.com/ | |
*/ | |
const { spawn } = require('child_process') | |
const fs = require('fs') | |
const chunkSize = 100 | |
function processImage (filename, id) { | |
return new Promise(resolve => { | |
const ls = spawn('ffmpeg', ['-n', '-i', `out/full/${filename}`, '-vf', 'crop=128:128:0:963', `out/crop/frame_${id}.png`]) | |
ls.on('close', code => { | |
resolve(code) | |
}) | |
}) | |
} | |
async function chunkThread (chunk, threadId) { | |
let currentPos = 0 | |
while (currentPos < chunk.length) { | |
const retCode = await processImage(chunk[currentPos], (chunkSize * threadId) + currentPos) | |
if (retCode !== 0 && retCode !== 1) console.log(`Return code ${retCode}`) | |
currentPos++ | |
} | |
console.log(`Thread ${threadId} done`) | |
} | |
const srcArray = fs.readdirSync('out/full') | |
const srcArrayChunks = [] | |
for (let i = 0; i < srcArray.length; i += chunkSize) { | |
srcArrayChunks[srcArrayChunks.length] = srcArray.slice(i, i + chunkSize) | |
} | |
srcArrayChunks.forEach((chunk, idx) => { | |
chunkThread(chunk, idx) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Was used to create r/place timelapses | |
Stitches together two (or more - just adjust ffmpeg arguments) images | |
Use if your art was placed on/near the borders | |
First stage tool for rplace_crop.js | |
Raw data source: https://place.thatguyalex.com/ | |
*/ | |
const { spawn } = require('child_process') | |
const fs = require('fs') | |
function processImage (filename) { | |
return new Promise(resolve => { | |
const ls = spawn('ffmpeg', ['-n', '-i', `src/0/${filename}`, '-i', `src/2/${filename.replace('0-', '2-')}`, '-filter_complex', 'vstack', `out/full/${filename.replace('0-', '')}`]) | |
ls.on('close', code => { | |
resolve(code) | |
}) | |
}) | |
} | |
async function chunkThread (chunk, threadId) { | |
let currentPos = 0 | |
while (currentPos < chunk.length) { | |
const retCode = await processImage(chunk[currentPos]) | |
if (retCode !== 0 && retCode !== 1) console.log(`Return code ${retCode}`) | |
currentPos++ | |
} | |
console.log(`Thread ${threadId} done`) | |
} | |
const srcArray = fs.readdirSync('src/0') | |
const srcArrayChunks = [] | |
const chunkSize = 100 | |
for (let i = 0; i < srcArray.length; i += chunkSize) { | |
srcArrayChunks[srcArrayChunks.length] = srcArray.slice(i, i + chunkSize) | |
} | |
srcArrayChunks.forEach((chunk, idx) => { | |
chunkThread(chunk, idx) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Was used to create r/place timelapses | |
Gets raw images, splits into sectors and subsectors, adds timestamps | |
First stage tool for rplace_compile_timestamped.js | |
IMPORTANT: Expects a font file named sp.ttf - I recommend using small_pixel.ttf | |
Raw data source: https://place.thatguyalex.com/ | |
*/ | |
const { spawn } = require('child_process') | |
const fs = require('fs') | |
const canvas = 0 | |
const chunkSize = 250 // Make bigger if your PC can't handle so many processes | |
const sectors = [[[1, [0, 0]], [2, [500, 0]], [5, [0, 500]], [6, [500, 500]]], [[3, [0, 0]], [4, [500, 0]], [7, [0, 500]], [8, [500, 500]]], | |
[[9, [0, 0]], [10, [500, 0]], [13, [0, 500]], [14, [500, 500]]], [[11, [0, 0]], [12, [500, 0]], [15, [0, 500]], [16, [500, 500]]]] | |
function processImage (filename, id) { | |
return new Promise(resolve => { | |
const args = ['-n', '-i', `src/${canvas}/${filename}`] | |
sectors[canvas].forEach((sectorData, sectorId) => { | |
const sector = sectorData[0] | |
const sectorCoords = sectorData[1] | |
for (let subsector = 1; subsector <= 16; subsector++) { | |
args.push('-vf', `crop=125:125:${sectorCoords[0] + 125 * ((subsector - 1) % 4)}:${sectorCoords[1] + 125 * Math.floor((subsector - 1) / 4)},pad=125:133:0:0:black,drawtext=text='${(new Date(parseInt(filename.substring(2, 12)) * 1000)).toGMTString().substring(5).replaceAll(':', '\\:')}':fontcolor=white:fontfile=sp.ttf:fontsize=8:x=1:y=126`, `out/${sector}/${subsector}/${id}_${filename}`) | |
} | |
}) | |
const ffmpeg = spawn('ffmpeg', args) | |
ffmpeg.on('error', err => { | |
console.err(`ffmpeg #${id}: ${err}`) | |
}) | |
ffmpeg.on('close', code => { | |
resolve(code) | |
}) | |
}) | |
} | |
async function chunkThread (chunk, threadId) { | |
let currentPos = 0 | |
while (currentPos < chunk.length) { | |
const retCode = await processImage(chunk[currentPos], (chunkSize * threadId) + currentPos) | |
if (retCode !== 0 && retCode !== 1) console.log(`Return code ${retCode}`) | |
currentPos++ | |
} | |
console.log(`Thread ${threadId} done`) | |
} | |
const srcArray = fs.readdirSync(`src/${canvas}`) | |
const srcArrayChunks = [] | |
for (let i = 0; i < srcArray.length; i += chunkSize) { | |
srcArrayChunks[srcArrayChunks.length] = srcArray.slice(i, i + chunkSize) | |
} | |
srcArrayChunks.forEach((chunk, idx) => { | |
chunkThread(chunk, idx) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment