1+ __version__ = "1.0"
2+
3+ from meshroom .core import desc
4+ from meshroom .core .utils import VERBOSE_LEVEL
5+
6+ class GenerateVideo (desc .CommandLineNode ):
7+ category = "Video Utils"
8+ documentation = """ Generate a video from a sfmData ordered by frameId """
9+
10+ inputs = [
11+ desc .File (
12+ name = "input" ,
13+ label = "Input" ,
14+ description = "SfmData used to get the list of images." ,
15+ value = "" ,
16+ ),
17+ desc .FloatParam (
18+ name = "frameRate" ,
19+ label = "Camera Frame Rate" ,
20+ description = "Define the camera's Frames per second." ,
21+ value = 24.0 ,
22+ range = (1.0 , 60.0 , 1.0 ),
23+ ),
24+ desc .ChoiceParam (
25+ name = "verboseLevel" ,
26+ label = "Verbose Level" ,
27+ description = "Verbosity level (fatal, error, warning, info, debug, trace)." ,
28+ values = VERBOSE_LEVEL ,
29+ value = "info" ,
30+ )
31+ ]
32+
33+ outputs = [
34+ desc .File (
35+ name = 'directory' ,
36+ label = 'Intermediate directory' ,
37+ description = "Intermediate directory" ,
38+ value = "{nodeCacheFolder}" ,
39+ ),
40+ desc .File (
41+ name = 'outputVideo' ,
42+ label = 'Output Video' ,
43+ description = "Generated video." ,
44+ value = "{nodeCacheFolder}/output.mp4" ,
45+ )
46+ ]
47+
48+ def processChunk (self , chunk ):
49+
50+ from pathlib import Path
51+
52+ import logging
53+ logging .getLogger ().setLevel (chunk .node .verboseLevel .value .upper ())
54+
55+ from pyalicevision import sfmDataIO as avsfmdataio
56+ from pyalicevision import sfmData as avsfmdata
57+
58+ logging .info ("Open input file" )
59+ data = avsfmdata .SfMData ()
60+ ret = avsfmdataio .load (data , chunk .node .input .value , avsfmdataio .VIEWS )
61+ if not ret :
62+ logging .error ("Cannot open input" )
63+ raise RuntimeError ()
64+
65+ # store all image path in sfmData indexed by frame id
66+ views = data .getViews ()
67+ viewsPerFrameId = {}
68+ for viewId in views :
69+ view = views [viewId ]
70+ frameId = view .getFrameId ()
71+ viewsPerFrameId [frameId ] = view .getImage ().getImagePath ()
72+
73+
74+ # sort path per frameId
75+ viewsPerFrameId = dict (sorted (viewsPerFrameId .items ()))
76+ # Check intermediate directory
77+ intermediate_directory = Path (chunk .node .directory .value )
78+ if not (intermediate_directory .exists () and intermediate_directory .is_dir ()):
79+ logging .error ("Intermediate directory is not valid" )
80+ raise RuntimeError ()
81+
82+ pos = 0
83+ suffix = ""
84+ for frameId , path in viewsPerFrameId .items ():
85+ target = Path (path )
86+ if suffix == "" :
87+ suffix = target .suffix
88+ if target .suffix .lower () != suffix .lower ():
89+ logging .error ("Multiple image types found." )
90+ raise RuntimeError ()
91+
92+ # Create a link using a sequential order
93+ link = intermediate_directory / f"{ pos :012d} { suffix } "
94+
95+ # Force symlink creation
96+ if link .exists () or link .is_symlink ():
97+ link .unlink ()
98+
99+ # Create symbolic link
100+ link .symlink_to (target )
101+ pos = pos + 1
102+
103+ self .commandLine = f"ffmpeg -framerate 24 -start_number 0 -i %012d{ suffix } -vf \" scale=trunc(iw/2)*2:trunc(ih/2)*2\" -c:v libx264 -pix_fmt yuv420p { chunk .node .outputVideo .value } "
104+ desc .CommandLineNode .processChunk (self , chunk )
0 commit comments