Merge pull request #126 from filipstrand/img2img-special-case

Img2img special case fix
This commit is contained in:
Filip Strand 2025-02-14 10:41:02 +01:00 committed by GitHub
commit dca6c0acea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 92 additions and 38 deletions

View File

@ -12,6 +12,8 @@ class BeforeLoopCallback(Protocol):
self, self,
seed: int, seed: int,
prompt: str, prompt: str,
latents: mx.array,
config: RuntimeConfig,
canny_image: PIL.Image.Image | None = None, canny_image: PIL.Image.Image | None = None,
) -> None: # fmt: off ) -> None: # fmt: off
... ...
@ -20,9 +22,9 @@ class BeforeLoopCallback(Protocol):
class InLoopCallback(Protocol): class InLoopCallback(Protocol):
def call_in_loop( def call_in_loop(
self, self,
t: int,
seed: int, seed: int,
prompt: str, prompt: str,
step: int,
latents: mx.array, latents: mx.array,
config: RuntimeConfig, config: RuntimeConfig,
time_steps: tqdm time_steps: tqdm
@ -33,9 +35,9 @@ class InLoopCallback(Protocol):
class InterruptCallback(Protocol): class InterruptCallback(Protocol):
def call_interrupt( def call_interrupt(
self, self,
t: int,
seed: int, seed: int,
prompt: str, prompt: str,
step: int,
latents: mx.array, latents: mx.array,
config: RuntimeConfig, config: RuntimeConfig,
time_steps: tqdm time_steps: tqdm

View File

@ -11,29 +11,33 @@ class Callbacks:
def before_loop( def before_loop(
seed: int, seed: int,
prompt: str, prompt: str,
latents: mx.array,
config: RuntimeConfig,
canny_image: PIL.Image.Image | None = None, canny_image: PIL.Image.Image | None = None,
): # fmt: off ): # fmt: off
for subscriber in CallbackRegistry.before_loop_callbacks(): for subscriber in CallbackRegistry.before_loop_callbacks():
subscriber.call_before_loop( subscriber.call_before_loop(
seed=seed, seed=seed,
prompt=prompt, prompt=prompt,
latents=latents,
config=config,
canny_image=canny_image, canny_image=canny_image,
) )
@staticmethod @staticmethod
def in_loop( def in_loop(
t: int,
seed: int, seed: int,
prompt: str, prompt: str,
step: int,
latents: mx.array, latents: mx.array,
config: RuntimeConfig, config: RuntimeConfig,
time_steps: tqdm time_steps: tqdm
): # fmt: off ): # fmt: off
for subscriber in CallbackRegistry.in_loop_callbacks(): for subscriber in CallbackRegistry.in_loop_callbacks():
subscriber.call_in_loop( subscriber.call_in_loop(
t=t,
seed=seed, seed=seed,
prompt=prompt, prompt=prompt,
step=step,
latents=latents, latents=latents,
config=config, config=config,
time_steps=time_steps, time_steps=time_steps,
@ -41,18 +45,18 @@ class Callbacks:
@staticmethod @staticmethod
def interruption( def interruption(
t: int,
seed: int, seed: int,
prompt: str, prompt: str,
step: int,
latents: mx.array, latents: mx.array,
config: RuntimeConfig, config: RuntimeConfig,
time_steps: tqdm time_steps: tqdm
): # fmt: off ): # fmt: off
for subscriber in CallbackRegistry.interrupt_callbacks(): for subscriber in CallbackRegistry.interrupt_callbacks():
subscriber.call_interrupt( subscriber.call_interrupt(
t=t,
seed=seed, seed=seed,
prompt=prompt, prompt=prompt,
step=step,
latents=latents, latents=latents,
config=config, config=config,
time_steps=time_steps, time_steps=time_steps,

View File

@ -1,10 +1,12 @@
import os import os
from pathlib import Path from pathlib import Path
import mlx.core as mx
import PIL.Image import PIL.Image
from mflux import ImageUtil from mflux import ImageUtil
from mflux.callbacks.callback import BeforeLoopCallback from mflux.callbacks.callback import BeforeLoopCallback
from mflux.config.runtime_config import RuntimeConfig
class CannyImageSaver(BeforeLoopCallback): class CannyImageSaver(BeforeLoopCallback):
@ -15,8 +17,10 @@ class CannyImageSaver(BeforeLoopCallback):
self, self,
seed: int, seed: int,
prompt: str, prompt: str,
latents: mx.array,
config: RuntimeConfig,
canny_image: PIL.Image.Image | None = None, canny_image: PIL.Image.Image | None = None,
) -> None: # fmt: off ) -> None:
base, ext = os.path.splitext(self.path) base, ext = os.path.splitext(self.path)
ImageUtil.save_image( ImageUtil.save_image(
image=canny_image, image=canny_image,

View File

@ -1,16 +1,17 @@
from pathlib import Path from pathlib import Path
import mlx.core as mx import mlx.core as mx
import PIL.Image
import tqdm import tqdm
from mflux import StopImageGenerationException from mflux import StopImageGenerationException
from mflux.callbacks.callback import InLoopCallback, InterruptCallback from mflux.callbacks.callback import BeforeLoopCallback, InLoopCallback, InterruptCallback
from mflux.config.runtime_config import RuntimeConfig from mflux.config.runtime_config import RuntimeConfig
from mflux.post_processing.array_util import ArrayUtil from mflux.post_processing.array_util import ArrayUtil
from mflux.post_processing.image_util import ImageUtil from mflux.post_processing.image_util import ImageUtil
class StepwiseHandler(InLoopCallback, InterruptCallback): class StepwiseHandler(BeforeLoopCallback, InLoopCallback, InterruptCallback):
def __init__( def __init__(
self, self,
flux, flux,
@ -23,17 +24,65 @@ class StepwiseHandler(InLoopCallback, InterruptCallback):
if self.output_dir: if self.output_dir:
self.output_dir.mkdir(parents=True, exist_ok=True) self.output_dir.mkdir(parents=True, exist_ok=True)
def call_in_loop( def call_before_loop(
self, self,
seed: int, seed: int,
prompt: str, prompt: str,
latents: mx.array,
config: RuntimeConfig,
canny_image: PIL.Image.Image | None = None,
) -> None: # fmt: off
self._save_image(
step=config.init_time_step,
seed=seed,
prompt=prompt,
latents=latents,
config=config,
time_steps=None,
)
def call_in_loop(
self,
t: int,
seed: int,
prompt: str,
latents: mx.array,
config: RuntimeConfig,
time_steps: tqdm
) -> None: # fmt: off
self._save_image(
step=t + 1,
seed=seed,
prompt=prompt,
latents=latents,
config=config,
time_steps=time_steps,
)
def call_interrupt(
self,
t: int,
seed: int,
prompt: str,
latents: mx.array,
config: RuntimeConfig,
time_steps: tqdm
) -> None: # fmt: off
self._save_composite(seed=seed)
raise StopImageGenerationException(f"Stopping image generation at step {t + 1}/{len(time_steps)}")
def _save_image(
self,
step: int, step: int,
seed: int,
prompt: str,
latents: mx.array, latents: mx.array,
config: RuntimeConfig, config: RuntimeConfig,
time_steps: tqdm time_steps: tqdm
) -> None: # fmt: off ) -> None: # fmt: off
unpack_latents = ArrayUtil.unpack_latents(latents=latents, height=config.height, width=config.width) unpack_latents = ArrayUtil.unpack_latents(latents=latents, height=config.height, width=config.width)
stepwise_decoded = self.flux.vae.decode(unpack_latents) stepwise_decoded = self.flux.vae.decode(unpack_latents)
generation_time = time_steps.format_dict["elapsed"] if time_steps is not None else 0
stepwise_img = ImageUtil.to_image( stepwise_img = ImageUtil.to_image(
decoded_latents=stepwise_decoded, decoded_latents=stepwise_decoded,
config=config, config=config,
@ -42,28 +91,15 @@ class StepwiseHandler(InLoopCallback, InterruptCallback):
quantization=self.flux.bits, quantization=self.flux.bits,
lora_paths=self.flux.lora_paths, lora_paths=self.flux.lora_paths,
lora_scales=self.flux.lora_scales, lora_scales=self.flux.lora_scales,
generation_time=time_steps.format_dict["elapsed"], generation_time=generation_time,
) )
self.step_wise_images.append(stepwise_img)
stepwise_img.save( stepwise_img.save(
path=self.output_dir / f"seed_{seed}_step{step}of{len(time_steps)}.png", path=self.output_dir / f"seed_{seed}_step{step}of{config.num_inference_steps}.png",
export_json_metadata=False, export_json_metadata=False,
) )
self.step_wise_images.append(stepwise_img)
self._save_composite(seed=seed) self._save_composite(seed=seed)
def call_interrupt(
self,
seed: int,
prompt: str,
step: int,
latents: mx.array,
config: RuntimeConfig,
time_steps: tqdm
) -> None: # fmt: off
self._save_composite(seed=seed)
raise StopImageGenerationException(f"Stopping image generation at step {step + 1}/{len(time_steps)}")
def _save_composite(self, seed: int) -> None: def _save_composite(self, seed: int) -> None:
if self.step_wise_images: if self.step_wise_images:
composite_img = ImageUtil.to_composite_image(self.step_wise_images) composite_img = ImageUtil.to_composite_image(self.step_wise_images)

View File

@ -53,14 +53,16 @@ class RuntimeConfig:
@property @property
def init_time_step(self) -> int: def init_time_step(self) -> int:
if self.config.init_image_path is None: is_txt2img = self.config.init_image_path is None or self.config.init_image_strength == 0.0
# For text-to-image, always begin at time step 0.
if is_txt2img:
return 0 return 0
else: else:
# For image-to-image: higher strength means we skip more steps. # 1. Clamp strength to [0, 1]
strength = max(0.0, min(1.0, self.config.init_image_strength)) strength = max(0.0, min(1.0, self.config.init_image_strength))
t = max(1, int(self.num_inference_steps * strength))
return t # 2. Return start time in [1, floor(num_steps * strength)]
return max(1, int(self.num_inference_steps * strength))
@property @property
def controlnet_strength(self) -> float | None: def controlnet_strength(self) -> float | None:

View File

@ -87,10 +87,12 @@ class Flux1Controlnet(nn.Module):
Callbacks.before_loop( Callbacks.before_loop(
seed=seed, seed=seed,
prompt=prompt, prompt=prompt,
latents=latents,
config=config,
canny_image=canny_image canny_image=canny_image
) # fmt: off ) # fmt: off
for gen_step, t in enumerate(time_steps, 1): for t in time_steps:
try: try:
# 4.t Compute controlnet samples # 4.t Compute controlnet samples
controlnet_block_samples, controlnet_single_block_samples = self.transformer_controlnet( controlnet_block_samples, controlnet_single_block_samples = self.transformer_controlnet(
@ -119,9 +121,9 @@ class Flux1Controlnet(nn.Module):
# (Optional) Call subscribes at end of loop # (Optional) Call subscribes at end of loop
Callbacks.in_loop( Callbacks.in_loop(
t=t,
seed=seed, seed=seed,
prompt=prompt, prompt=prompt,
step=gen_step,
latents=latents, latents=latents,
config=config, config=config,
time_steps=time_steps, time_steps=time_steps,
@ -132,9 +134,9 @@ class Flux1Controlnet(nn.Module):
except KeyboardInterrupt: # noqa: PERF203 except KeyboardInterrupt: # noqa: PERF203
Callbacks.interruption( Callbacks.interruption(
t=t,
seed=seed, seed=seed,
prompt=prompt, prompt=prompt,
step=gen_step,
latents=latents, latents=latents,
config=config, config=config,
time_steps=time_steps, time_steps=time_steps,

View File

@ -79,10 +79,12 @@ class Flux1(nn.Module):
# (Optional) Call subscribers for beginning of loop # (Optional) Call subscribers for beginning of loop
Callbacks.before_loop( Callbacks.before_loop(
seed=seed, seed=seed,
prompt=prompt prompt=prompt,
latents=latents,
config=config,
) # fmt: off ) # fmt: off
for gen_step, t in enumerate(time_steps, 1): for t in time_steps:
try: try:
# 3.t Predict the noise # 3.t Predict the noise
noise = self.transformer( noise = self.transformer(
@ -99,9 +101,9 @@ class Flux1(nn.Module):
# (Optional) Call subscribes at end of loop # (Optional) Call subscribes at end of loop
Callbacks.in_loop( Callbacks.in_loop(
t=t,
seed=seed, seed=seed,
prompt=prompt, prompt=prompt,
step=gen_step,
latents=latents, latents=latents,
config=config, config=config,
time_steps=time_steps, time_steps=time_steps,
@ -112,9 +114,9 @@ class Flux1(nn.Module):
except KeyboardInterrupt: # noqa: PERF203 except KeyboardInterrupt: # noqa: PERF203
Callbacks.interruption( Callbacks.interruption(
t=t,
seed=seed, seed=seed,
prompt=prompt, prompt=prompt,
step=gen_step,
latents=latents, latents=latents,
config=config, config=config,
time_steps=time_steps, time_steps=time_steps,

View File

@ -26,6 +26,7 @@ def main():
# 2. Register the optional callbacks # 2. Register the optional callbacks
if args.stepwise_image_output_dir: if args.stepwise_image_output_dir:
handler = StepwiseHandler(flux=flux, output_dir=args.stepwise_image_output_dir) handler = StepwiseHandler(flux=flux, output_dir=args.stepwise_image_output_dir)
CallbackRegistry.register_before_loop(handler)
CallbackRegistry.register_in_loop(handler) CallbackRegistry.register_in_loop(handler)
CallbackRegistry.register_interrupt(handler) CallbackRegistry.register_interrupt(handler)

View File

@ -29,6 +29,7 @@ def main():
CallbackRegistry.register_before_loop(CannyImageSaver(path=args.output)) CallbackRegistry.register_before_loop(CannyImageSaver(path=args.output))
if args.stepwise_image_output_dir: if args.stepwise_image_output_dir:
handler = StepwiseHandler(flux=flux, output_dir=args.stepwise_image_output_dir) handler = StepwiseHandler(flux=flux, output_dir=args.stepwise_image_output_dir)
CallbackRegistry.register_before_loop(handler)
CallbackRegistry.register_in_loop(handler) CallbackRegistry.register_in_loop(handler)
CallbackRegistry.register_interrupt(handler) CallbackRegistry.register_interrupt(handler)