From 9271101ed72761183f8ecc6bfc3e587ffd786c26 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 09:56:54 +0100 Subject: [PATCH 01/40] removed modifiable save modes from extractor --- .../tvpaint/publish/extract_sequence.py | 53 ++----------------- 1 file changed, 4 insertions(+), 49 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index aa625a497a1..8fbf195fde1 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -45,13 +45,6 @@ class ExtractSequence(pyblish.api.Extractor): "tga" } - default_save_mode = "\"PNG\"" - save_mode_for_family = { - "review": "\"PNG\"", - "renderPass": "\"PNG\"", - "renderLayer": "\"PNG\"", - } - def process(self, instance): self.log.info( "* Processing instance \"{}\"".format(instance.data["label"]) @@ -80,34 +73,15 @@ def process(self, instance): len(layer_names), joined_layer_names ) ) - # This is plugin attribe cleanup method - self._prepare_save_modes() family_lowered = instance.data["family"].lower() - save_mode = self.save_mode_for_family.get( - family_lowered, self.default_save_mode - ) - save_mode_type = self._get_save_mode_type(save_mode) - - if not bool(save_mode_type in self.sequential_save_mode): - raise AssertionError(( - "Plugin can export only sequential frame output" - " but save mode for family \"{}\" is not for sequence > {} <" - ).format(instance.data["family"], save_mode)) - frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] - filename_template = self._get_filename_template( - save_mode_type, save_mode, frame_end - ) + filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") - self.log.debug( - "Using save mode > {} < and file template \"{}\"".format( - save_mode, filename_template - ) - ) + self.log.debug("Using file template \"{}\"".format(filename_template)) # Save to staging dir output_dir = instance.data.get("stagingDir") @@ -186,19 +160,6 @@ def process(self, instance): } instance.data["representations"].append(thumbnail_repre) - def _prepare_save_modes(self): - """Lower family names in keys and skip empty values.""" - new_specifications = {} - for key, value in self.save_mode_for_family.items(): - if value: - new_specifications[key.lower()] = value - else: - self.log.warning(( - "Save mode for family \"{}\" has empty value." - " The family will use default save mode: > {} <." - ).format(key, self.default_save_mode)) - self.save_mode_for_family = new_specifications - def _get_save_mode_type(self, save_mode): """Extract type of save mode. @@ -212,7 +173,7 @@ def _get_save_mode_type(self, save_mode): self.log.debug("Save mode type is \"{}\"".format(save_mode_type)) return save_mode_type - def _get_filename_template(self, save_mode_type, save_mode, frame_end): + def _get_filename_template(self, frame_end): """Get filetemplate for rendered files. This is simple template contains `{frame}{ext}` for sequential outputs @@ -220,18 +181,12 @@ def _get_filename_template(self, save_mode_type, save_mode, frame_end): temporary folder so filename should not matter as integrator change them. """ - ext = self.save_mode_to_ext.get(save_mode_type) - if ext is None: - raise AssertionError(( - "Couldn't find file extension for TVPaint's save mode: > {} <" - ).format(save_mode)) - frame_padding = 4 frame_end_str_len = len(str(frame_end)) if frame_end_str_len > frame_padding: frame_padding = frame_end_str_len - return "{{frame:0>{}}}".format(frame_padding) + ext + return "{{:0>{}}}".format(frame_padding) + ".png" def render( self, save_mode, filename_template, output_dir, layers, From f85edd36ca0ef7e1337e742ad0d2106d09aad014 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 09:57:50 +0100 Subject: [PATCH 02/40] imlpemented method for copying same temp image files --- pype/plugins/tvpaint/publish/extract_sequence.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 8fbf195fde1..9755bb28502 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -317,3 +317,10 @@ def fill_missing_frames( ) filepaths_by_frame[frame] = space_filepath shutil.copy(previous_frame_filepath, space_filepath) + + def _copy_image(self, src_path, dst_path): + # Create hardlink of image instead of copying if possible + if hasattr(os, "link"): + os.link(src_path, dst_path) + else: + shutil.copy(src_path, dst_path) From bed900fc49640b151e03a208fb7e41fc54f413cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 09:59:35 +0100 Subject: [PATCH 03/40] implemented method that will fill frames by pre behavior of a layer --- .../tvpaint/publish/extract_sequence.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 9755bb28502..87d5dfc9cc5 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -318,6 +318,64 @@ def fill_missing_frames( filepaths_by_frame[frame] = space_filepath shutil.copy(previous_frame_filepath, space_filepath) + def _fill_frame_by_pre_behavior( + self, + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_in_index >= frame_start_index: + return + + if pre_behavior == "none": + return + + if pre_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_start_index] + for frame_idx in range(mark_in_index, frame_start_index): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = ( + (frame_end_index - frame_idx) % frame_count + ) + eq_frame_idx = frame_end_index - eq_frame_idx_offset + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = (seq_len - eq_frame_idx_offset) + eq_frame_idx = frame_start_index + eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 72a6e1d9c67040e8cb7e6971906f3a5b0373c602 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:00:17 +0100 Subject: [PATCH 04/40] implemented method that will fill frames by post behavior of a layer --- .../tvpaint/publish/extract_sequence.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 87d5dfc9cc5..0f1ce8691ac 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -376,6 +376,61 @@ def _fill_frame_by_pre_behavior( self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath + def _fill_frame_by_post_behavior( + self, + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_out_index <= frame_end_index: + return + + if post_behavior == "none": + return + + if post_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_end_index] + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx = frame_idx % frame_count + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = seq_len - eq_frame_idx_offset + eq_frame_idx = frame_end_index - eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 5ece25b123bc0a4216ac0e2ba17df15be3f592b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:01:26 +0100 Subject: [PATCH 05/40] implemented logic of layers compositing using Pillow --- .../tvpaint/publish/extract_sequence.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 0f1ce8691ac..449da3c1e0d 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -4,6 +4,7 @@ import pyblish.api from avalon.tvpaint import lib +from PIL import Image class ExtractSequence(pyblish.api.Extractor): @@ -431,6 +432,51 @@ def _fill_frame_by_post_behavior( self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath + def _composite_files( + self, files_by_position, output_dir, frame_start, frame_end, + filename_template, thumbnail_filename + ): + # Prepare paths to images by frames into list where are stored + # in order of compositing. + images_by_frame = {} + for frame_idx in range(frame_start, frame_end + 1): + images_by_frame[frame_idx] = [] + for position in sorted(files_by_position.keys(), reverse=True): + position_data = files_by_position[position] + if frame_idx in position_data: + images_by_frame[frame_idx].append(position_data[frame_idx]) + + output_filepaths = [] + thumbnail_src_filepath = None + for frame_idx in sorted(images_by_frame.keys()): + image_filepaths = images_by_frame[frame_idx] + frame = frame_idx + 1 + output_filename = filename_template.format(frame) + output_filepath = os.path.join(output_dir, output_filename) + img_obj = None + for image_filepath in image_filepaths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + continue + + img_obj.alpha_composite(_img_obj) + img_obj.save(output_filepath) + output_filepaths.append(output_filepath) + + if thumbnail_filename and thumbnail_src_filepath is None: + thumbnail_src_filepath = output_filepath + + thumbnail_filepath = None + if thumbnail_src_filepath: + source_img = Image.open(thumbnail_src_filepath) + thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + + return output_filepaths, thumbnail_filepath + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 796e7224e1238798fce181a6f30a8f1bf03178c9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:02:47 +0100 Subject: [PATCH 06/40] frame start/end are defined by mark in/out of published clip --- .../tvpaint/publish/collect_workfile_data.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index bd2e5745189..f25e2745814 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -113,7 +113,7 @@ def process(self, context): self.log.info("Collecting scene data from workfile") workfile_info_parts = lib.execute_george("tv_projectinfo").split(" ") - frame_start = int(workfile_info_parts.pop(-1)) + _frame_start = int(workfile_info_parts.pop(-1)) field_order = workfile_info_parts.pop(-1) frame_rate = float(workfile_info_parts.pop(-1)) pixel_apsect = float(workfile_info_parts.pop(-1)) @@ -121,21 +121,14 @@ def process(self, context): width = int(workfile_info_parts.pop(-1)) workfile_path = " ".join(workfile_info_parts).replace("\"", "") - # TODO This is not porper way of getting last frame - # - but don't know better - last_frame = frame_start - for layer in layers_data: - frame_end = layer["frame_end"] - if frame_end > last_frame: - last_frame = frame_end - + frame_start, frame_end = self.collect_clip_frames() scene_data = { "currentFile": workfile_path, "sceneWidth": width, "sceneHeight": height, "pixelAspect": pixel_apsect, "frameStart": frame_start, - "frameEnd": last_frame, + "frameEnd": frame_end, "fps": frame_rate, "fieldOrder": field_order } @@ -143,3 +136,19 @@ def process(self, context): "Scene data: {}".format(json.dumps(scene_data, indent=4)) ) context.data.update(scene_data) + + def collect_clip_frames(self): + clip_info_str = lib.execute_george("tv_clipinfo") + self.log.debug("Clip info: {}".format(clip_info_str)) + clip_info_items = clip_info_str.split(" ") + # Color index + color_idx = clip_info_items.pop(-1) + clip_info_items.pop(-1) + + mark_out = int(clip_info_items.pop(-1)) + 1 + clip_info_items.pop(-1) + + mark_in = int(clip_info_items.pop(-1)) + 1 + clip_info_items.pop(-1) + + return mark_in, mark_out From 9b78deb2ebd412fbc6395d7e3b032ddcb9cd6b19 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:04:28 +0100 Subject: [PATCH 07/40] collect both layer's position and all layer ids --- pype/plugins/tvpaint/publish/extract_sequence.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 449da3c1e0d..576d294c425 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -214,10 +214,13 @@ def render( save_mode = "tv_SaveMode {}".format(save_mode) # Map layers by position - layers_by_position = { - layer["position"]: layer - for layer in layers - } + layers_by_position = {} + layer_ids = [] + for layer in layers: + position = layer["position"] + layers_by_position[position] = layer + + layer_ids.append(layer["layer_id"]) # Sort layer positions in reverse order sorted_positions = list(reversed(sorted(layers_by_position.keys()))) From df1435e80e97b191b9bf16e7ae0afbd31b59a8df Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:04:55 +0100 Subject: [PATCH 08/40] skip savemode filling --- pype/plugins/tvpaint/publish/extract_sequence.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 576d294c425..6efa22d1cdc 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -190,8 +190,8 @@ def _get_filename_template(self, frame_end): return "{{:0>{}}}".format(frame_padding) + ".png" def render( - self, save_mode, filename_template, output_dir, layers, - first_frame, last_frame, thumbnail_filename + self, filename_template, output_dir, layers, + frame_start, frame_end, thumbnail_filename ): """ Export images from TVPaint. @@ -210,9 +210,6 @@ def render( dict: Mapping frame to output filepath. """ - # Add save mode arguments to function - save_mode = "tv_SaveMode {}".format(save_mode) - # Map layers by position layers_by_position = {} layer_ids = [] From cf6d649cd1fe92a30b79f6a0ea386375398923fb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:08:00 +0100 Subject: [PATCH 09/40] removed previous logic of rendering --- .../tvpaint/publish/extract_sequence.py | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 6efa22d1cdc..0fe4018157a 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -102,17 +102,6 @@ def process(self, instance): save_mode, filename_template, output_dir, filtered_layers, frame_start, frame_end, thumbnail_filename ) - thumbnail_fullpath = output_files_by_frame.pop( - thumbnail_filename, None - ) - - # Fill gaps in sequence - self.fill_missing_frames( - output_files_by_frame, - frame_start, - frame_end, - filename_template - ) # Fill tags and new families tags = [] @@ -224,100 +213,9 @@ def render( if not sorted_positions: return - # Create temporary layer - new_layer_id = lib.execute_george("tv_layercreate _tmp_layer") - # Merge layers to temp layer - george_script_lines = [] - # Set duplicated layer as current - george_script_lines.append("tv_layerset {}".format(new_layer_id)) for position in sorted_positions: layer = layers_by_position[position] - george_script_lines.append( - "tv_layermerge {}".format(layer["layer_id"]) - ) - - lib.execute_george_through_file("\n".join(george_script_lines)) - - # Frames with keyframe - exposure_frames = lib.get_exposure_frames( - new_layer_id, first_frame, last_frame - ) - - # TODO what if there is not exposue frames? - # - this force to have first frame all the time - if first_frame not in exposure_frames: - exposure_frames.insert(0, first_frame) - - # Restart george script lines - george_script_lines = [] - george_script_lines.append(save_mode) - - all_output_files = {} - for frame in exposure_frames: - filename = filename_template.format(frame, frame=frame) - dst_path = "/".join([output_dir, filename]) - all_output_files[frame] = os.path.normpath(dst_path) - - # Go to frame - george_script_lines.append("tv_layerImage {}".format(frame)) - # Store image to output - george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) - - # Export thumbnail - if thumbnail_filename: - basename, ext = os.path.splitext(thumbnail_filename) - if not ext: - ext = ".jpg" - thumbnail_fullpath = "/".join([output_dir, basename + ext]) - all_output_files[thumbnail_filename] = thumbnail_fullpath - # Force save mode to png for thumbnail - george_script_lines.append("tv_SaveMode \"JPG\"") - # Go to frame - george_script_lines.append("tv_layerImage {}".format(first_frame)) - # Store image to output - george_script_lines.append( - "tv_saveimage \"{}\"".format(thumbnail_fullpath) - ) - - # Delete temporary layer - george_script_lines.append("tv_layerkill {}".format(new_layer_id)) - - lib.execute_george_through_file("\n".join(george_script_lines)) - - return all_output_files - - def fill_missing_frames( - self, filepaths_by_frame, first_frame, last_frame, filename_template - ): - """Fill not rendered frames with previous frame. - - Extractor is rendering only frames with keyframes (exposure frames) to - get output faster which means there may be gaps between frames. - This function fill the missing frames. - """ - output_dir = None - previous_frame_filepath = None - for frame in range(first_frame, last_frame + 1): - if frame in filepaths_by_frame: - previous_frame_filepath = filepaths_by_frame[frame] - continue - - elif previous_frame_filepath is None: - self.log.warning( - "No frames to fill. Seems like nothing was exported." - ) - break - - if output_dir is None: - output_dir = os.path.dirname(previous_frame_filepath) - - filename = filename_template.format(frame=frame) - space_filepath = os.path.normpath( - os.path.join(output_dir, filename) - ) - filepaths_by_frame[frame] = space_filepath - shutil.copy(previous_frame_filepath, space_filepath) def _fill_frame_by_pre_behavior( self, From cc9369ef8d8553552fabebdcd2b4061e1299db65 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:08:38 +0100 Subject: [PATCH 10/40] collect behavior of layer ids to process --- pype/plugins/tvpaint/publish/extract_sequence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 0fe4018157a..64bd023f5f9 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -213,6 +213,7 @@ def render( if not sorted_positions: return + behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) for position in sorted_positions: layer = layers_by_position[position] From 838ebbeb060f84fafd408f19178b104e3878c107 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:09:32 +0100 Subject: [PATCH 11/40] implemented method that will render and fill all frames of given layer --- .../tvpaint/publish/extract_sequence.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 64bd023f5f9..852ec011838 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -218,6 +218,80 @@ def render( for position in sorted_positions: layer = layers_by_position[position] + def render_layer( + self, + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ): + layer_id = layer["layer_id"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + exposure_frames = lib.get_exposure_frames( + layer_id, frame_start_index, frame_end_index + ) + if frame_start_index not in exposure_frames: + exposure_frames.append(frame_start_index) + + layer_files_by_frame = {} + george_script_lines = [ + "tv_SaveMode \"PNG\"" + ] + layer_position = layer["position"] + + for frame_idx in exposure_frames: + filename = tmp_filename_template.format(layer_position, frame_idx) + dst_path = "/".join([output_dir, filename]) + layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) + + # Go to frame + george_script_lines.append("tv_layerImage {}".format(frame_idx)) + # Store image to output + george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + + # Let TVPaint render layer's image + lib.execute_george_through_file("\n".join(george_script_lines)) + + # Fill frames between `frame_start_index` and `frame_end_index` + prev_filepath = None + for frame_idx in range(frame_start_index, frame_end_index + 1): + if frame_idx in layer_files_by_frame: + prev_filepath = layer_files_by_frame[frame_idx] + continue + + if prev_filepath is None: + raise ValueError("BUG: First frame of layer was not rendered!") + + filename = tmp_filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(prev_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + # Fill frames by pre/post behavior of layer + pre_behavior = behavior["pre"] + post_behavior = behavior["post"] + # Pre behavior + self._fill_frame_by_pre_behavior( + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + self._fill_frame_by_post_behavior( + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + return layer_files_by_frame + def _fill_frame_by_pre_behavior( self, layer, From df4e28153549d00d553053f8ced31059fd122881 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:10:13 +0100 Subject: [PATCH 12/40] layers are rendered one by one and stored by their position (order) --- .../plugins/tvpaint/publish/extract_sequence.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 852ec011838..e1667997fb5 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -215,8 +215,25 @@ def render( behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) + mark_in_index = frame_start - 1 + mark_out_index = frame_end - 1 + + tmp_filename_template = "pos_{}." + filename_template + + files_by_position = {} for position in sorted_positions: layer = layers_by_position[position] + behavior = behavior_by_layer_id[layer["layer_id"]] + files_by_frames = self.render_layer( + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ) + files_by_position[position] = files_by_frames + def render_layer( self, From d14e584a8d9d5bd516c8bd1399c42e167757567f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:10:49 +0100 Subject: [PATCH 13/40] rendered frames are composite to final output --- pype/plugins/tvpaint/publish/extract_sequence.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index e1667997fb5..919dd02f7c5 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -234,6 +234,15 @@ def render( ) files_by_position[position] = files_by_frames + output = self._composite_files( + files_by_position, + output_dir, + mark_in_index, + mark_out_index, + filename_template, + thumbnail_filename + ) + return output def render_layer( self, From d8033fc6cabcba26e25e3ed4a1470d42f007927f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:11:14 +0100 Subject: [PATCH 14/40] added cleanup method that will remove temp image files of individial layers --- pype/plugins/tvpaint/publish/extract_sequence.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 919dd02f7c5..f1929707b43 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -242,6 +242,7 @@ def render( filename_template, thumbnail_filename ) + self._cleanup_tmp_files(files_by_position) return output def render_layer( @@ -476,6 +477,11 @@ def _composite_files( return output_filepaths, thumbnail_filepath + def _cleanup_tmp_files(self, files_by_position): + for data in files_by_position.values(): + for filepath in data.values(): + os.remove(filepath) + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From f57ecfa2e705ba27162dfcab954056805a2ba487 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:12:03 +0100 Subject: [PATCH 15/40] pass different arguments and expect different output of render method --- pype/plugins/tvpaint/publish/extract_sequence.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index f1929707b43..f8aeace6177 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -95,12 +95,12 @@ def process(self, instance): "Files will be rendered to folder: {}".format(output_dir) ) - thumbnail_filename = "thumbnail" + thumbnail_filename = "thumbnail.jpg" # Render output - output_files_by_frame = self.render( - save_mode, filename_template, output_dir, - filtered_layers, frame_start, frame_end, thumbnail_filename + output_filepaths, thumbnail_fullpath = self.render( + filename_template, output_dir, filtered_layers, + frame_start, frame_end, thumbnail_filename ) # Fill tags and new families @@ -110,7 +110,7 @@ def process(self, instance): repre_files = [ os.path.basename(filepath) - for filepath in output_files_by_frame.values() + for filepath in output_filepaths ] # Sequence of one frame if len(repre_files) == 1: From b8c57e0057a3eabb78618afc492e06adb9f7e111 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:12:17 +0100 Subject: [PATCH 16/40] keep frame start/end as they are --- pype/plugins/tvpaint/publish/extract_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index f8aeace6177..821b212f839 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -121,8 +121,8 @@ def process(self, instance): "ext": ext, "files": repre_files, "stagingDir": output_dir, - "frameStart": frame_start + 1, - "frameEnd": frame_end + 1, + "frameStart": frame_start, + "frameEnd": frame_end, "tags": tags } self.log.debug("Creating new representation: {}".format(new_repre)) From c452dc953f2e0cc37defba957a912334f9e8fb36 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:26:43 +0100 Subject: [PATCH 17/40] added some extra logs --- pype/plugins/tvpaint/publish/extract_sequence.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 821b212f839..729eb5a9488 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -61,7 +61,7 @@ def process(self, instance): layer_names = [str(layer["name"]) for layer in filtered_layers] if not layer_names: self.log.info( - f"None of the layers from the instance" + "None of the layers from the instance" " are visible. Extraction skipped." ) return @@ -198,6 +198,7 @@ def render( Retruns: dict: Mapping frame to output filepath. """ + self.log.debug("Preparing data for rendering.") # Map layers by position layers_by_position = {} @@ -213,6 +214,7 @@ def render( if not sorted_positions: return + self.log.debug("Collecting pre/post behavior of individual layers.") behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) mark_in_index = frame_start - 1 @@ -279,10 +281,17 @@ def render_layer( # Store image to output george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + self.log.debug("Rendering exposure frames {} of layer {}".format( + str(exposure_frames), layer_id + )) # Let TVPaint render layer's image lib.execute_george_through_file("\n".join(george_script_lines)) # Fill frames between `frame_start_index` and `frame_end_index` + self.log.debug(( + "Filling frames between first and last frame of layer ({} - {})." + ).format(frame_start_index + 1, frame_end_index + 1)) + prev_filepath = None for frame_idx in range(frame_start_index, frame_end_index + 1): if frame_idx in layer_files_by_frame: @@ -300,6 +309,11 @@ def render_layer( # Fill frames by pre/post behavior of layer pre_behavior = behavior["pre"] post_behavior = behavior["post"] + self.log.debug(( + "Completing image sequence of layer by pre/post behavior." + " PRE: {} | POST: {}" + ).format(pre_behavior, post_behavior)) + # Pre behavior self._fill_frame_by_pre_behavior( layer, From 043a9d9e038372e44a34b6ad7c8c631990834798 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 11:54:26 +0100 Subject: [PATCH 18/40] fixed case when all layers miss image for frame --- .../tvpaint/publish/extract_sequence.py | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 729eb5a9488..847292814d2 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -7,6 +7,22 @@ from PIL import Image +def composite_images( + input_image_paths, output_filepath, scene_width, scene_height +): + img_obj = None + for image_filepath in input_image_paths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + else: + img_obj.alpha_composite(_img_obj) + + if img_obj is None: + img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) + img_obj.save(output_filepath) + + class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] @@ -78,6 +94,8 @@ def process(self, instance): family_lowered = instance.data["family"].lower() frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] + scene_width = instance.context.data["sceneWidth"] + scene_height = instance.context.data["sceneHeight"] filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -100,7 +118,8 @@ def process(self, instance): # Render output output_filepaths, thumbnail_fullpath = self.render( filename_template, output_dir, filtered_layers, - frame_start, frame_end, thumbnail_filename + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height ) # Fill tags and new families @@ -180,7 +199,8 @@ def _get_filename_template(self, frame_end): def render( self, filename_template, output_dir, layers, - frame_start, frame_end, thumbnail_filename + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height ): """ Export images from TVPaint. @@ -242,7 +262,9 @@ def render( mark_in_index, mark_out_index, filename_template, - thumbnail_filename + thumbnail_filename, + scene_width, + scene_height ) self._cleanup_tmp_files(files_by_position) return output @@ -448,7 +470,7 @@ def _fill_frame_by_post_behavior( def _composite_files( self, files_by_position, output_dir, frame_start, frame_end, - filename_template, thumbnail_filename + filename_template, thumbnail_filename, scene_width, scene_height ): # Prepare paths to images by frames into list where are stored # in order of compositing. @@ -465,22 +487,18 @@ def _composite_files( for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] frame = frame_idx + 1 + output_filename = filename_template.format(frame) output_filepath = os.path.join(output_dir, output_filename) - img_obj = None - for image_filepath in image_filepaths: - _img_obj = Image.open(image_filepath) - if img_obj is None: - img_obj = _img_obj - continue - - img_obj.alpha_composite(_img_obj) - img_obj.save(output_filepath) output_filepaths.append(output_filepath) if thumbnail_filename and thumbnail_src_filepath is None: thumbnail_src_filepath = output_filepath + composite_images( + image_filepaths, output_filepath, scene_width, scene_height + ) + thumbnail_filepath = None if thumbnail_src_filepath: source_img = Image.open(thumbnail_src_filepath) From 84f38013c972f573e924623bf5e7caf6bd2b5eaa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 12:05:26 +0100 Subject: [PATCH 19/40] moved composite_images to pype's tvpaint.lib --- pype/hosts/tvpaint/lib.py | 17 +++++++++++++++++ .../plugins/tvpaint/publish/extract_sequence.py | 17 +---------------- 2 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 pype/hosts/tvpaint/lib.py diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py new file mode 100644 index 00000000000..8172392c7f2 --- /dev/null +++ b/pype/hosts/tvpaint/lib.py @@ -0,0 +1,17 @@ +from PIL import Image + + +def composite_images( + input_image_paths, output_filepath, scene_width, scene_height +): + img_obj = None + for image_filepath in input_image_paths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + else: + img_obj.alpha_composite(_img_obj) + + if img_obj is None: + img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) + img_obj.save(output_filepath) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 847292814d2..d33ec3c68cb 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -4,25 +4,10 @@ import pyblish.api from avalon.tvpaint import lib +from pype.hosts.tvpaint.lib import composite_images from PIL import Image -def composite_images( - input_image_paths, output_filepath, scene_width, scene_height -): - img_obj = None - for image_filepath in input_image_paths: - _img_obj = Image.open(image_filepath) - if img_obj is None: - img_obj = _img_obj - else: - img_obj.alpha_composite(_img_obj) - - if img_obj is None: - img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) - img_obj.save(output_filepath) - - class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] From 3193ade4f93893a2e847c163a21ed07d565c6268 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 12:10:38 +0100 Subject: [PATCH 20/40] using multiprocessing to speed up compositing part --- .../tvpaint/publish/extract_sequence.py | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index d33ec3c68cb..e43fb06f7a5 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -1,6 +1,8 @@ import os import shutil +import time import tempfile +import multiprocessing import pyblish.api from avalon.tvpaint import lib @@ -467,6 +469,11 @@ def _composite_files( if frame_idx in position_data: images_by_frame[frame_idx].append(position_data[frame_idx]) + process_count = os.cpu_count() + if process_count > 1: + process_count -= 1 + + processes = {} output_filepaths = [] thumbnail_src_filepath = None for frame_idx in sorted(images_by_frame.keys()): @@ -480,10 +487,35 @@ def _composite_files( if thumbnail_filename and thumbnail_src_filepath is None: thumbnail_src_filepath = output_filepath - composite_images( - image_filepaths, output_filepath, scene_width, scene_height + processes[frame_idx] = multiprocessing.Process( + target=composite_images, + args=( + image_filepaths, output_filepath, scene_width, scene_height + ) ) + # Wait until all processes are done + running_processes = {} + while True: + for idx in tuple(running_processes.keys()): + process = running_processes[idx] + if not process.is_alive(): + running_processes.pop(idx).join() + + if processes and len(running_processes) != process_count: + indexes = list(processes.keys()) + for _ in range(process_count - len(running_processes)): + if not indexes: + break + idx = indexes.pop(0) + running_processes[idx] = processes.pop(idx) + running_processes[idx].start() + + if not running_processes and not processes: + break + + time.sleep(0.01) + thumbnail_filepath = None if thumbnail_src_filepath: source_img = Image.open(thumbnail_src_filepath) From 554e0f57b03bbfc0847bbc2a6efd4b306d3e29ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:35:43 +0100 Subject: [PATCH 21/40] simplified extractor with tv_savesequence command --- .../tvpaint/publish/extract_sequence.py | 429 +++--------------- 1 file changed, 65 insertions(+), 364 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index e43fb06f7a5..17d8dc60f47 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -1,12 +1,8 @@ import os -import shutil -import time import tempfile -import multiprocessing import pyblish.api from avalon.tvpaint import lib -from pype.hosts.tvpaint.lib import composite_images from PIL import Image @@ -61,6 +57,10 @@ def process(self, instance): for layer in layers if layer["visible"] ] + filtered_layer_ids = [ + layer["layer_id"] + for layer in filtered_layers + ] layer_names = [str(layer["name"]) for layer in filtered_layers] if not layer_names: self.log.info( @@ -81,8 +81,6 @@ def process(self, instance): family_lowered = instance.data["family"].lower() frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] - scene_width = instance.context.data["sceneWidth"] - scene_height = instance.context.data["sceneHeight"] filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -100,24 +98,53 @@ def process(self, instance): "Files will be rendered to folder: {}".format(output_dir) ) - thumbnail_filename = "thumbnail.jpg" + first_frame_filename = filename_template.format(frame_start) + first_frame_filepath = os.path.join(output_dir, first_frame_filename) + + # Store layers visibility + layer_visibility_by_id = {} + for layer in instance.context.data["layersData"]: + layer_id = layer["layer_id"] + layer_visibility_by_id[layer_id] = layer["visible"] + + george_script_lines = [] + for layer_id in layer_visibility_by_id.keys(): + visible = layer_id in filtered_layer_ids + value = "on" if visible else "off" + george_script_lines.append( + "tv_layerdisplay {} \"{}\"".format(layer_id, value) + ) + lib.execute_george_through_file("\n".join(george_script_lines)) # Render output - output_filepaths, thumbnail_fullpath = self.render( - filename_template, output_dir, filtered_layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height + repre_files = self.render( + filename_template, + output_dir, + frame_start, + frame_end ) + # Restore visibility + george_script_lines = [] + for layer_id, visible in layer_visibility_by_id.items(): + value = "on" if visible else "off" + george_script_lines.append( + "tv_layerdisplay {} \"{}\"".format(layer_id, value) + ) + lib.execute_george_through_file("\n".join(george_script_lines)) + + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") + if os.path.exists(first_frame_filepath): + source_img = Image.open(first_frame_filepath) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + # Fill tags and new families tags = [] if family_lowered in ("review", "renderlayer"): tags.append("review") - repre_files = [ - os.path.basename(filepath) - for filepath in output_filepaths - ] # Sequence of one frame if len(repre_files) == 1: repre_files = repre_files[0] @@ -139,36 +166,23 @@ def process(self, instance): # Change family to render instance.data["family"] = "render" - if not thumbnail_fullpath: + if not os.path.exists(thumbnail_filepath): return thumbnail_ext = os.path.splitext( - thumbnail_fullpath + thumbnail_filepath )[1].replace(".", "") # Create thumbnail representation thumbnail_repre = { "name": "thumbnail", "ext": thumbnail_ext, "outputName": "thumb", - "files": os.path.basename(thumbnail_fullpath), + "files": os.path.basename(thumbnail_filepath), "stagingDir": output_dir, "tags": ["thumbnail"] } instance.data["representations"].append(thumbnail_repre) - def _get_save_mode_type(self, save_mode): - """Extract type of save mode. - - Helps to define output files extension. - """ - save_mode_type = ( - save_mode.lower() - .split(" ")[0] - .replace("\"", "") - ) - self.log.debug("Save mode type is \"{}\"".format(save_mode_type)) - return save_mode_type - def _get_filename_template(self, frame_end): """Get filetemplate for rendered files. @@ -184,356 +198,43 @@ def _get_filename_template(self, frame_end): return "{{:0>{}}}".format(frame_padding) + ".png" - def render( - self, filename_template, output_dir, layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height - ): + def render(self, filename_template, output_dir, frame_start, frame_end): """ Export images from TVPaint. Args: - save_mode (str): Argument for `tv_savemode` george script function. - More about save mode in documentation. filename_template (str): Filename template of an output. Template should already contain extension. Template may contain only keyword argument `{frame}` or index argument (for same value). Extension in template must match `save_mode`. - layers (list): List of layers to be exported. - first_frame (int): Starting frame from which export will begin. - last_frame (int): On which frame export will end. + output_dir (list): List of layers to be exported. + frame_start (int): Starting frame from which export will begin. + frame_end (int): On which frame export will end. Retruns: dict: Mapping frame to output filepath. """ self.log.debug("Preparing data for rendering.") - - # Map layers by position - layers_by_position = {} - layer_ids = [] - for layer in layers: - position = layer["position"] - layers_by_position[position] = layer - - layer_ids.append(layer["layer_id"]) - - # Sort layer positions in reverse order - sorted_positions = list(reversed(sorted(layers_by_position.keys()))) - if not sorted_positions: - return - - self.log.debug("Collecting pre/post behavior of individual layers.") - behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) - - mark_in_index = frame_start - 1 - mark_out_index = frame_end - 1 - - tmp_filename_template = "pos_{}." + filename_template - - files_by_position = {} - for position in sorted_positions: - layer = layers_by_position[position] - behavior = behavior_by_layer_id[layer["layer_id"]] - files_by_frames = self.render_layer( - layer, - tmp_filename_template, - output_dir, - behavior, - mark_in_index, - mark_out_index - ) - files_by_position[position] = files_by_frames - - output = self._composite_files( - files_by_position, + first_frame_filepath = os.path.join( output_dir, - mark_in_index, - mark_out_index, - filename_template, - thumbnail_filename, - scene_width, - scene_height - ) - self._cleanup_tmp_files(files_by_position) - return output - - def render_layer( - self, - layer, - tmp_filename_template, - output_dir, - behavior, - mark_in_index, - mark_out_index - ): - layer_id = layer["layer_id"] - frame_start_index = layer["frame_start"] - frame_end_index = layer["frame_end"] - exposure_frames = lib.get_exposure_frames( - layer_id, frame_start_index, frame_end_index + filename_template.format(frame_start, frame=frame_start) ) - if frame_start_index not in exposure_frames: - exposure_frames.append(frame_start_index) + mark_in = frame_start - 1 + mark_out = frame_end - 1 - layer_files_by_frame = {} george_script_lines = [ - "tv_SaveMode \"PNG\"" + "tv_SaveMode \"PNG\"", + "export_path = \"{}\"".format( + first_frame_filepath.replace("\\", "/") + ), + "tv_savesequence '\"'export_path'\"' {} {}".format( + mark_in, mark_out + ) ] - layer_position = layer["position"] - - for frame_idx in exposure_frames: - filename = tmp_filename_template.format(layer_position, frame_idx) - dst_path = "/".join([output_dir, filename]) - layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) - - # Go to frame - george_script_lines.append("tv_layerImage {}".format(frame_idx)) - # Store image to output - george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) - - self.log.debug("Rendering exposure frames {} of layer {}".format( - str(exposure_frames), layer_id - )) - # Let TVPaint render layer's image lib.execute_george_through_file("\n".join(george_script_lines)) - # Fill frames between `frame_start_index` and `frame_end_index` - self.log.debug(( - "Filling frames between first and last frame of layer ({} - {})." - ).format(frame_start_index + 1, frame_end_index + 1)) - - prev_filepath = None - for frame_idx in range(frame_start_index, frame_end_index + 1): - if frame_idx in layer_files_by_frame: - prev_filepath = layer_files_by_frame[frame_idx] - continue - - if prev_filepath is None: - raise ValueError("BUG: First frame of layer was not rendered!") - - filename = tmp_filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(prev_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - # Fill frames by pre/post behavior of layer - pre_behavior = behavior["pre"] - post_behavior = behavior["post"] - self.log.debug(( - "Completing image sequence of layer by pre/post behavior." - " PRE: {} | POST: {}" - ).format(pre_behavior, post_behavior)) - - # Pre behavior - self._fill_frame_by_pre_behavior( - layer, - pre_behavior, - mark_in_index, - layer_files_by_frame, - tmp_filename_template, - output_dir - ) - self._fill_frame_by_post_behavior( - layer, - post_behavior, - mark_out_index, - layer_files_by_frame, - tmp_filename_template, - output_dir - ) - return layer_files_by_frame - - def _fill_frame_by_pre_behavior( - self, - layer, - pre_behavior, - mark_in_index, - layer_files_by_frame, - filename_template, - output_dir - ): - layer_position = layer["position"] - frame_start_index = layer["frame_start"] - frame_end_index = layer["frame_end"] - frame_count = frame_end_index - frame_start_index + 1 - if mark_in_index >= frame_start_index: - return - - if pre_behavior == "none": - return - - if pre_behavior == "hold": - # Keep first frame for whole time - eq_frame_filepath = layer_files_by_frame[frame_start_index] - for frame_idx in range(mark_in_index, frame_start_index): - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif pre_behavior == "loop": - # Loop backwards from last frame of layer - for frame_idx in reversed(range(mark_in_index, frame_start_index)): - eq_frame_idx_offset = ( - (frame_end_index - frame_idx) % frame_count - ) - eq_frame_idx = frame_end_index - eq_frame_idx_offset - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif pre_behavior == "pingpong": - half_seq_len = frame_count - 1 - seq_len = half_seq_len * 2 - for frame_idx in reversed(range(mark_in_index, frame_start_index)): - eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len - if eq_frame_idx_offset > half_seq_len: - eq_frame_idx_offset = (seq_len - eq_frame_idx_offset) - eq_frame_idx = frame_start_index + eq_frame_idx_offset - - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - def _fill_frame_by_post_behavior( - self, - layer, - post_behavior, - mark_out_index, - layer_files_by_frame, - filename_template, - output_dir - ): - layer_position = layer["position"] - frame_start_index = layer["frame_start"] - frame_end_index = layer["frame_end"] - frame_count = frame_end_index - frame_start_index + 1 - if mark_out_index <= frame_end_index: - return - - if post_behavior == "none": - return - - if post_behavior == "hold": - # Keep first frame for whole time - eq_frame_filepath = layer_files_by_frame[frame_end_index] - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif post_behavior == "loop": - # Loop backwards from last frame of layer - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - eq_frame_idx = frame_idx % frame_count - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif post_behavior == "pingpong": - half_seq_len = frame_count - 1 - seq_len = half_seq_len * 2 - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len - if eq_frame_idx_offset > half_seq_len: - eq_frame_idx_offset = seq_len - eq_frame_idx_offset - eq_frame_idx = frame_end_index - eq_frame_idx_offset - - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - def _composite_files( - self, files_by_position, output_dir, frame_start, frame_end, - filename_template, thumbnail_filename, scene_width, scene_height - ): - # Prepare paths to images by frames into list where are stored - # in order of compositing. - images_by_frame = {} - for frame_idx in range(frame_start, frame_end + 1): - images_by_frame[frame_idx] = [] - for position in sorted(files_by_position.keys(), reverse=True): - position_data = files_by_position[position] - if frame_idx in position_data: - images_by_frame[frame_idx].append(position_data[frame_idx]) - - process_count = os.cpu_count() - if process_count > 1: - process_count -= 1 - - processes = {} - output_filepaths = [] - thumbnail_src_filepath = None - for frame_idx in sorted(images_by_frame.keys()): - image_filepaths = images_by_frame[frame_idx] - frame = frame_idx + 1 - - output_filename = filename_template.format(frame) - output_filepath = os.path.join(output_dir, output_filename) - output_filepaths.append(output_filepath) - - if thumbnail_filename and thumbnail_src_filepath is None: - thumbnail_src_filepath = output_filepath - - processes[frame_idx] = multiprocessing.Process( - target=composite_images, - args=( - image_filepaths, output_filepath, scene_width, scene_height - ) + output = [] + for frame in range(frame_start, frame_end + 1): + output.append( + filename_template.format(frame, frame=frame) ) - - # Wait until all processes are done - running_processes = {} - while True: - for idx in tuple(running_processes.keys()): - process = running_processes[idx] - if not process.is_alive(): - running_processes.pop(idx).join() - - if processes and len(running_processes) != process_count: - indexes = list(processes.keys()) - for _ in range(process_count - len(running_processes)): - if not indexes: - break - idx = indexes.pop(0) - running_processes[idx] = processes.pop(idx) - running_processes[idx].start() - - if not running_processes and not processes: - break - - time.sleep(0.01) - - thumbnail_filepath = None - if thumbnail_src_filepath: - source_img = Image.open(thumbnail_src_filepath) - thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) - - return output_filepaths, thumbnail_filepath - - def _cleanup_tmp_files(self, files_by_position): - for data in files_by_position.values(): - for filepath in data.values(): - os.remove(filepath) - - def _copy_image(self, src_path, dst_path): - # Create hardlink of image instead of copying if possible - if hasattr(os, "link"): - os.link(src_path, dst_path) - else: - shutil.copy(src_path, dst_path) + return output From bda4296a86e61bf12fdb70f1bcdd8e67d7e83192 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:42:21 +0100 Subject: [PATCH 22/40] removed unused lib --- pype/hosts/tvpaint/lib.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 pype/hosts/tvpaint/lib.py diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py deleted file mode 100644 index 8172392c7f2..00000000000 --- a/pype/hosts/tvpaint/lib.py +++ /dev/null @@ -1,17 +0,0 @@ -from PIL import Image - - -def composite_images( - input_image_paths, output_filepath, scene_width, scene_height -): - img_obj = None - for image_filepath in input_image_paths: - _img_obj = Image.open(image_filepath) - if img_obj is None: - img_obj = _img_obj - else: - img_obj.alpha_composite(_img_obj) - - if img_obj is None: - img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) - img_obj.save(output_filepath) From d6a93414cd97e24bc987b72baca1a118a6e3fca4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:50:22 +0100 Subject: [PATCH 23/40] fixed hound --- pype/plugins/tvpaint/publish/collect_workfile_data.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index f25e2745814..bb25e244ef7 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -113,7 +113,8 @@ def process(self, context): self.log.info("Collecting scene data from workfile") workfile_info_parts = lib.execute_george("tv_projectinfo").split(" ") - _frame_start = int(workfile_info_parts.pop(-1)) + # Project frame start - not used + workfile_info_parts.pop(-1) field_order = workfile_info_parts.pop(-1) frame_rate = float(workfile_info_parts.pop(-1)) pixel_apsect = float(workfile_info_parts.pop(-1)) @@ -141,8 +142,8 @@ def collect_clip_frames(self): clip_info_str = lib.execute_george("tv_clipinfo") self.log.debug("Clip info: {}".format(clip_info_str)) clip_info_items = clip_info_str.split(" ") - # Color index - color_idx = clip_info_items.pop(-1) + # Color index - not used + clip_info_items.pop(-1) clip_info_items.pop(-1) mark_out = int(clip_info_items.pop(-1)) + 1 From df5916e43a597c2e5ef66f0cf8d434075709d5a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:57:02 +0100 Subject: [PATCH 24/40] fix variable names --- pype/plugins/tvpaint/publish/collect_workfile_data.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index bb25e244ef7..7965112136c 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -146,10 +146,12 @@ def collect_clip_frames(self): clip_info_items.pop(-1) clip_info_items.pop(-1) - mark_out = int(clip_info_items.pop(-1)) + 1 + mark_out = int(clip_info_items.pop(-1)) + frame_end = mark_out + 1 clip_info_items.pop(-1) - mark_in = int(clip_info_items.pop(-1)) + 1 + mark_in = int(clip_info_items.pop(-1)) + frame_start = mark_in + 1 clip_info_items.pop(-1) - return mark_in, mark_out + return frame_start, frame_end From e3b686ed17edb5fbbcc9af895c5ca436337b349f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:03:08 +0100 Subject: [PATCH 25/40] fix frame range of pass output --- pype/plugins/tvpaint/publish/collect_instances.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index 1a5a187c16b..efe265e7916 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -70,15 +70,8 @@ def process(self, context): if instance is None: continue - frame_start = context.data["frameStart"] - frame_end = frame_start - for layer in instance.data["layers"]: - _frame_end = layer["frame_end"] - if _frame_end > frame_end: - frame_end = _frame_end - - instance.data["frameStart"] = frame_start - instance.data["frameEnd"] = frame_end + instance.data["frameStart"] = context.data["frameStart"] + instance.data["frameEnd"] = context.data["frameEnd"] self.log.debug("Created instance: {}\n{}".format( instance, json.dumps(instance.data, indent=4) From 86ccfb60d2a02fc88ec2d239deb304e9d8ffe2ec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:17:12 +0100 Subject: [PATCH 26/40] renamed extract sequence to extract review sequence --- ...sequence.py => extract_review_sequence.py} | 40 ++----------------- 1 file changed, 3 insertions(+), 37 deletions(-) rename pype/plugins/tvpaint/publish/{extract_sequence.py => extract_review_sequence.py} (89%) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_review_sequence.py similarity index 89% rename from pype/plugins/tvpaint/publish/extract_sequence.py rename to pype/plugins/tvpaint/publish/extract_review_sequence.py index 17d8dc60f47..54f21cb9742 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_review_sequence.py @@ -6,44 +6,10 @@ from PIL import Image -class ExtractSequence(pyblish.api.Extractor): - label = "Extract Sequence" +class ExtractReviewSequence(pyblish.api.Extractor): + label = "Extract Review Sequence" hosts = ["tvpaint"] - families = ["review", "renderPass", "renderLayer"] - - save_mode_to_ext = { - "avi": ".avi", - "bmp": ".bmp", - "cin": ".cin", - "deep": ".dip", - "dps": ".dps", - "dpx": ".dpx", - "flc": ".fli", - "gif": ".gif", - "ilbm": ".iff", - "jpg": ".jpg", - "jpeg": ".jpg", - "pcx": ".pcx", - "png": ".png", - "psd": ".psd", - "qt": ".qt", - "rtv": ".rtv", - "sun": ".ras", - "tiff": ".tiff", - "tga": ".tga", - "vpb": ".vpb" - } - sequential_save_mode = { - "bmp", - "dpx", - "ilbm", - "jpg", - "jpeg", - "png", - "sun", - "tiff", - "tga" - } + families = ["review"] def process(self, instance): self.log.info( From 7cb7a5b9b1272e4fb36cc0199e6fd643d65622bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:18:40 +0100 Subject: [PATCH 27/40] moved back tvpaint's lib for compositing --- pype/hosts/tvpaint/lib.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 pype/hosts/tvpaint/lib.py diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py new file mode 100644 index 00000000000..8172392c7f2 --- /dev/null +++ b/pype/hosts/tvpaint/lib.py @@ -0,0 +1,17 @@ +from PIL import Image + + +def composite_images( + input_image_paths, output_filepath, scene_width, scene_height +): + img_obj = None + for image_filepath in input_image_paths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + else: + img_obj.alpha_composite(_img_obj) + + if img_obj is None: + img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) + img_obj.save(output_filepath) From 253fba67a7ccd30ba5438d004b0d4fd6129f4cd4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:19:57 +0100 Subject: [PATCH 28/40] implemented extract review that can render layer by layer with alpha --- .../tvpaint/publish/extract_sequence.py | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 pype/plugins/tvpaint/publish/extract_sequence.py diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py new file mode 100644 index 00000000000..035f50c0585 --- /dev/null +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -0,0 +1,492 @@ +import os +import shutil +import time +import tempfile +import multiprocessing + +import pyblish.api +from avalon.tvpaint import lib +from pype.hosts.tvpaint.lib import composite_images +from PIL import Image + + +class ExtractSequence(pyblish.api.Extractor): + label = "Extract Sequence" + hosts = ["tvpaint"] + families = ["renderPass", "renderLayer"] + + def process(self, instance): + self.log.info( + "* Processing instance \"{}\"".format(instance.data["label"]) + ) + + # Get all layers and filter out not visible + layers = instance.data["layers"] + filtered_layers = [ + layer + for layer in layers + if layer["visible"] + ] + layer_names = [str(layer["name"]) for layer in filtered_layers] + if not layer_names: + self.log.info( + "None of the layers from the instance" + " are visible. Extraction skipped." + ) + return + + joined_layer_names = ", ".join( + ["\"{}\"".format(name) for name in layer_names] + ) + self.log.debug( + "Instance has {} layers with names: {}".format( + len(layer_names), joined_layer_names + ) + ) + + family_lowered = instance.data["family"].lower() + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + scene_width = instance.context.data["sceneWidth"] + scene_height = instance.context.data["sceneHeight"] + + filename_template = self._get_filename_template(frame_end) + ext = os.path.splitext(filename_template)[1].replace(".", "") + + self.log.debug("Using file template \"{}\"".format(filename_template)) + + # Save to staging dir + output_dir = instance.data.get("stagingDir") + if not output_dir: + # Create temp folder if staging dir is not set + output_dir = tempfile.mkdtemp().replace("\\", "/") + instance.data["stagingDir"] = output_dir + + self.log.debug( + "Files will be rendered to folder: {}".format(output_dir) + ) + + thumbnail_filename = "thumbnail.jpg" + + # Render output + output_filepaths, thumbnail_fullpath = self.render( + filename_template, output_dir, filtered_layers, + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height + ) + + # Fill tags and new families + tags = [] + if family_lowered in ("review", "renderlayer"): + tags.append("review") + + repre_files = [ + os.path.basename(filepath) + for filepath in output_filepaths + ] + # Sequence of one frame + if len(repre_files) == 1: + repre_files = repre_files[0] + + new_repre = { + "name": ext, + "ext": ext, + "files": repre_files, + "stagingDir": output_dir, + "frameStart": frame_start, + "frameEnd": frame_end, + "tags": tags + } + self.log.debug("Creating new representation: {}".format(new_repre)) + + instance.data["representations"].append(new_repre) + + if family_lowered in ("renderpass", "renderlayer"): + # Change family to render + instance.data["family"] = "render" + + if not thumbnail_fullpath: + return + + thumbnail_ext = os.path.splitext( + thumbnail_fullpath + )[1].replace(".", "") + # Create thumbnail representation + thumbnail_repre = { + "name": "thumbnail", + "ext": thumbnail_ext, + "outputName": "thumb", + "files": os.path.basename(thumbnail_fullpath), + "stagingDir": output_dir, + "tags": ["thumbnail"] + } + instance.data["representations"].append(thumbnail_repre) + + def _get_filename_template(self, frame_end): + """Get filetemplate for rendered files. + + This is simple template contains `{frame}{ext}` for sequential outputs + and `single_file{ext}` for single file output. Output is rendered to + temporary folder so filename should not matter as integrator change + them. + """ + frame_padding = 4 + frame_end_str_len = len(str(frame_end)) + if frame_end_str_len > frame_padding: + frame_padding = frame_end_str_len + + return "{{:0>{}}}".format(frame_padding) + ".png" + + def render( + self, filename_template, output_dir, layers, + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height + ): + """ Export images from TVPaint. + + Args: + save_mode (str): Argument for `tv_savemode` george script function. + More about save mode in documentation. + filename_template (str): Filename template of an output. Template + should already contain extension. Template may contain only + keyword argument `{frame}` or index argument (for same value). + Extension in template must match `save_mode`. + layers (list): List of layers to be exported. + first_frame (int): Starting frame from which export will begin. + last_frame (int): On which frame export will end. + + Retruns: + dict: Mapping frame to output filepath. + """ + self.log.debug("Preparing data for rendering.") + + # Map layers by position + layers_by_position = {} + layer_ids = [] + for layer in layers: + position = layer["position"] + layers_by_position[position] = layer + + layer_ids.append(layer["layer_id"]) + + # Sort layer positions in reverse order + sorted_positions = list(reversed(sorted(layers_by_position.keys()))) + if not sorted_positions: + return + + self.log.debug("Collecting pre/post behavior of individual layers.") + behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) + + mark_in_index = frame_start - 1 + mark_out_index = frame_end - 1 + + tmp_filename_template = "pos_{}." + filename_template + + files_by_position = {} + for position in sorted_positions: + layer = layers_by_position[position] + behavior = behavior_by_layer_id[layer["layer_id"]] + files_by_frames = self.render_layer( + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ) + files_by_position[position] = files_by_frames + + output = self._composite_files( + files_by_position, + output_dir, + mark_in_index, + mark_out_index, + filename_template, + thumbnail_filename, + scene_width, + scene_height + ) + self._cleanup_tmp_files(files_by_position) + return output + + def render_layer( + self, + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ): + layer_id = layer["layer_id"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + exposure_frames = lib.get_exposure_frames( + layer_id, frame_start_index, frame_end_index + ) + if frame_start_index not in exposure_frames: + exposure_frames.append(frame_start_index) + + layer_files_by_frame = {} + george_script_lines = [ + "tv_SaveMode \"PNG\"" + ] + layer_position = layer["position"] + + for frame_idx in exposure_frames: + filename = tmp_filename_template.format(layer_position, frame_idx) + dst_path = "/".join([output_dir, filename]) + layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) + + # Go to frame + george_script_lines.append("tv_layerImage {}".format(frame_idx)) + # Store image to output + george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + + self.log.debug("Rendering exposure frames {} of layer {}".format( + str(exposure_frames), layer_id + )) + # Let TVPaint render layer's image + lib.execute_george_through_file("\n".join(george_script_lines)) + + # Fill frames between `frame_start_index` and `frame_end_index` + self.log.debug(( + "Filling frames between first and last frame of layer ({} - {})." + ).format(frame_start_index + 1, frame_end_index + 1)) + + prev_filepath = None + for frame_idx in range(frame_start_index, frame_end_index + 1): + if frame_idx in layer_files_by_frame: + prev_filepath = layer_files_by_frame[frame_idx] + continue + + if prev_filepath is None: + raise ValueError("BUG: First frame of layer was not rendered!") + + filename = tmp_filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(prev_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + # Fill frames by pre/post behavior of layer + pre_behavior = behavior["pre"] + post_behavior = behavior["post"] + self.log.debug(( + "Completing image sequence of layer by pre/post behavior." + " PRE: {} | POST: {}" + ).format(pre_behavior, post_behavior)) + + # Pre behavior + self._fill_frame_by_pre_behavior( + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + self._fill_frame_by_post_behavior( + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + return layer_files_by_frame + + def _fill_frame_by_pre_behavior( + self, + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_in_index >= frame_start_index: + return + + if pre_behavior == "none": + return + + if pre_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_start_index] + for frame_idx in range(mark_in_index, frame_start_index): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = ( + (frame_end_index - frame_idx) % frame_count + ) + eq_frame_idx = frame_end_index - eq_frame_idx_offset + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = (seq_len - eq_frame_idx_offset) + eq_frame_idx = frame_start_index + eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + def _fill_frame_by_post_behavior( + self, + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_out_index <= frame_end_index: + return + + if post_behavior == "none": + return + + if post_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_end_index] + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx = frame_idx % frame_count + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = seq_len - eq_frame_idx_offset + eq_frame_idx = frame_end_index - eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + def _composite_files( + self, files_by_position, output_dir, frame_start, frame_end, + filename_template, thumbnail_filename, scene_width, scene_height + ): + # Prepare paths to images by frames into list where are stored + # in order of compositing. + images_by_frame = {} + for frame_idx in range(frame_start, frame_end + 1): + images_by_frame[frame_idx] = [] + for position in sorted(files_by_position.keys(), reverse=True): + position_data = files_by_position[position] + if frame_idx in position_data: + images_by_frame[frame_idx].append(position_data[frame_idx]) + + process_count = os.cpu_count() + if process_count > 1: + process_count -= 1 + + processes = {} + output_filepaths = [] + thumbnail_src_filepath = None + for frame_idx in sorted(images_by_frame.keys()): + image_filepaths = images_by_frame[frame_idx] + frame = frame_idx + 1 + + output_filename = filename_template.format(frame) + output_filepath = os.path.join(output_dir, output_filename) + output_filepaths.append(output_filepath) + + if thumbnail_filename and thumbnail_src_filepath is None: + thumbnail_src_filepath = output_filepath + + processes[frame_idx] = multiprocessing.Process( + target=composite_images, + args=( + image_filepaths, output_filepath, scene_width, scene_height + ) + ) + + # Wait until all processes are done + running_processes = {} + while True: + for idx in tuple(running_processes.keys()): + process = running_processes[idx] + if not process.is_alive(): + running_processes.pop(idx).join() + + if processes and len(running_processes) != process_count: + indexes = list(processes.keys()) + for _ in range(process_count - len(running_processes)): + if not indexes: + break + idx = indexes.pop(0) + running_processes[idx] = processes.pop(idx) + running_processes[idx].start() + + if not running_processes and not processes: + break + + time.sleep(0.01) + + thumbnail_filepath = None + if thumbnail_src_filepath: + source_img = Image.open(thumbnail_src_filepath) + thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + + return output_filepaths, thumbnail_filepath + + def _cleanup_tmp_files(self, files_by_position): + for data in files_by_position.values(): + for filepath in data.values(): + os.remove(filepath) + + def _copy_image(self, src_path, dst_path): + # Create hardlink of image instead of copying if possible + if hasattr(os, "link"): + os.link(src_path, dst_path) + else: + shutil.copy(src_path, dst_path) From 285d91d5e2225feea8b0bc797d7a4cd5eeb46a37 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:27:35 +0100 Subject: [PATCH 29/40] modified extract sequence to skip compositing is rendering only one layer --- .../tvpaint/publish/extract_sequence.py | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 035f50c0585..ad87ebbd81e 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -183,31 +183,55 @@ def render( tmp_filename_template = "pos_{}." + filename_template files_by_position = {} + is_single_layer = len(sorted_positions) == 1 for position in sorted_positions: layer = layers_by_position[position] behavior = behavior_by_layer_id[layer["layer_id"]] + + if is_single_layer: + _template = filename_template + else: + _template = tmp_filename_template + files_by_frames = self.render_layer( layer, - tmp_filename_template, + _template, output_dir, behavior, mark_in_index, mark_out_index ) - files_by_position[position] = files_by_frames + if is_single_layer: + output_filepaths = list(files_by_frames.values()) + else: + files_by_position[position] = files_by_frames + + if not is_single_layer: + output_filepaths = self._composite_files( + files_by_position, + output_dir, + mark_in_index, + mark_out_index, + filename_template, + thumbnail_filename, + scene_width, + scene_height + ) + self._cleanup_tmp_files(files_by_position) - output = self._composite_files( - files_by_position, - output_dir, - mark_in_index, - mark_out_index, - filename_template, - thumbnail_filename, - scene_width, - scene_height - ) - self._cleanup_tmp_files(files_by_position) - return output + thumbnail_src_filepath = None + thumbnail_filepath = None + if output_filepaths: + thumbnail_src_filepath = tuple(sorted(output_filepaths))[0] + + if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath): + source_img = Image.open(thumbnail_src_filepath) + thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + + return output_filepaths, thumbnail_filepath def render_layer( self, @@ -469,15 +493,7 @@ def _composite_files( time.sleep(0.01) - thumbnail_filepath = None - if thumbnail_src_filepath: - source_img = Image.open(thumbnail_src_filepath) - thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) - - return output_filepaths, thumbnail_filepath + return output_filepaths def _cleanup_tmp_files(self, files_by_position): for data in files_by_position.values(): From 87142459344234cacbea9165a38278098faf4933 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:58:29 +0100 Subject: [PATCH 30/40] removed thumbnail filename variable --- .../plugins/tvpaint/publish/extract_sequence.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index ad87ebbd81e..a66141fa193 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -66,13 +66,10 @@ def process(self, instance): "Files will be rendered to folder: {}".format(output_dir) ) - thumbnail_filename = "thumbnail.jpg" - # Render output output_filepaths, thumbnail_fullpath = self.render( filename_template, output_dir, filtered_layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height + frame_start, frame_end, scene_width, scene_height ) # Fill tags and new families @@ -139,8 +136,7 @@ def _get_filename_template(self, frame_end): def render( self, filename_template, output_dir, layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height + frame_start, frame_end, scene_width, scene_height ): """ Export images from TVPaint. @@ -213,7 +209,6 @@ def render( mark_in_index, mark_out_index, filename_template, - thumbnail_filename, scene_width, scene_height ) @@ -226,7 +221,7 @@ def render( if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath): source_img = Image.open(thumbnail_src_filepath) - thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) thumbnail_obj.paste(source_img) thumbnail_obj.save(thumbnail_filepath) @@ -434,7 +429,7 @@ def _fill_frame_by_post_behavior( def _composite_files( self, files_by_position, output_dir, frame_start, frame_end, - filename_template, thumbnail_filename, scene_width, scene_height + filename_template, scene_width, scene_height ): # Prepare paths to images by frames into list where are stored # in order of compositing. @@ -452,7 +447,6 @@ def _composite_files( processes = {} output_filepaths = [] - thumbnail_src_filepath = None for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] frame = frame_idx + 1 @@ -461,9 +455,6 @@ def _composite_files( output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) - if thumbnail_filename and thumbnail_src_filepath is None: - thumbnail_src_filepath = output_filepath - processes[frame_idx] = multiprocessing.Process( target=composite_images, args=( From 8004d3c47501b02a2268cb859e7f2c74e7c1d5b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 14:21:06 +0100 Subject: [PATCH 31/40] merged extractors to one extractor --- .../publish/extract_review_sequence.py | 206 ------------------ .../tvpaint/publish/extract_sequence.py | 88 ++++++-- 2 files changed, 74 insertions(+), 220 deletions(-) delete mode 100644 pype/plugins/tvpaint/publish/extract_review_sequence.py diff --git a/pype/plugins/tvpaint/publish/extract_review_sequence.py b/pype/plugins/tvpaint/publish/extract_review_sequence.py deleted file mode 100644 index 54f21cb9742..00000000000 --- a/pype/plugins/tvpaint/publish/extract_review_sequence.py +++ /dev/null @@ -1,206 +0,0 @@ -import os -import tempfile - -import pyblish.api -from avalon.tvpaint import lib -from PIL import Image - - -class ExtractReviewSequence(pyblish.api.Extractor): - label = "Extract Review Sequence" - hosts = ["tvpaint"] - families = ["review"] - - def process(self, instance): - self.log.info( - "* Processing instance \"{}\"".format(instance.data["label"]) - ) - - # Get all layers and filter out not visible - layers = instance.data["layers"] - filtered_layers = [ - layer - for layer in layers - if layer["visible"] - ] - filtered_layer_ids = [ - layer["layer_id"] - for layer in filtered_layers - ] - layer_names = [str(layer["name"]) for layer in filtered_layers] - if not layer_names: - self.log.info( - "None of the layers from the instance" - " are visible. Extraction skipped." - ) - return - - joined_layer_names = ", ".join( - ["\"{}\"".format(name) for name in layer_names] - ) - self.log.debug( - "Instance has {} layers with names: {}".format( - len(layer_names), joined_layer_names - ) - ) - - family_lowered = instance.data["family"].lower() - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - - filename_template = self._get_filename_template(frame_end) - ext = os.path.splitext(filename_template)[1].replace(".", "") - - self.log.debug("Using file template \"{}\"".format(filename_template)) - - # Save to staging dir - output_dir = instance.data.get("stagingDir") - if not output_dir: - # Create temp folder if staging dir is not set - output_dir = tempfile.mkdtemp().replace("\\", "/") - instance.data["stagingDir"] = output_dir - - self.log.debug( - "Files will be rendered to folder: {}".format(output_dir) - ) - - first_frame_filename = filename_template.format(frame_start) - first_frame_filepath = os.path.join(output_dir, first_frame_filename) - - # Store layers visibility - layer_visibility_by_id = {} - for layer in instance.context.data["layersData"]: - layer_id = layer["layer_id"] - layer_visibility_by_id[layer_id] = layer["visible"] - - george_script_lines = [] - for layer_id in layer_visibility_by_id.keys(): - visible = layer_id in filtered_layer_ids - value = "on" if visible else "off" - george_script_lines.append( - "tv_layerdisplay {} \"{}\"".format(layer_id, value) - ) - lib.execute_george_through_file("\n".join(george_script_lines)) - - # Render output - repre_files = self.render( - filename_template, - output_dir, - frame_start, - frame_end - ) - - # Restore visibility - george_script_lines = [] - for layer_id, visible in layer_visibility_by_id.items(): - value = "on" if visible else "off" - george_script_lines.append( - "tv_layerdisplay {} \"{}\"".format(layer_id, value) - ) - lib.execute_george_through_file("\n".join(george_script_lines)) - - thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") - if os.path.exists(first_frame_filepath): - source_img = Image.open(first_frame_filepath) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) - - # Fill tags and new families - tags = [] - if family_lowered in ("review", "renderlayer"): - tags.append("review") - - # Sequence of one frame - if len(repre_files) == 1: - repre_files = repre_files[0] - - new_repre = { - "name": ext, - "ext": ext, - "files": repre_files, - "stagingDir": output_dir, - "frameStart": frame_start, - "frameEnd": frame_end, - "tags": tags - } - self.log.debug("Creating new representation: {}".format(new_repre)) - - instance.data["representations"].append(new_repre) - - if family_lowered in ("renderpass", "renderlayer"): - # Change family to render - instance.data["family"] = "render" - - if not os.path.exists(thumbnail_filepath): - return - - thumbnail_ext = os.path.splitext( - thumbnail_filepath - )[1].replace(".", "") - # Create thumbnail representation - thumbnail_repre = { - "name": "thumbnail", - "ext": thumbnail_ext, - "outputName": "thumb", - "files": os.path.basename(thumbnail_filepath), - "stagingDir": output_dir, - "tags": ["thumbnail"] - } - instance.data["representations"].append(thumbnail_repre) - - def _get_filename_template(self, frame_end): - """Get filetemplate for rendered files. - - This is simple template contains `{frame}{ext}` for sequential outputs - and `single_file{ext}` for single file output. Output is rendered to - temporary folder so filename should not matter as integrator change - them. - """ - frame_padding = 4 - frame_end_str_len = len(str(frame_end)) - if frame_end_str_len > frame_padding: - frame_padding = frame_end_str_len - - return "{{:0>{}}}".format(frame_padding) + ".png" - - def render(self, filename_template, output_dir, frame_start, frame_end): - """ Export images from TVPaint. - - Args: - filename_template (str): Filename template of an output. Template - should already contain extension. Template may contain only - keyword argument `{frame}` or index argument (for same value). - Extension in template must match `save_mode`. - output_dir (list): List of layers to be exported. - frame_start (int): Starting frame from which export will begin. - frame_end (int): On which frame export will end. - - Retruns: - dict: Mapping frame to output filepath. - """ - self.log.debug("Preparing data for rendering.") - first_frame_filepath = os.path.join( - output_dir, - filename_template.format(frame_start, frame=frame_start) - ) - mark_in = frame_start - 1 - mark_out = frame_end - 1 - - george_script_lines = [ - "tv_SaveMode \"PNG\"", - "export_path = \"{}\"".format( - first_frame_filepath.replace("\\", "/") - ), - "tv_savesequence '\"'export_path'\"' {} {}".format( - mark_in, mark_out - ) - ] - lib.execute_george_through_file("\n".join(george_script_lines)) - - output = [] - for frame in range(frame_start, frame_end + 1): - output.append( - filename_template.format(frame, frame=frame) - ) - return output diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index a66141fa193..6fe35f62519 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -13,7 +13,7 @@ class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] - families = ["renderPass", "renderLayer"] + families = ["review", "renderPass", "renderLayer"] def process(self, instance): self.log.info( @@ -66,21 +66,22 @@ def process(self, instance): "Files will be rendered to folder: {}".format(output_dir) ) - # Render output - output_filepaths, thumbnail_fullpath = self.render( - filename_template, output_dir, filtered_layers, - frame_start, frame_end, scene_width, scene_height - ) + if instance.data["family"] == "review": + repre_files, thumbnail_fullpath = self.render_review( + filename_template, output_dir, frame_start, frame_end + ) + else: + # Render output + repre_files, thumbnail_fullpath = self.render( + filename_template, output_dir, filtered_layers, + frame_start, frame_end, scene_width, scene_height + ) # Fill tags and new families tags = [] if family_lowered in ("review", "renderlayer"): tags.append("review") - repre_files = [ - os.path.basename(filepath) - for filepath in output_filepaths - ] # Sequence of one frame if len(repre_files) == 1: repre_files = repre_files[0] @@ -134,6 +135,58 @@ def _get_filename_template(self, frame_end): return "{{:0>{}}}".format(frame_padding) + ".png" + def render_review( + self, filename_template, output_dir, frame_start, frame_end + ): + """ Export images from TVPaint. + + Args: + filename_template (str): Filename template of an output. Template + should already contain extension. Template may contain only + keyword argument `{frame}` or index argument (for same value). + Extension in template must match `save_mode`. + output_dir (list): List of layers to be exported. + frame_start (int): Starting frame from which export will begin. + frame_end (int): On which frame export will end. + + Retruns: + dict: Mapping frame to output filepath. + """ + self.log.debug("Preparing data for rendering.") + first_frame_filepath = os.path.join( + output_dir, + filename_template.format(frame_start, frame=frame_start) + ) + mark_in = frame_start - 1 + mark_out = frame_end - 1 + + george_script_lines = [ + "tv_SaveMode \"PNG\"", + "export_path = \"{}\"".format( + first_frame_filepath.replace("\\", "/") + ), + "tv_savesequence '\"'export_path'\"' {} {}".format( + mark_in, mark_out + ) + ] + lib.execute_george_through_file("\n".join(george_script_lines)) + + output = [] + first_frame_filepath = None + for frame in range(frame_start, frame_end + 1): + filename = filename_template.format(frame, frame=frame) + output.append(filename) + if first_frame_filepath is None: + first_frame_filepath = os.path.join(output_dir, filename) + + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") + if first_frame_filepath and os.path.exists(first_frame_filepath): + source_img = Image.open(first_frame_filepath) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + return output, thumbnail_filepath + def render( self, filename_template, output_dir, layers, frame_start, frame_end, scene_width, scene_height @@ -189,7 +242,7 @@ def render( else: _template = tmp_filename_template - files_by_frames = self.render_layer( + files_by_frames = self._render_layer( layer, _template, output_dir, @@ -226,9 +279,13 @@ def render( thumbnail_obj.paste(source_img) thumbnail_obj.save(thumbnail_filepath) - return output_filepaths, thumbnail_filepath + repre_files = [ + os.path.basename(path) + for path in output_filepaths + ] + return repre_files, thumbnail_filepath - def render_layer( + def _render_layer( self, layer, tmp_filename_template, @@ -282,7 +339,10 @@ def render_layer( if prev_filepath is None: raise ValueError("BUG: First frame of layer was not rendered!") - filename = tmp_filename_template.format(layer_position, frame_idx) + filename = tmp_filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(prev_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath From 445b6b5dd6565ab557006427e0d53d41c9363ecc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 14:31:53 +0100 Subject: [PATCH 32/40] extract sequence use key word arguments in filename template --- .../tvpaint/publish/extract_sequence.py | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 6fe35f62519..b7f01982edd 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -133,7 +133,7 @@ def _get_filename_template(self, frame_end): if frame_end_str_len > frame_padding: frame_padding = frame_end_str_len - return "{{:0>{}}}".format(frame_padding) + ".png" + return "{{frame:0>{}}}".format(frame_padding) + ".png" def render_review( self, filename_template, output_dir, frame_start, frame_end @@ -142,9 +142,9 @@ def render_review( Args: filename_template (str): Filename template of an output. Template - should already contain extension. Template may contain only - keyword argument `{frame}` or index argument (for same value). - Extension in template must match `save_mode`. + should already contain extension. Template must contain + keyword argument `{frame}`. Extension in template must match + `save_mode`. output_dir (list): List of layers to be exported. frame_start (int): Starting frame from which export will begin. frame_end (int): On which frame export will end. @@ -155,7 +155,7 @@ def render_review( self.log.debug("Preparing data for rendering.") first_frame_filepath = os.path.join( output_dir, - filename_template.format(frame_start, frame=frame_start) + filename_template.format(frame=frame_start) ) mark_in = frame_start - 1 mark_out = frame_end - 1 @@ -174,7 +174,7 @@ def render_review( output = [] first_frame_filepath = None for frame in range(frame_start, frame_end + 1): - filename = filename_template.format(frame, frame=frame) + filename = filename_template.format(frame=frame) output.append(filename) if first_frame_filepath is None: first_frame_filepath = os.path.join(output_dir, filename) @@ -229,7 +229,7 @@ def render( mark_in_index = frame_start - 1 mark_out_index = frame_end - 1 - tmp_filename_template = "pos_{}." + filename_template + tmp_filename_template = "pos_{pos}." + filename_template files_by_position = {} is_single_layer = len(sorted_positions) == 1 @@ -310,7 +310,10 @@ def _render_layer( layer_position = layer["position"] for frame_idx in exposure_frames: - filename = tmp_filename_template.format(layer_position, frame_idx) + filename = tmp_filename_template.format( + pos=layer_position, + frame=frame_idx + ) dst_path = "/".join([output_dir, filename]) layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) @@ -397,7 +400,10 @@ def _fill_frame_by_pre_behavior( # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_start_index] for frame_idx in range(mark_in_index, frame_start_index): - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -411,7 +417,10 @@ def _fill_frame_by_pre_behavior( eq_frame_idx = frame_end_index - eq_frame_idx_offset eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -427,7 +436,10 @@ def _fill_frame_by_pre_behavior( eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -455,7 +467,10 @@ def _fill_frame_by_post_behavior( # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_end_index] for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -466,7 +481,10 @@ def _fill_frame_by_post_behavior( eq_frame_idx = frame_idx % frame_count eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -482,7 +500,10 @@ def _fill_frame_by_post_behavior( eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -511,7 +532,7 @@ def _composite_files( image_filepaths = images_by_frame[frame_idx] frame = frame_idx + 1 - output_filename = filename_template.format(frame) + output_filename = filename_template.format(frame=frame) output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) From aae052244bda45f3d1280e1f157fe96a17b167ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 14:46:58 +0100 Subject: [PATCH 33/40] handle "none" behavior --- .../tvpaint/publish/extract_sequence.py | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index b7f01982edd..2c318136e6b 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -7,7 +7,7 @@ import pyblish.api from avalon.tvpaint import lib from pype.hosts.tvpaint.lib import composite_images -from PIL import Image +from PIL import Image, ImageDraw class ExtractSequence(pyblish.api.Extractor): @@ -394,9 +394,29 @@ def _fill_frame_by_pre_behavior( return if pre_behavior == "none": - return - - if pre_behavior == "hold": + # Take size from first image and fill it with transparent color + first_filename = filename_template.format( + pos=layer_position, + frame=frame_start_index + ) + first_filepath = os.path.join(output_dir, first_filename) + empty_image_filepath = None + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) + filepath = os.path.join(output_dir, filename) + if empty_image_filepath is None: + img_obj = Image.open(first_filepath) + painter = ImageDraw.Draw(img_obj) + painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) + img_obj.save(filepath) + empty_image_filepath = filepath + else: + self._copy_image(empty_image_filepath, filepath) + + elif pre_behavior == "hold": # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_start_index] for frame_idx in range(mark_in_index, frame_start_index): @@ -461,9 +481,29 @@ def _fill_frame_by_post_behavior( return if post_behavior == "none": - return - - if post_behavior == "hold": + # Take size from last image and fill it with transparent color + last_filename = filename_template.format( + pos=layer_position, + frame=frame_end_index + ) + last_filepath = os.path.join(output_dir, last_filename) + empty_image_filepath = None + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) + filepath = os.path.join(output_dir, filename) + if empty_image_filepath is None: + img_obj = Image.open(last_filepath) + painter = ImageDraw.Draw(img_obj) + painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) + img_obj.save(filepath) + empty_image_filepath = filepath + else: + self._copy_image(empty_image_filepath, filepath) + + elif post_behavior == "hold": # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_end_index] for frame_idx in range(frame_end_index + 1, mark_out_index + 1): From 7c696a09600e59e44bea42559dd4319eb36b5689 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:26:41 +0100 Subject: [PATCH 34/40] add files to `layer_files_by_frame` on creation --- .../tvpaint/publish/extract_sequence.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 2c318136e6b..667799a7cd2 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -406,15 +406,16 @@ def _fill_frame_by_pre_behavior( pos=layer_position, frame=frame_idx ) - filepath = os.path.join(output_dir, filename) + new_filepath = os.path.join(output_dir, filename) if empty_image_filepath is None: img_obj = Image.open(first_filepath) painter = ImageDraw.Draw(img_obj) painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(filepath) - empty_image_filepath = filepath + img_obj.save(new_filepath) + empty_image_filepath = new_filepath else: - self._copy_image(empty_image_filepath, filepath) + self._copy_image(empty_image_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath elif pre_behavior == "hold": # Keep first frame for whole time @@ -493,15 +494,16 @@ def _fill_frame_by_post_behavior( pos=layer_position, frame=frame_idx ) - filepath = os.path.join(output_dir, filename) + new_filepath = os.path.join(output_dir, filename) if empty_image_filepath is None: img_obj = Image.open(last_filepath) painter = ImageDraw.Draw(img_obj) painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(filepath) - empty_image_filepath = filepath + img_obj.save(new_filepath) + empty_image_filepath = new_filepath else: - self._copy_image(empty_image_filepath, filepath) + self._copy_image(empty_image_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath elif post_behavior == "hold": # Keep first frame for whole time From 075080b658498170800f3563ab1669176f0bba5e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:26:52 +0100 Subject: [PATCH 35/40] added some debug loggins messages --- .../tvpaint/publish/extract_sequence.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 667799a7cd2..8f302cb746b 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -300,6 +300,9 @@ def _render_layer( exposure_frames = lib.get_exposure_frames( layer_id, frame_start_index, frame_end_index ) + self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( + str(exposure_frames), layer["layer_id"], layer["name"] + )) if frame_start_index not in exposure_frames: exposure_frames.append(frame_start_index) @@ -333,6 +336,7 @@ def _render_layer( "Filling frames between first and last frame of layer ({} - {})." ).format(frame_start_index + 1, frame_end_index + 1)) + _debug_filled_frames = [] prev_filepath = None for frame_idx in range(frame_start_index, frame_end_index + 1): if frame_idx in layer_files_by_frame: @@ -341,7 +345,7 @@ def _render_layer( if prev_filepath is None: raise ValueError("BUG: First frame of layer was not rendered!") - + _debug_filled_frames.append(frame_idx) filename = tmp_filename_template.format( pos=layer_position, frame=frame_idx @@ -350,6 +354,8 @@ def _render_layer( self._copy_image(prev_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath + self.log.debug("Filled frames {}".format(str(_debug_filled_frames))) + # Fill frames by pre/post behavior of layer pre_behavior = behavior["pre"] post_behavior = behavior["post"] @@ -391,9 +397,16 @@ def _fill_frame_by_pre_behavior( frame_end_index = layer["frame_end"] frame_count = frame_end_index - frame_start_index + 1 if mark_in_index >= frame_start_index: + self.log.debug(( + "Skipping pre-behavior." + " All frames after Mark In are rendered." + )) return if pre_behavior == "none": + self.log.debug("Creating empty images for range {} - {}".format( + mark_in_index, frame_start_index + )) # Take size from first image and fill it with transparent color first_filename = filename_template.format( pos=layer_position, @@ -479,9 +492,16 @@ def _fill_frame_by_post_behavior( frame_end_index = layer["frame_end"] frame_count = frame_end_index - frame_start_index + 1 if mark_out_index <= frame_end_index: + self.log.debug(( + "Skipping post-behavior." + " All frames up to Mark Out are rendered." + )) return if post_behavior == "none": + self.log.debug("Creating empty images for range {} - {}".format( + frame_end_index + 1, mark_out_index + 1 + )) # Take size from last image and fill it with transparent color last_filename = filename_template.format( pos=layer_position, From 97446f63a784b2e7cb2482d8f39e5e45ae8fa9b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:56:37 +0100 Subject: [PATCH 36/40] review instance stores copy of layers data --- pype/plugins/tvpaint/publish/collect_instances.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index efe265e7916..57602d96103 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -48,7 +48,10 @@ def process(self, context): instance_data["subset"] = new_subset_name instance = context.create_instance(**instance_data) - instance.data["layers"] = context.data["layersData"] + + instance.data["layers"] = copy.deepcopy( + context.data["layersData"] + ) # Add ftrack family instance.data["families"].append("ftrack") From d69f70082daaad3b0ea0a7cd58ae61cfb1322267 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:56:53 +0100 Subject: [PATCH 37/40] added validation of layers visibility --- .../publish/validate_layers_visibility.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pype/plugins/tvpaint/publish/validate_layers_visibility.py diff --git a/pype/plugins/tvpaint/publish/validate_layers_visibility.py b/pype/plugins/tvpaint/publish/validate_layers_visibility.py new file mode 100644 index 00000000000..74ef34169ed --- /dev/null +++ b/pype/plugins/tvpaint/publish/validate_layers_visibility.py @@ -0,0 +1,16 @@ +import pyblish.api + + +class ValidateLayersVisiblity(pyblish.api.InstancePlugin): + """Validate existence of renderPass layers.""" + + label = "Validate Layers Visibility" + order = pyblish.api.ValidatorOrder + families = ["review", "renderPass", "renderLayer"] + + def process(self, instance): + for layer in instance.data["layers"]: + if layer["visible"]: + return + + raise AssertionError("All layers of instance are not visible.") From b1cda0b527ea7d88a5415f72de5d05a9514b0788 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:57:33 +0100 Subject: [PATCH 38/40] do not skip not visible layers for render layer --- pype/plugins/tvpaint/publish/collect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index 57602d96103..f7e8b96c0ba 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -103,7 +103,7 @@ def create_render_layer_instance(self, context, instance_data): group_id = instance_data["group_id"] group_layers = [] for layer in layers_data: - if layer["group_id"] == group_id and layer["visible"]: + if layer["group_id"] == group_id: group_layers.append(layer) if not group_layers: From d7992f165bb45a01a0c31f4374baeb78798c4e81 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:57:49 +0100 Subject: [PATCH 39/40] turn of publishing based on visibility of layers --- pype/plugins/tvpaint/publish/collect_instances.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index f7e8b96c0ba..e03833b96b0 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -73,6 +73,14 @@ def process(self, context): if instance is None: continue + any_visible = False + for layer in instance.data["layers"]: + if layer["visible"]: + any_visible = True + break + + instance.data["publish"] = any_visible + instance.data["frameStart"] = context.data["frameStart"] instance.data["frameEnd"] = context.data["frameEnd"] From 50a59f227411ad7d7c82243d5b142c72c03121af Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 20:43:29 +0100 Subject: [PATCH 40/40] extraction is a little bit faster --- pype/hosts/tvpaint/lib.py | 15 +- .../tvpaint/publish/extract_sequence.py | 203 ++++++++---------- 2 files changed, 104 insertions(+), 114 deletions(-) diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py index 8172392c7f2..4267129fe6b 100644 --- a/pype/hosts/tvpaint/lib.py +++ b/pype/hosts/tvpaint/lib.py @@ -1,9 +1,15 @@ from PIL import Image -def composite_images( - input_image_paths, output_filepath, scene_width, scene_height -): +def composite_images(input_image_paths, output_filepath): + """Composite images in order from passed list. + + Raises: + ValueError: When entered list is empty. + """ + if not input_image_paths: + raise ValueError("Nothing to composite.") + img_obj = None for image_filepath in input_image_paths: _img_obj = Image.open(image_filepath) @@ -11,7 +17,4 @@ def composite_images( img_obj = _img_obj else: img_obj.alpha_composite(_img_obj) - - if img_obj is None: - img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) img_obj.save(output_filepath) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 8f302cb746b..cec3e2edbca 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -47,8 +47,6 @@ def process(self, instance): family_lowered = instance.data["family"].lower() frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] - scene_width = instance.context.data["sceneWidth"] - scene_height = instance.context.data["sceneHeight"] filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -73,8 +71,8 @@ def process(self, instance): else: # Render output repre_files, thumbnail_fullpath = self.render( - filename_template, output_dir, filtered_layers, - frame_start, frame_end, scene_width, scene_height + filename_template, output_dir, frame_start, frame_end, + filtered_layers ) # Fill tags and new families @@ -138,19 +136,20 @@ def _get_filename_template(self, frame_end): def render_review( self, filename_template, output_dir, frame_start, frame_end ): - """ Export images from TVPaint. + """ Export images from TVPaint using `tv_savesequence` command. Args: filename_template (str): Filename template of an output. Template - should already contain extension. Template must contain - keyword argument `{frame}`. Extension in template must match - `save_mode`. - output_dir (list): List of layers to be exported. - frame_start (int): Starting frame from which export will begin. - frame_end (int): On which frame export will end. + should already contain extension. Template may contain only + keyword argument `{frame}` or index argument (for same value). + Extension in template must match `save_mode`. + output_dir (str): Directory where files will be stored. + first_frame (int): Starting frame from which export will begin. + last_frame (int): On which frame export will end. Retruns: - dict: Mapping frame to output filepath. + tuple: With 2 items first is list of filenames second is path to + thumbnail. """ self.log.debug("Preparing data for rendering.") first_frame_filepath = os.path.join( @@ -188,24 +187,23 @@ def render_review( return output, thumbnail_filepath def render( - self, filename_template, output_dir, layers, - frame_start, frame_end, scene_width, scene_height + self, filename_template, output_dir, frame_start, frame_end, layers ): """ Export images from TVPaint. Args: - save_mode (str): Argument for `tv_savemode` george script function. - More about save mode in documentation. filename_template (str): Filename template of an output. Template should already contain extension. Template may contain only keyword argument `{frame}` or index argument (for same value). Extension in template must match `save_mode`. - layers (list): List of layers to be exported. + output_dir (str): Directory where files will be stored. first_frame (int): Starting frame from which export will begin. last_frame (int): On which frame export will end. + layers (list): List of layers to be exported. Retruns: - dict: Mapping frame to output filepath. + tuple: With 2 items first is list of filenames second is path to + thumbnail. """ self.log.debug("Preparing data for rendering.") @@ -232,40 +230,28 @@ def render( tmp_filename_template = "pos_{pos}." + filename_template files_by_position = {} - is_single_layer = len(sorted_positions) == 1 for position in sorted_positions: layer = layers_by_position[position] behavior = behavior_by_layer_id[layer["layer_id"]] - if is_single_layer: - _template = filename_template - else: - _template = tmp_filename_template - files_by_frames = self._render_layer( layer, - _template, + tmp_filename_template, output_dir, behavior, mark_in_index, mark_out_index ) - if is_single_layer: - output_filepaths = list(files_by_frames.values()) - else: - files_by_position[position] = files_by_frames + files_by_position[position] = files_by_frames - if not is_single_layer: - output_filepaths = self._composite_files( - files_by_position, - output_dir, - mark_in_index, - mark_out_index, - filename_template, - scene_width, - scene_height - ) - self._cleanup_tmp_files(files_by_position) + output_filepaths = self._composite_files( + files_by_position, + mark_in_index, + mark_out_index, + filename_template, + output_dir + ) + self._cleanup_tmp_files(files_by_position) thumbnail_src_filepath = None thumbnail_filepath = None @@ -300,9 +286,7 @@ def _render_layer( exposure_frames = lib.get_exposure_frames( layer_id, frame_start_index, frame_end_index ) - self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( - str(exposure_frames), layer["layer_id"], layer["name"] - )) + if frame_start_index not in exposure_frames: exposure_frames.append(frame_start_index) @@ -325,8 +309,8 @@ def _render_layer( # Store image to output george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) - self.log.debug("Rendering exposure frames {} of layer {}".format( - str(exposure_frames), layer_id + self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( + str(exposure_frames), layer_id, layer["name"] )) # Let TVPaint render layer's image lib.execute_george_through_file("\n".join(george_script_lines)) @@ -404,31 +388,8 @@ def _fill_frame_by_pre_behavior( return if pre_behavior == "none": - self.log.debug("Creating empty images for range {} - {}".format( - mark_in_index, frame_start_index - )) - # Take size from first image and fill it with transparent color - first_filename = filename_template.format( - pos=layer_position, - frame=frame_start_index - ) - first_filepath = os.path.join(output_dir, first_filename) - empty_image_filepath = None - for frame_idx in reversed(range(mark_in_index, frame_start_index)): - filename = filename_template.format( - pos=layer_position, - frame=frame_idx - ) - new_filepath = os.path.join(output_dir, filename) - if empty_image_filepath is None: - img_obj = Image.open(first_filepath) - painter = ImageDraw.Draw(img_obj) - painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(new_filepath) - empty_image_filepath = new_filepath - else: - self._copy_image(empty_image_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath + # Empty frames are handled during `_composite_files` + pass elif pre_behavior == "hold": # Keep first frame for whole time @@ -499,31 +460,8 @@ def _fill_frame_by_post_behavior( return if post_behavior == "none": - self.log.debug("Creating empty images for range {} - {}".format( - frame_end_index + 1, mark_out_index + 1 - )) - # Take size from last image and fill it with transparent color - last_filename = filename_template.format( - pos=layer_position, - frame=frame_end_index - ) - last_filepath = os.path.join(output_dir, last_filename) - empty_image_filepath = None - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - filename = filename_template.format( - pos=layer_position, - frame=frame_idx - ) - new_filepath = os.path.join(output_dir, filename) - if empty_image_filepath is None: - img_obj = Image.open(last_filepath) - painter = ImageDraw.Draw(img_obj) - painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(new_filepath) - empty_image_filepath = new_filepath - else: - self._copy_image(empty_image_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath + # Empty frames are handled during `_composite_files` + pass elif post_behavior == "hold": # Keep first frame for whole time @@ -571,9 +509,16 @@ def _fill_frame_by_post_behavior( layer_files_by_frame[frame_idx] = new_filepath def _composite_files( - self, files_by_position, output_dir, frame_start, frame_end, - filename_template, scene_width, scene_height + self, files_by_position, frame_start, frame_end, + filename_template, output_dir ): + """Composite frames when more that one layer was exported. + + This method is used when more than one layer is rendered out so and + output should be composition of each frame of rendered layers. + Missing frames are filled with transparent images. + """ + self.log.debug("Preparing files for compisiting.") # Prepare paths to images by frames into list where are stored # in order of compositing. images_by_frame = {} @@ -582,7 +527,8 @@ def _composite_files( for position in sorted(files_by_position.keys(), reverse=True): position_data = files_by_position[position] if frame_idx in position_data: - images_by_frame[frame_idx].append(position_data[frame_idx]) + filepath = position_data[frame_idx] + images_by_frame[frame_idx].append(filepath) process_count = os.cpu_count() if process_count > 1: @@ -590,22 +536,41 @@ def _composite_files( processes = {} output_filepaths = [] + missing_frame_paths = [] + random_frame_path = None for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] - frame = frame_idx + 1 - - output_filename = filename_template.format(frame=frame) + output_filename = filename_template.format(frame=frame_idx + 1) output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) - processes[frame_idx] = multiprocessing.Process( - target=composite_images, - args=( - image_filepaths, output_filepath, scene_width, scene_height + # Store information about missing frame and skip + if not image_filepaths: + missing_frame_paths.append(output_filepath) + continue + + # Just rename the file if is no need of compositing + if len(image_filepaths) == 1: + os.rename(image_filepaths[0], output_filepath) + + # Prepare process for compositing of images + else: + processes[frame_idx] = multiprocessing.Process( + target=composite_images, + args=(image_filepaths, output_filepath) ) - ) - # Wait until all processes are done + # Store path of random output image that will 100% exist after all + # multiprocessing as mockup for missing frames + if random_frame_path is None: + random_frame_path = output_filepath + + self.log.info( + "Running {} compositing processes - this mey take a while.".format( + len(processes) + ) + ) + # Wait until all compositing processes are done running_processes = {} while True: for idx in tuple(running_processes.keys()): @@ -627,14 +592,36 @@ def _composite_files( time.sleep(0.01) + self.log.debug( + "Creating transparent images for frames without render {}.".format( + str(missing_frame_paths) + ) + ) + # Fill the sequence with transparent frames + transparent_filepath = None + for filepath in missing_frame_paths: + if transparent_filepath is None: + img_obj = Image.open(random_frame_path) + painter = ImageDraw.Draw(img_obj) + painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) + img_obj.save(filepath) + transparent_filepath = filepath + else: + self._copy_image(transparent_filepath, filepath) return output_filepaths def _cleanup_tmp_files(self, files_by_position): + """Remove temporary files that were used for compositing.""" for data in files_by_position.values(): for filepath in data.values(): - os.remove(filepath) + if os.path.exists(filepath): + os.remove(filepath) def _copy_image(self, src_path, dst_path): + """Create a copy of an image. + + This was added to be able easier change copy method. + """ # Create hardlink of image instead of copying if possible if hasattr(os, "link"): os.link(src_path, dst_path)