diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f8930737b69bbdf0b84e4a190dc575b05734c6f..963293c49377ab76eca24ce5d6fdd95b3d1fefbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ changed functionality, fixed bugs). Server are no longer supported. - 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. +- 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) diff --git a/flamenco_worker/commands.py b/flamenco_worker/commands.py index e859e383668818b05eb5d68c21cf7fe14f3ab147..4f14095a4ac7418a89b41682fd0d09b3ad13c1e4 100644 --- a/flamenco_worker/commands.py +++ b/flamenco_worker/commands.py @@ -45,14 +45,52 @@ nodes["image2"].image = image2 nodes["weight1"].outputs[0].default_value = %(weight1)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.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) """ +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__) @@ -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}' +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') class MoveOutOfWayCommand(AbstractCommand): def validate(self, settings: Settings): @@ -918,6 +965,8 @@ class BlenderRenderProgressiveCommand(BlenderRenderCommand): @command_executor('merge_progressive_renders') class MergeProgressiveRendersCommand(AbstractSubprocessCommand): + script_template = MERGE_EXR_PYTHON + def validate(self, settings: Settings): blender_cmd, err = self._setting(settings, 'blender_cmd', True) if err: @@ -928,20 +977,23 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): settings['blender_cmd'] = cmd input1, err = self._setting(settings, 'input1', True, str) - if err: return err + if err: + return err if '"' in input1: return 'Double quotes are not allowed in filenames: %r' % input1 if not Path(input1).exists(): return 'Input 1 %r does not exist' % input1 input2, err = self._setting(settings, 'input2', True, str) - if err: return err + if err: + return err if '"' in input2: return 'Double quotes are not allowed in filenames: %r' % input2 if not Path(input2).exists(): return 'Input 2 %r does not exist' % input2 output, err = self._setting(settings, 'output', True, str) - if err: return err + if err: + return err if '"' in output: return 'Double quotes are not allowed in filenames: %r' % output @@ -950,24 +1002,17 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): settings['output'] = Path(output).as_posix() _, err = self._setting(settings, 'weight1', True, int) - if err: return err + if err: + return err _, err = self._setting(settings, 'weight2', True, int) - if err: return err + if err: + return err return super().validate(settings) async def execute(self, settings: Settings): - blendpath = Path(__file__).parent / 'resources/merge-exr.blend' - - cmd = settings['blender_cmd'][:] - cmd += [ - '--factory-startup', - '--enable-autoexec', - '-noaudio', - '--background', - blendpath.as_posix(), - ] + cmd = self._base_blender_cli(settings) # set up node properties and render settings. output = Path(settings['output']) @@ -979,7 +1024,7 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): settings['tmpdir'] = tmppath.as_posix() 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') @@ -987,11 +1032,23 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): # move output files into the correct spot. 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. 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): """Moves a file to another location.""" @@ -1006,6 +1063,44 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): 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? @command_executor('blender_render_audio') class BlenderRenderAudioCommand(BlenderRenderCommand): diff --git a/flamenco_worker/resources/merge-exr.blend b/flamenco_worker/resources/merge-exr.blend index 2dda49f47950de3bc5f9a6959b62dab5c3b27b59..c2904b068f393d9d8a54bdfcd4fe8d8dbfcdb5e5 100644 Binary files a/flamenco_worker/resources/merge-exr.blend and b/flamenco_worker/resources/merge-exr.blend differ diff --git a/tests/test_commands_merge_exr.py b/tests/test_commands_merge_exr.py index d34339852b974639795735ca5396ae9b42895adc..64455d427ddd85f051205b8d88aef8ce07606a5a 100644 --- a/tests/test_commands_merge_exr.py +++ b/tests/test_commands_merge_exr.py @@ -43,7 +43,47 @@ class MergeProgressiveRendersCommandTest(AbstractCommandTest): self.assertTrue(output.exists()) self.assertTrue(output.is_file()) - # Sybren disabled preview generation since we don't use those any more - # in the studio. - # self.assertTrue(output.with_suffix('.jpg').exists()) - # self.assertTrue(output.with_suffix('.jpg').is_file()) + +class MergeProgressiveRenderSequenceCommandTest(AbstractCommandTest): + def setUp(self): + 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')