Skip to content
Snippets Groups Projects
Commit 05b6fda8 authored by Sybren A. Stüvel's avatar Sybren A. Stüvel
Browse files

Added `merge_progressive_render_sequence` command

Added the `merge_progressive_render_sequence` for sample-merging sequences of EXR files. The
already-existing `merge_progressive_renders` command only performed on one frame at a time.

This commit also removes the last traces of the preview generation that
half-happened while merging. It was intrinsincly flawed and was superseded
by the `exr_sequence_to_jpeg` command.
parent ce734c8c
Branches
No related tags found
No related merge requests found
...@@ -10,6 +10,8 @@ changed functionality, fixed bugs). ...@@ -10,6 +10,8 @@ changed functionality, fixed bugs).
Server are no longer supported. Server are no longer supported.
- Added the `exr_sequence_to_jpeg` command. This command uses Blender to convert a sequence of - Added the `exr_sequence_to_jpeg` command. This command uses Blender to convert a sequence of
EXR files to JPEG files. This is used in progressive rendering to get intermediary previews. EXR files to JPEG files. This is used in progressive rendering to get intermediary previews.
- Added the `merge_progressive_render_sequence` for sample-merging sequences of EXR files. The
already-existing `merge_progressive_renders` command only performed on one frame at a time.
## Version 2.2.1 (2019-01-14) ## Version 2.2.1 (2019-01-14)
......
...@@ -45,14 +45,52 @@ nodes["image2"].image = image2 ...@@ -45,14 +45,52 @@ nodes["image2"].image = image2
nodes["weight1"].outputs[0].default_value = %(weight1)i nodes["weight1"].outputs[0].default_value = %(weight1)i
nodes["weight2"].outputs[0].default_value = %(weight2)i nodes["weight2"].outputs[0].default_value = %(weight2)i
nodes["output"].base_path = "%(tmpdir)s"
scene.render.resolution_x, scene.render.resolution_y = image1.size scene.render.resolution_x, scene.render.resolution_y = image1.size
scene.render.tile_x, scene.render.tile_y = image1.size scene.render.tile_x, scene.render.tile_y = image1.size
scene.render.filepath = "%(tmpdir)s/preview.jpg" scene.render.filepath = "%(tmpdir)s/merged0001.exr"
scene.frame_start = 1
scene.frame_end = 1
scene.frame_set(1)
bpy.ops.render.render(write_still=True) bpy.ops.render.render(write_still=True)
""" """
MERGE_EXR_SEQUENCE_PYTHON = """\
import bpy
scene = bpy.context.scene
nodes = scene.node_tree.nodes
image1 = bpy.data.images.load("%(input1)s")
image2 = bpy.data.images.load("%(input2)s")
image1.source = 'SEQUENCE'
image2.source = 'SEQUENCE'
node_img1 = nodes["image1"]
node_img2 = nodes["image2"]
node_img1.image = image1
node_img2.image = image2
node_img1.frame_duration = %(frame_end)d - %(frame_start)d + 1
node_img2.frame_duration = %(frame_end)d - %(frame_start)d + 1
node_img1.frame_start = %(frame_start)d
node_img2.frame_start = %(frame_start)d
node_img1.frame_offset = %(frame_start)d - 1
node_img2.frame_offset = %(frame_start)d - 1
nodes["weight1"].outputs[0].default_value = %(weight1)i
nodes["weight2"].outputs[0].default_value = %(weight2)i
scene.render.resolution_x, scene.render.resolution_y = image1.size
scene.render.tile_x, scene.render.tile_y = image1.size
scene.render.filepath = "%(output)s"
scene.frame_start = %(frame_start)d
scene.frame_end = %(frame_end)d
bpy.ops.render.render(animation=True)
"""
HASHES_RE = re.compile('#+')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -330,6 +368,15 @@ def _numbered_path(directory: Path, fname_prefix: str, fname_suffix: str) -> Pat ...@@ -330,6 +368,15 @@ def _numbered_path(directory: Path, fname_prefix: str, fname_suffix: str) -> Pat
return directory / f'{fname_prefix}{max_nr + 1:03}{fname_suffix}' return directory / f'{fname_prefix}{max_nr + 1:03}{fname_suffix}'
def _hashes_to_glob(path: Path) -> Path:
"""Transform bla-#####.exr to bla-*.exr.
>>> _hashes_to_glob(Path('/path/to/bla-####.exr'))
Path('/path/to/bla-*.exr')
"""
return path.with_name(HASHES_RE.sub('*', path.name))
@command_executor('move_out_of_way') @command_executor('move_out_of_way')
class MoveOutOfWayCommand(AbstractCommand): class MoveOutOfWayCommand(AbstractCommand):
def validate(self, settings: Settings): def validate(self, settings: Settings):
...@@ -918,6 +965,8 @@ class BlenderRenderProgressiveCommand(BlenderRenderCommand): ...@@ -918,6 +965,8 @@ class BlenderRenderProgressiveCommand(BlenderRenderCommand):
@command_executor('merge_progressive_renders') @command_executor('merge_progressive_renders')
class MergeProgressiveRendersCommand(AbstractSubprocessCommand): class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
script_template = MERGE_EXR_PYTHON
def validate(self, settings: Settings): def validate(self, settings: Settings):
blender_cmd, err = self._setting(settings, 'blender_cmd', True) blender_cmd, err = self._setting(settings, 'blender_cmd', True)
if err: if err:
...@@ -928,20 +977,23 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): ...@@ -928,20 +977,23 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
settings['blender_cmd'] = cmd settings['blender_cmd'] = cmd
input1, err = self._setting(settings, 'input1', True, str) input1, err = self._setting(settings, 'input1', True, str)
if err: return err if err:
return err
if '"' in input1: if '"' in input1:
return 'Double quotes are not allowed in filenames: %r' % input1 return 'Double quotes are not allowed in filenames: %r' % input1
if not Path(input1).exists(): if not Path(input1).exists():
return 'Input 1 %r does not exist' % input1 return 'Input 1 %r does not exist' % input1
input2, err = self._setting(settings, 'input2', True, str) input2, err = self._setting(settings, 'input2', True, str)
if err: return err if err:
return err
if '"' in input2: if '"' in input2:
return 'Double quotes are not allowed in filenames: %r' % input2 return 'Double quotes are not allowed in filenames: %r' % input2
if not Path(input2).exists(): if not Path(input2).exists():
return 'Input 2 %r does not exist' % input2 return 'Input 2 %r does not exist' % input2
output, err = self._setting(settings, 'output', True, str) output, err = self._setting(settings, 'output', True, str)
if err: return err if err:
return err
if '"' in output: if '"' in output:
return 'Double quotes are not allowed in filenames: %r' % output return 'Double quotes are not allowed in filenames: %r' % output
...@@ -950,24 +1002,17 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): ...@@ -950,24 +1002,17 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
settings['output'] = Path(output).as_posix() settings['output'] = Path(output).as_posix()
_, err = self._setting(settings, 'weight1', True, int) _, err = self._setting(settings, 'weight1', True, int)
if err: return err if err:
return err
_, err = self._setting(settings, 'weight2', True, int) _, err = self._setting(settings, 'weight2', True, int)
if err: return err if err:
return err
return super().validate(settings) return super().validate(settings)
async def execute(self, settings: Settings): async def execute(self, settings: Settings):
blendpath = Path(__file__).parent / 'resources/merge-exr.blend' cmd = self._base_blender_cli(settings)
cmd = settings['blender_cmd'][:]
cmd += [
'--factory-startup',
'--enable-autoexec',
'-noaudio',
'--background',
blendpath.as_posix(),
]
# set up node properties and render settings. # set up node properties and render settings.
output = Path(settings['output']) output = Path(settings['output'])
...@@ -979,7 +1024,7 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): ...@@ -979,7 +1024,7 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
settings['tmpdir'] = tmppath.as_posix() settings['tmpdir'] = tmppath.as_posix()
cmd += [ cmd += [
'--python-expr', MERGE_EXR_PYTHON % settings '--python-expr', self.script_template % settings
] ]
await self.worker.register_task_update(activity='Starting Blender to merge EXR files') await self.worker.register_task_update(activity='Starting Blender to merge EXR files')
...@@ -987,11 +1032,23 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): ...@@ -987,11 +1032,23 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
# move output files into the correct spot. # move output files into the correct spot.
await self.move(tmppath / 'merged0001.exr', output) await self.move(tmppath / 'merged0001.exr', output)
# await self.move(tmppath / 'preview.jpg', output.with_suffix('.jpg'))
# See if this line logs the saving of a file. # See if this line logs the saving of a file.
self.worker.output_produced(output) self.worker.output_produced(output)
def _base_blender_cli(self, settings):
blendpath = Path(__file__).parent / 'resources/merge-exr.blend'
cmd = settings['blender_cmd'] + [
'--factory-startup',
'--enable-autoexec',
'-noaudio',
'--background',
blendpath.as_posix(),
'--python-exit-code', '47',
]
return cmd
async def move(self, src: Path, dst: Path): async def move(self, src: Path, dst: Path):
"""Moves a file to another location.""" """Moves a file to another location."""
...@@ -1006,6 +1063,44 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): ...@@ -1006,6 +1063,44 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
shutil.move(str(src), str(dst)) shutil.move(str(src), str(dst))
@command_executor('merge_progressive_render_sequence')
class MergeProgressiveRenderSequenceCommand(MergeProgressiveRendersCommand):
script_template = MERGE_EXR_SEQUENCE_PYTHON
def validate(self, settings: Settings):
err = super().validate(settings)
if err:
return err
if '##' not in settings['output']:
return 'Output filename should contain at least two "##" marks'
_, err = self._setting(settings, 'frame_start', True, int)
if err:
return err
_, err = self._setting(settings, 'frame_end', True, int)
if err:
return err
async def execute(self, settings: Settings):
cmd = self._base_blender_cli(settings)
# set up node properties and render settings.
output = Path(settings['output'])
await self._mkdir_if_not_exists(output.parent)
cmd += [
'--python-expr', self.script_template % settings
]
await self.worker.register_task_update(activity='Starting Blender to merge EXR sequence')
await self.subprocess(cmd)
as_glob = _hashes_to_glob(output)
for fpath in as_glob.parent.glob(as_glob.name):
self.worker.output_produced(fpath)
# TODO(Sybren): maybe subclass AbstractBlenderCommand instead? # TODO(Sybren): maybe subclass AbstractBlenderCommand instead?
@command_executor('blender_render_audio') @command_executor('blender_render_audio')
class BlenderRenderAudioCommand(BlenderRenderCommand): class BlenderRenderAudioCommand(BlenderRenderCommand):
......
No preview for this file type
...@@ -43,7 +43,47 @@ class MergeProgressiveRendersCommandTest(AbstractCommandTest): ...@@ -43,7 +43,47 @@ class MergeProgressiveRendersCommandTest(AbstractCommandTest):
self.assertTrue(output.exists()) self.assertTrue(output.exists())
self.assertTrue(output.is_file()) self.assertTrue(output.is_file())
# Sybren disabled preview generation since we don't use those any more
# in the studio. class MergeProgressiveRenderSequenceCommandTest(AbstractCommandTest):
# self.assertTrue(output.with_suffix('.jpg').exists()) def setUp(self):
# self.assertTrue(output.with_suffix('.jpg').is_file()) super().setUp()
from flamenco_worker.commands import MergeProgressiveRenderSequenceCommand
import tempfile
self.tmpdir = tempfile.TemporaryDirectory()
self.mypath = Path(__file__).parent
self.cmd = MergeProgressiveRenderSequenceCommand(
worker=self.fworker,
task_id='12345',
command_idx=0,
)
def tearDown(self):
super().tearDown()
self.tmpdir.cleanup()
def test_happy_flow(self):
output = Path(self.tmpdir.name) / 'merged-samples-######.exr'
settings = {
'blender_cmd': self.find_blender_cmd(),
'input1': str(self.mypath / 'Corn field-1k.exr'),
'input2': str(self.mypath / 'Deventer-1k.exr'),
'weight1': 20,
'weight2': 100,
'output': str(output),
'frame_start': 3,
'frame_end': 5,
}
task = self.cmd.run(settings)
ok = self.loop.run_until_complete(task)
self.assertTrue(ok)
# Assuming that if the files exist, the merge was ok.
for framenr in range(3, 6):
framefile = output.with_name(f'merged-samples-{framenr:06}.exr')
self.assertTrue(framefile.exists(), f'cannot find {framefile}')
self.assertTrue(framefile.is_file(), f'{framefile} is not a file')
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment