From b6d23670d87277f4ae237c499237d4eb3b9d9903 Mon Sep 17 00:00:00 2001 From: Manuel Schmid Date: Mon, 26 Feb 2024 15:31:32 +0100 Subject: [PATCH] feat: add jpg and webp support, add exif data handling for metadata (#1863) * feature: added flag, config and ui update for image extension change #1789 * moved function to config module * moved image extension to webui via async worker. Passing as parameter to log and get_current_html_path functions per feedback * check flag before displaying image extension radio button * disabled if image log flag is passed in * fix: add missing image_extension parameter to log call * refactor: change label * feat: add webp to image_extensions supported image extemsions: see https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html * feat: use consistent file name in gradio returns and uses filepaths instead of numpy image by saving to temp dir uses double the temp dir file storage on disk as it saves to temp dir and gradio temp dir when displaying the image, but reuses logged output image * feat: delete temp images after yielding to gradio * feat: use args temp path if given * chore: code cleanup, remove redundant if statement * feat: always show image_extension element this is now possible due to image extension support in gradio via https://github.com/lllyasviel/Fooocus/pull/1932 * refactor: rename image_extension to image_file_extension * feat: use optimized jpg parameters when saving the image quality=95 optimize=True progressive=True * refactor: rename image_file_extension to output_format * feat: add exif handling * refactor: code cleanup, remove items from metadata output --------- Co-authored-by: Manuel Schmid Co-authored-by: Manuel Schmid <9307310+mashb1t@users.noreply.github.com> Co-authored-by: Manuel Schmid Co-authored by: eddyizm --- modules/async_worker.py | 7 +++-- modules/config.py | 5 +++ modules/flags.py | 2 ++ modules/meta_parser.py | 66 ++++++++++++++++++++++++++++++++++++--- modules/private_logger.py | 32 ++++++++++++------- webui.py | 20 +++++++----- 6 files changed, 104 insertions(+), 28 deletions(-) diff --git a/modules/async_worker.py b/modules/async_worker.py index 677cf4691..2c029cfbe 100644 --- a/modules/async_worker.py +++ b/modules/async_worker.py @@ -1,5 +1,4 @@ import threading -import os from modules.patch import PatchSettings, patch_settings, patch_all patch_all() @@ -142,6 +141,7 @@ def handler(async_task): performance_selection = Performance(args.pop()) aspect_ratios_selection = args.pop() image_number = args.pop() + output_format = args.pop() image_seed = args.pop() sharpness = args.pop() guidance_scale = args.pop() @@ -414,6 +414,7 @@ def handler(async_task): progressbar(async_task, 3, 'Processing prompts ...') tasks = [] + for i in range(image_number): if disable_seed_increment: task_seed = seed @@ -553,7 +554,7 @@ def handler(async_task): if direct_return: d = [('Upscale (Fast)', '2x')] - uov_input_image_path = log(uov_input_image, d) + uov_input_image_path = log(uov_input_image, d, output_format) yield_result(async_task, uov_input_image_path, do_not_show_finished_images=True) return @@ -863,7 +864,7 @@ def callback(step, x0, x, total_steps, y): d.append((f'LoRA {li + 1}', f'lora_combined_{li + 1}', f'{n} : {w}')) d.append(('Version', 'version', 'Fooocus v' + fooocus_version.version)) - img_paths.append(log(x, d, metadata_parser)) + img_paths.append(log(x, d, metadata_parser, output_format)) yield_result(async_task, img_paths, do_not_show_finished_images=len(tasks) == 1 or disable_intermediate_results) except ldm_patched.modules.model_management.InterruptProcessingException as e: diff --git a/modules/config.py b/modules/config.py index a393e24cc..6800c0042 100644 --- a/modules/config.py +++ b/modules/config.py @@ -306,6 +306,11 @@ def get_config_item_or_set_default(key, default_value, validator, disable_empty_ default_value=32, validator=lambda x: isinstance(x, int) and x >= 1 ) +default_output_format = get_config_item_or_set_default( + key='default_output_format', + default_value='png', + validator=lambda x: x in modules.flags.output_formats +) default_image_number = get_config_item_or_set_default( key='default_image_number', default_value=2, diff --git a/modules/flags.py b/modules/flags.py index 206f51218..6f12bc8f3 100644 --- a/modules/flags.py +++ b/modules/flags.py @@ -67,6 +67,8 @@ cn_ip: (0.5, 0.6), cn_ip_face: (0.9, 0.75), cn_canny: (0.5, 1.0), cn_cpds: (0.5, 1.0) } # stop, weight +output_formats = ['png', 'jpg', 'webp'] + inpaint_engine_versions = ['None', 'v1', 'v2.5', 'v2.6'] inpaint_option_default = 'Inpaint or Outpaint (default)' inpaint_option_detail = 'Improve Detail (face, hand, eyes, etc.)' diff --git a/modules/meta_parser.py b/modules/meta_parser.py index e9f1d0332..9b2dadb32 100644 --- a/modules/meta_parser.py +++ b/modules/meta_parser.py @@ -7,6 +7,7 @@ import gradio as gr from PIL import Image +import fooocus_version import modules.config import modules.sdxl_styles from modules.flags import MetadataScheme, Performance, Steps @@ -181,13 +182,43 @@ def get_lora(key: str, fallback: str | None, source_dict: dict, results: list): def get_sha256(filepath): global hash_cache - if filepath not in hash_cache: hash_cache[filepath] = calculate_sha256(filepath) return hash_cache[filepath] +def parse_meta_from_preset(preset_content): + assert isinstance(preset_content, dict) + preset_prepared = {} + items = preset_content + + for settings_key, meta_key in modules.config.possible_preset_keys.items(): + if settings_key == "default_loras": + loras = getattr(modules.config, settings_key) + if settings_key in items: + loras = items[settings_key] + for index, lora in enumerate(loras[:5]): + preset_prepared[f'lora_combined_{index + 1}'] = ' : '.join(map(str, lora)) + elif settings_key == "default_aspect_ratio": + if settings_key in items and items[settings_key] is not None: + default_aspect_ratio = items[settings_key] + width, height = default_aspect_ratio.split('*') + else: + default_aspect_ratio = getattr(modules.config, settings_key) + width, height = default_aspect_ratio.split('×') + height = height[:height.index(" ")] + preset_prepared[meta_key] = (width, height) + else: + preset_prepared[meta_key] = items[settings_key] if settings_key in items and items[ + settings_key] is not None else getattr(modules.config, settings_key) + + if settings_key == "default_styles" or settings_key == "default_aspect_ratio": + preset_prepared[meta_key] = str(preset_prepared[meta_key]) + + return preset_prepared + + class MetadataParser(ABC): def __init__(self): self.raw_prompt: str = '' @@ -213,7 +244,8 @@ def parse_json(self, metadata: dict | str) -> dict: def parse_string(self, metadata: dict) -> str: raise NotImplementedError - def set_data(self, raw_prompt, full_prompt, raw_negative_prompt, full_negative_prompt, steps, base_model_name, refiner_model_name, loras): + def set_data(self, raw_prompt, full_prompt, raw_negative_prompt, full_negative_prompt, steps, base_model_name, + refiner_model_name, loras): self.raw_prompt = raw_prompt self.full_prompt = full_prompt self.raw_negative_prompt = raw_negative_prompt @@ -492,16 +524,28 @@ def get_metadata_parser(metadata_scheme: MetadataScheme) -> MetadataParser: raise NotImplementedError -def read_info_from_image(filepath) -> tuple[str | None, dict, MetadataScheme | None]: +def read_info_from_image(filepath) -> tuple[str | None, MetadataScheme | None]: with Image.open(filepath) as image: items = (image.info or {}).copy() parameters = items.pop('parameters', None) + metadata_scheme = items.pop('fooocus_scheme', None) + exif = items.pop('exif', None) + if parameters is not None and is_json(parameters): parameters = json.loads(parameters) + elif exif is not None: + exif = image.getexif() + # 0x9286 = UserComment + parameters = exif.get(0x9286, None) + # 0x927C = MakerNote + metadata_scheme = exif.get(0x927C, None) + + if is_json(parameters): + parameters = json.loads(parameters) try: - metadata_scheme = MetadataScheme(items.pop('fooocus_scheme', None)) + metadata_scheme = MetadataScheme(metadata_scheme) except ValueError: metadata_scheme = None @@ -512,4 +556,16 @@ def read_info_from_image(filepath) -> tuple[str | None, dict, MetadataScheme | N if isinstance(parameters, str): metadata_scheme = MetadataScheme.A1111 - return parameters, items, metadata_scheme + return parameters, metadata_scheme + + +def get_exif(metadata: str | None, metadata_scheme: str): + exif = Image.Exif() + # tags see see https://github.com/python-pillow/Pillow/blob/9.2.x/src/PIL/ExifTags.py + # 0x9286 = UserComment + exif[0x9286] = metadata + # 0x0131 = Software + exif[0x0131] = 'Fooocus v' + fooocus_version.version + # 0x927C = MakerNote + exif[0x927C] = metadata_scheme + return exif \ No newline at end of file diff --git a/modules/private_logger.py b/modules/private_logger.py index 2213cbbab..8fa5f73c6 100644 --- a/modules/private_logger.py +++ b/modules/private_logger.py @@ -7,34 +7,42 @@ from PIL import Image from PIL.PngImagePlugin import PngInfo from modules.util import generate_temp_filename -from modules.meta_parser import MetadataParser -from tempfile import gettempdir +from modules.meta_parser import MetadataParser, get_exif log_cache = {} -def get_current_html_path(): +def get_current_html_path(output_format=None): + output_format = output_format if output_format else modules.config.default_output_format date_string, local_temp_filename, only_name = generate_temp_filename(folder=modules.config.path_outputs, - extension='png') + extension=output_format) html_name = os.path.join(os.path.dirname(local_temp_filename), 'log.html') return html_name -def log(img, metadata, metadata_parser: MetadataParser | None = None) -> str: +def log(img, metadata, metadata_parser: MetadataParser | None = None, output_format=None) -> str: path_outputs = args_manager.args.temp_path if args_manager.args.disable_image_log else modules.config.path_outputs - date_string, local_temp_filename, only_name = generate_temp_filename(folder=path_outputs, extension='png') + output_format = output_format if output_format else modules.config.default_output_format + date_string, local_temp_filename, only_name = generate_temp_filename(folder=path_outputs, extension=output_format) os.makedirs(os.path.dirname(local_temp_filename), exist_ok=True) parsed_parameters = metadata_parser.parse_string(metadata) if metadata_parser is not None else '' image = Image.fromarray(img) - if parsed_parameters != '': - pnginfo = PngInfo() - pnginfo.add_text('parameters', parsed_parameters) - pnginfo.add_text('fooocus_scheme', metadata_parser.get_scheme().value) + if output_format == 'png': + if parsed_parameters != '': + pnginfo = PngInfo() + pnginfo.add_text('parameters', parsed_parameters) + pnginfo.add_text('fooocus_scheme', metadata_parser.get_scheme().value) + else: + pnginfo = None + image.save(local_temp_filename, pnginfo=pnginfo) + elif output_format == 'jpg': + image.save(local_temp_filename, quality=95, optimize=True, progressive=True, exif=get_exif(parsed_parameters, metadata_parser.get_scheme().value) if metadata_parser else Image.Exif()) + elif output_format == 'webp': + image.save(local_temp_filename, quality=95, lossless=False, exif=get_exif(parsed_parameters, metadata_parser.get_scheme().value) if metadata_parser else Image.Exif()) else: - pnginfo = None - image.save(local_temp_filename, pnginfo=pnginfo) + image.save(local_temp_filename) if args_manager.args.disable_image_log: return local_temp_filename diff --git a/webui.py b/webui.py index 7020438e6..5e8853ede 100644 --- a/webui.py +++ b/webui.py @@ -224,15 +224,12 @@ def ip_advance_checked(x): metadata_import_button = gr.Button(value='Apply Metadata') def trigger_metadata_preview(filepath): - parameters, items, metadata_scheme = modules.meta_parser.read_info_from_image(filepath) + parameters, metadata_scheme = modules.meta_parser.read_info_from_image(filepath) results = {} if parameters is not None: results['parameters'] = parameters - if items: - results['items'] = items - if isinstance(metadata_scheme, flags.MetadataScheme): results['metadata_scheme'] = metadata_scheme.value @@ -263,6 +260,11 @@ def trigger_metadata_preview(filepath): value=modules.config.default_aspect_ratio, info='width × height', elem_classes='aspect_ratios') image_number = gr.Slider(label='Image Number', minimum=1, maximum=modules.config.default_max_image_number, step=1, value=modules.config.default_image_number) + + output_format = gr.Radio(label='Output Format', + choices=modules.flags.output_formats, + value=modules.config.default_output_format) + negative_prompt = gr.Textbox(label='Negative Prompt', show_label=True, placeholder="Type prompt here.", info='Describing what you do not want to see.', lines=2, elem_id='negative_prompt', @@ -292,7 +294,7 @@ def update_history_link(): if args_manager.args.disable_image_log: return gr.update(value='') - return gr.update(value=f'\U0001F4DA History Log') + return gr.update(value=f'\U0001F4DA History Log') history_link = gr.HTML() shared.gradio_root.load(update_history_link, outputs=history_link, queue=False, show_progress=False) @@ -532,7 +534,9 @@ def model_refresh_clicked(): adm_scaler_negative, refiner_switch, refiner_model, sampler_name, scheduler_name, adaptive_cfg, refiner_swap_method, negative_prompt, disable_intermediate_results ], queue=False, show_progress=False) - + + output_format.input(lambda x: gr.update(output_format=x), inputs=output_format) + advanced_checkbox.change(lambda x: gr.update(visible=x), advanced_checkbox, advanced_column, queue=False, show_progress=False) \ .then(fn=lambda: None, _js='refresh_grid_delayed', queue=False, show_progress=False) @@ -573,7 +577,7 @@ def inpaint_mode_change(mode): ctrls = [currentTask, generate_image_grid] ctrls += [ prompt, negative_prompt, style_selections, - performance_selection, aspect_ratios_selection, image_number, image_seed, sharpness, guidance_scale + performance_selection, aspect_ratios_selection, image_number, output_format, image_seed, sharpness, guidance_scale ] ctrls += [base_model, refiner_model, refiner_switch] + lora_ctrls @@ -622,7 +626,7 @@ def parse_meta(raw_prompt_txt, is_generating): load_parameter_button.click(modules.meta_parser.load_parameter_button_click, inputs=[prompt, state_is_generating], outputs=load_data_outputs, queue=False, show_progress=False) def trigger_metadata_import(filepath, state_is_generating): - parameters, items, metadata_scheme = modules.meta_parser.read_info_from_image(filepath) + parameters, metadata_scheme = modules.meta_parser.read_info_from_image(filepath) if parameters is None: print('Could not find metadata in the image!') parsed_parameters = {}