diff --git a/Documentation/02_Developer_Guide/05_Processing_Pipeline.md b/Documentation/02_Developer_Guide/05_Processing_Pipeline.md index f894636..8372415 100644 --- a/Documentation/02_Developer_Guide/05_Processing_Pipeline.md +++ b/Documentation/02_Developer_Guide/05_Processing_Pipeline.md @@ -33,7 +33,11 @@ The pipeline steps are: 6. **Map Processing (`_process_maps`)**: * Iterates through files classified as maps in the `SourceRule`. * Loads images (`cv2.imread`). - * Handles Glossiness-to-Roughness inversion. + * **Glossiness-to-Roughness Inversion**: + * The system identifies a map as a gloss map if its input filename contains "MAP_GLOSS" (case-insensitive). + * If such a map is intended to become a roughness map (e.g., its `item_type` or `item_type_override` in the `SourceRule` effectively designates it as roughness), its colors are inverted. + * After inversion, the map is treated as a "MAP_ROUGH" type for subsequent processing steps. + * This filename-driven approach is the primary mechanism for triggering gloss-to-roughness inversion, replacing reliance on older contextual flags (like `file_rule.is_gloss_source`) or general `gloss_map_identifiers` from the configuration for this specific transformation within the processing engine. * Resizes images based on `Configuration`. * Determines output bit depth and format based on `Configuration` and `SourceRule`. * Converts data types and saves images (`cv2.imwrite`). diff --git a/processing_engine.py b/processing_engine.py index 28db4da..6ae4b23 100644 --- a/processing_engine.py +++ b/processing_engine.py @@ -520,7 +520,7 @@ class ProcessingEngine: def _load_and_transform_source(self, source_path_abs: Path, map_type: str, target_resolution_key: str, is_gloss_source: bool) -> Tuple[Optional[np.ndarray], Optional[np.dtype]]: """ - Loads a source image file, performs initial prep (BGR->RGB, Gloss->Rough), + Loads a source image file, performs initial prep (BGR->RGB, Gloss->Rough if applicable), resizes it to the target resolution, and caches the result. Uses static configuration from self.config_obj. @@ -528,7 +528,7 @@ class ProcessingEngine: source_path_abs: Absolute path to the source file in the workspace. map_type: The item_type_override (e.g., "MAP_NRM", "MAP_ROUGH-1"). target_resolution_key: The key for the target resolution (e.g., "4K"). - is_gloss_source: Boolean indicating if this source should be treated as gloss for inversion. + is_gloss_source: Boolean indicating if this source should be treated as gloss for inversion (if map_type is ROUGH). Returns: Tuple containing: @@ -608,11 +608,10 @@ class ProcessingEngine: if img_prepared is None: raise ProcessingEngineError("Image data is None after MASK/Color prep.") - # Gloss -> Roughness Inversion - # map_type is item_type_override, e.g. "MAP_ROUGH-1" - # standard_type_for_checks is "ROUGH" + # Gloss -> Roughness Inversion (if map_type is ROUGH and is_gloss_source is True) + # This is triggered by the new filename logic in _process_individual_maps if standard_type_for_checks == 'ROUGH' and is_gloss_source: - log.info(f"Performing Gloss->Roughness inversion for {source_path_abs.name} (map_type: {map_type})") + log.info(f"Performing filename-triggered Gloss->Roughness inversion for {source_path_abs.name} (map_type: {map_type})") if len(img_prepared.shape) == 3: log.debug("Gloss Inversion: Converting 3-channel image to grayscale before inversion.") img_prepared = cv2.cvtColor(img_prepared, cv2.COLOR_RGB2GRAY) # Should be RGB at this point if 3-channel @@ -666,7 +665,9 @@ class ProcessingEngine: # --- 4. Cache and Return --- # Keep resized dtype unless it was gloss-inverted (which is float32) final_data_to_cache = img_resized - if map_type.startswith('ROUGH') and is_gloss_source and final_data_to_cache.dtype != np.float32: + # Ensure gloss-inverted maps are float32 + if standard_type_for_checks == 'ROUGH' and is_gloss_source and final_data_to_cache.dtype != np.float32: + log.debug(f"Ensuring gloss-inverted ROUGH map ({map_type}) is float32.") final_data_to_cache = final_data_to_cache.astype(np.float32) log.debug(f"CACHING result for {cache_key}. Shape: {final_data_to_cache.shape}, Dtype: {final_data_to_cache.dtype}") @@ -994,7 +995,7 @@ class ProcessingEngine: source_path_abs, first_map_rule_for_aspect.item_type_override, first_res_key, - is_gloss_source=False, # Added: Not relevant for dimension check, but required by method + is_gloss_source=False # Not relevant for dimension check # self.loaded_data_cache is used internally by the method ) if temp_img_for_dims is not None: @@ -1033,15 +1034,49 @@ class ProcessingEngine: # as individual maps should have been copied there by the caller (ProcessingTask) # Correction: _process_individual_maps receives the *engine's* temp_dir as workspace_path source_path_abs = workspace_path / source_path_rel - map_type = file_rule.item_type_override # Use the explicit map type from the rule - # Determine if the source is gloss based on the flag set during prediction - # is_gloss_source = map_type in gloss_identifiers # <<< INCORRECT: Re-calculates based on target type - is_gloss_source = getattr(file_rule, 'is_gloss_source', False) # <<< CORRECT: Use flag from FileRule object - log.debug(f"Using is_gloss_source={is_gloss_source} directly from FileRule for {file_rule.file_path}") # DEBUG ADDED + # Store original rule-based type and gloss flag + original_item_type_override = file_rule.item_type_override + # original_is_gloss_source_context removed as it's part of deprecated logic + + # --- New gloss map filename logic --- + filename_str = source_path_rel.name + is_filename_gloss_map = "map_gloss" in filename_str.lower() + + effective_map_type_for_processing = original_item_type_override + effective_is_gloss_source_for_load = False # Default to False, new filename logic will set to True if applicable + map_was_retagged_from_filename_gloss = False + + if is_filename_gloss_map: + log.info(f"-- Asset '{asset_name}': Filename '{filename_str}' contains 'MAP_GLOSS'. Applying new gloss handling. Original type from rule: '{original_item_type_override}'.") + effective_is_gloss_source_for_load = True # Force inversion if type becomes ROUGH (handled by filename logic below) + map_was_retagged_from_filename_gloss = True + + # Attempt to retag original_item_type_override from GLOSS to ROUGH, preserving MAP_ prefix case and suffix + if original_item_type_override and "gloss" in original_item_type_override.lower(): + match = re.match(r"(MAP_)(GLOSS)((?:[-_]\w+)*)", original_item_type_override, re.IGNORECASE) + if match: + prefix = match.group(1) # e.g., "MAP_" + suffix = match.group(3) if match.group(3) else "" # e.g., "-variant1_detail" or "" + effective_map_type_for_processing = f"{prefix}ROUGH{suffix}" + log.debug(f"Retagged filename gloss: original FTD key '{original_item_type_override}' to '{effective_map_type_for_processing}' for processing.") + else: + log.warning(f"Filename gloss '{original_item_type_override}' matched 'gloss' but not the expected 'MAP_GLOSS' pattern for precise retagging. Defaulting to 'MAP_ROUGH'.") + effective_map_type_for_processing = "MAP_ROUGH" + else: + # If original_item_type_override was None or didn't contain "gloss" (e.g., file was untyped but filename had MAP_GLOSS) + log.debug(f"Filename '{filename_str}' identified as gloss, but original type override ('{original_item_type_override}') was not GLOSS-specific. Setting type to 'MAP_ROUGH' for processing.") + effective_map_type_for_processing = "MAP_ROUGH" + # --- End of new gloss map filename logic --- + original_extension = source_path_rel.suffix.lower() # Get from path - log.info(f"-- Asset '{asset_name}': Processing Individual Map: {map_type} (Source: {source_path_rel.name}, IsGlossSource: {is_gloss_source}) --") # DEBUG: Added flag to log - current_map_details = {"derived_from_gloss": is_gloss_source} + log.info(f"-- Asset '{asset_name}': Processing Individual Map: {effective_map_type_for_processing} (Source: {source_path_rel.name}, EffectiveIsGlossSourceForLoad: {effective_is_gloss_source_for_load}, OriginalRuleItemType: {original_item_type_override}) --") + + current_map_details = {} # Old "derived_from_gloss_context" removed + if map_was_retagged_from_filename_gloss: + current_map_details["derived_from_gloss_filename"] = True + current_map_details["original_item_type_override_before_gloss_filename_retag"] = original_item_type_override + current_map_details["effective_item_type_override_after_gloss_filename_retag"] = effective_map_type_for_processing source_bit_depth_found = None # Track if we've found the bit depth for this map type try: @@ -1053,29 +1088,29 @@ class ProcessingEngine: # This now only runs for files that have an item_type_override img_resized, source_dtype = self._load_and_transform_source( source_path_abs=source_path_abs, - map_type=map_type, # Pass the specific map type (e.g., ROUGH-1) + map_type=effective_map_type_for_processing, # Use effective type target_resolution_key=res_key, - is_gloss_source=is_gloss_source + is_gloss_source=effective_is_gloss_source_for_load # Pass the flag determined by filename logic # self.loaded_data_cache is used internally ) if img_resized is None: # This warning now correctly indicates a failure for a map we *intended* to process - log.warning(f"Failed to load/transform source map {source_path_rel} for {res_key}. Skipping resolution.") + log.warning(f"Failed to load/transform source map {source_path_rel} (processed as {effective_map_type_for_processing}) for {res_key}. Skipping resolution.") continue # Skip this resolution # Store source bit depth once found if source_dtype is not None and source_bit_depth_found is None: source_bit_depth_found = 16 if source_dtype == np.uint16 else (8 if source_dtype == np.uint8 else 8) # Default non-uint to 8 current_map_details["source_bit_depth"] = source_bit_depth_found - log.debug(f"Stored source bit depth for {map_type}: {source_bit_depth_found}") + log.debug(f"Stored source bit depth for {effective_map_type_for_processing}: {source_bit_depth_found}") # --- 2. Calculate Stats (if applicable) --- if res_key == stats_res_key and stats_target_dim: - log.debug(f"Calculating stats for {map_type} using {res_key} image...") + log.debug(f"Calculating stats for {effective_map_type_for_processing} using {res_key} image...") stats = _calculate_image_stats(img_resized) - if stats: image_stats_asset[map_type] = stats # Store locally first - else: log.warning(f"Stats calculation failed for {map_type} at {res_key}.") + if stats: image_stats_asset[effective_map_type_for_processing] = stats # Store locally first + else: log.warning(f"Stats calculation failed for {effective_map_type_for_processing} at {res_key}.") # --- 3. Calculate Aspect Ratio Change String (once per asset) --- if aspect_ratio_change_string_asset == "N/A" and orig_w_aspect is not None and orig_h_aspect is not None: @@ -1097,14 +1132,14 @@ class ProcessingEngine: 'involved_extensions': {original_extension} # Only self for individual maps } # Get bit depth rule solely from the static configuration using the correct method signature - bit_depth_rule = self.config_obj.get_bit_depth_rule(map_type) # Pass only map_type + bit_depth_rule = self.config_obj.get_bit_depth_rule(effective_map_type_for_processing) # Use effective type - # Determine the map_type to use for saving (use item_type_override) - save_map_type = file_rule.item_type_override - # If item_type_override is None, this file shouldn't be saved as an individual map. + # Determine the map_type to use for saving (use effective_map_type_for_processing) + save_map_type_for_filename = effective_map_type_for_processing + # If effective_map_type_for_processing is None, this file shouldn't be saved as an individual map. # This case should ideally be caught by the skip logic earlier, but adding a check here for safety. - if save_map_type is None: - log.warning(f"Skipping save for {file_rule.file_path}: item_type_override is None.") + if save_map_type_for_filename is None: + log.warning(f"Skipping save for {file_rule.file_path}: effective_map_type_for_processing is None.") continue # Skip saving this file # Get supplier name from metadata (set in process method) @@ -1114,7 +1149,7 @@ class ProcessingEngine: image_data=img_resized, supplier_name=supplier_name, asset_name=base_name, - current_map_identifier=save_map_type, # Pass the map type to be saved + current_map_identifier=save_map_type_for_filename, # Pass the effective map type to be saved resolution_key=res_key, source_info=source_info, output_bit_depth_rule=bit_depth_rule @@ -1122,20 +1157,20 @@ class ProcessingEngine: # --- 5. Store Result --- if save_result: - processed_maps_details_asset.setdefault(map_type, {})[res_key] = save_result + processed_maps_details_asset.setdefault(effective_map_type_for_processing, {})[res_key] = save_result # Update overall map detail (e.g., final format) if needed current_map_details["output_format"] = save_result.get("format") else: - log.error(f"Failed to save {map_type} at {res_key}.") - processed_maps_details_asset.setdefault(map_type, {})[f'error_{res_key}'] = "Save failed" + log.error(f"Failed to save {effective_map_type_for_processing} at {res_key}.") + processed_maps_details_asset.setdefault(effective_map_type_for_processing, {})[f'error_{res_key}'] = "Save failed" except Exception as map_proc_err: - log.error(f"Failed processing map {map_type} from {source_path_rel.name}: {map_proc_err}", exc_info=True) - processed_maps_details_asset.setdefault(map_type, {})['error'] = str(map_proc_err) + log.error(f"Failed processing map {effective_map_type_for_processing} from {source_path_rel.name}: {map_proc_err}", exc_info=True) + processed_maps_details_asset.setdefault(effective_map_type_for_processing, {})['error'] = str(map_proc_err) - # Store collected details for this map type - map_details_asset[map_type] = current_map_details + # Store collected details for this map type (using effective_map_type_for_processing as the key) + map_details_asset[effective_map_type_for_processing] = current_map_details # --- Final Metadata Updates --- # Update the passed-in current_asset_metadata dictionary directly @@ -1278,16 +1313,22 @@ class ProcessingEngine: source_path_rel_str = file_rule.file_path # Keep original string if needed source_path_rel = Path(source_path_rel_str) # Convert to Path object source_path_abs = workspace_path / source_path_rel - is_gloss = file_rule.item_type_override in getattr(self.config_obj, 'gloss_map_identifiers', []) original_ext = source_path_rel.suffix.lower() # Now works on Path object source_info_for_save['involved_extensions'].add(original_ext) - log.debug(f"Loading source '{source_path_rel}' for merge input '{map_type_needed}' at {current_res_key} (Gloss: {is_gloss})") + # Determine if this specific source for merge should be treated as gloss + # based on its filename, aligning with the new primary rule. + filename_str_for_merge_input = source_path_rel.name + is_gloss_for_merge_input = "map_gloss" in filename_str_for_merge_input.lower() + if is_gloss_for_merge_input: + log.debug(f"Merge input '{filename_str_for_merge_input}' for '{map_type_needed}' identified as gloss by filename. Will pass is_gloss_source=True.") + + log.debug(f"Loading source '{source_path_rel}' for merge input '{map_type_needed}' at {current_res_key} (is_gloss_for_merge_input: {is_gloss_for_merge_input})") img_resized, source_dtype = self._load_and_transform_source( source_path_abs=source_path_abs, map_type=file_rule.item_type_override, # Use the specific type override from rule (e.g., ROUGH-1) target_resolution_key=current_res_key, - is_gloss_source=is_gloss + is_gloss_source=is_gloss_for_merge_input # Pass determined gloss state # self.loaded_data_cache used internally )