|
|
|
|
|
import os |
|
from PIL import Image, ImageDraw, ImageFont, ImageColor |
|
import requests |
|
from io import BytesIO |
|
import textwrap |
|
import cv2 |
|
import numpy as np |
|
from dotenv import load_dotenv |
|
|
|
|
|
from utils.configuration import ( |
|
fonts, |
|
default_guidance_scale, |
|
default_control_mode, |
|
default_num_inference_steps, |
|
default_seed, |
|
default_controlnet_conditioning_scale, |
|
flux_model_url_template, |
|
control_net_url, |
|
get_headers, |
|
get_control_net_headers, |
|
valid_aspect_ratios |
|
) |
|
|
|
|
|
|
|
|
|
|
|
def load_env(dotenv_path): |
|
"""Loads environment variables from a .env file.""" |
|
load_dotenv(dotenv_path, override=True) |
|
|
|
|
|
|
|
|
|
|
|
def get_font(font_path, font_size): |
|
"""Tries to load a specified font. Falls back to default if not found.""" |
|
try: |
|
font = ImageFont.truetype(font_path, font_size) |
|
except IOError: |
|
font = ImageFont.load_default() |
|
return font |
|
|
|
def send_post_request(url, headers, data, files=None): |
|
"""A general function to send POST requests and handle responses.""" |
|
if files: |
|
response = requests.post(url, headers=headers, files=files, data=data) |
|
else: |
|
response = requests.post(url, headers=headers, json=data) |
|
|
|
if response.status_code == 200: |
|
return response |
|
else: |
|
raise RuntimeError(f"Request failed with status code: {response.status_code}, Response: {response.text}") |
|
|
|
def resize_image(image, size): |
|
"""Resizes the image to the specified size.""" |
|
return image.resize(size) |
|
|
|
def combine_images(image1, image2): |
|
"""Combines two images side by side.""" |
|
combined = Image.new("RGB", (image1.width + image2.width, max(image1.height, image2.height))) |
|
combined.paste(image1, (0, 0)) |
|
combined.paste(image2, (image1.width, 0)) |
|
return combined |
|
|
|
|
|
|
|
|
|
|
|
def generate_flux_image(model_path, api_key, prompt, steps=default_num_inference_steps, |
|
aspect_ratio="16:9", guidance_scale=default_guidance_scale, |
|
seed=default_seed, deployment=None): |
|
""" |
|
Generates an image using the FLUX model based on the provided parameters. |
|
|
|
:param model_path: Path to the FLUX model |
|
:param api_key: API key for authentication |
|
:param prompt: Text prompt to generate the image |
|
:param steps: Number of inference steps for the model |
|
:param aspect_ratio: Desired aspect ratio for the output image |
|
:param guidance_scale: How strictly the model should follow the prompt |
|
:param seed: Seed value for randomization (for reproducibility) |
|
:param deployment: Optional deployment string for specific model deployments |
|
:return: Generated image as a PIL image |
|
""" |
|
|
|
base_url = flux_model_url_template.format(model_path=model_path) |
|
|
|
|
|
if deployment: |
|
url = f"{base_url}?deployment={deployment}" |
|
else: |
|
url = base_url |
|
|
|
headers = get_headers(api_key) |
|
|
|
|
|
data = { |
|
"prompt": prompt, |
|
"aspect_ratio": aspect_ratio, |
|
"guidance_scale": guidance_scale, |
|
"num_inference_steps": steps, |
|
"seed": seed |
|
} |
|
|
|
|
|
response = requests.post(url, headers=headers, json=data) |
|
|
|
if response.status_code == 200: |
|
|
|
img = Image.open(BytesIO(response.content)) |
|
return img |
|
else: |
|
|
|
raise RuntimeError(f"Failed to generate image: {response.status_code}, {response.text}") |
|
|
|
def call_control_net_api(control_image, prompt, api_key, |
|
control_mode=0, |
|
guidance_scale=default_guidance_scale, |
|
num_inference_steps=default_num_inference_steps, |
|
seed=default_seed, |
|
controlnet_conditioning_scale=default_controlnet_conditioning_scale): |
|
""" |
|
Calls the ControlNet API, sending a control image and prompt. |
|
Generates a new image based on ControlNet, processes the control image, |
|
and handles aspect ratios. |
|
""" |
|
|
|
processed_image_bytes, processed_image = process_image(control_image) |
|
files = {'control_image': ('control_image.jpg', processed_image_bytes, 'image/jpeg')} |
|
|
|
|
|
width, height = control_image.size |
|
aspect_ratio = f"{width}:{height}" |
|
|
|
data = { |
|
'prompt': prompt, |
|
'control_mode': control_mode, |
|
'aspect_ratio': aspect_ratio, |
|
'guidance_scale': guidance_scale, |
|
'num_inference_steps': num_inference_steps, |
|
'seed': seed, |
|
'controlnet_conditioning_scale': controlnet_conditioning_scale |
|
} |
|
|
|
url = control_net_url |
|
headers = get_control_net_headers(api_key) |
|
|
|
|
|
response = send_post_request(url, headers, data, files) |
|
|
|
|
|
generated_image = Image.open(BytesIO(response.content)) |
|
return generated_image, processed_image |
|
|
|
|
|
|
|
|
|
|
|
def overlay_text_on_image(image, text, font_path, font_size, position): |
|
"""Draws text on the image at the specified position.""" |
|
draw = ImageDraw.Draw(image) |
|
font = get_font(font_path, font_size) |
|
draw.text(position, text, font=font, fill="black") |
|
return image |
|
|
|
def get_closest_aspect_ratio(width, height): |
|
""" |
|
Finds the closest valid aspect ratio for the given image dimensions. |
|
Uses the valid_aspect_ratios from configuration.py. |
|
""" |
|
aspect_ratio = width / height |
|
closest_ratio = min(valid_aspect_ratios.keys(), key=lambda x: abs((x[0] / x[1]) - aspect_ratio)) |
|
return valid_aspect_ratios[closest_ratio] |
|
|
|
|
|
def get_next_largest_aspect_ratio(width, height): |
|
""" |
|
Finds the next largest valid aspect ratio for the given image dimensions. |
|
Returns the aspect ratio as a tuple, formatted as (width, height). |
|
""" |
|
aspect_ratio = width / height |
|
larger_ratios = [(x[0] / x[1], x) for x in valid_aspect_ratios.keys() if (x[0] / x[1]) >= aspect_ratio] |
|
|
|
if larger_ratios: |
|
|
|
next_largest_ratio = min(larger_ratios, key=lambda x: x[0]) |
|
return next_largest_ratio[1] |
|
else: |
|
|
|
closest_ratio = min(valid_aspect_ratios.keys(), key=lambda x: abs((x[0] / x[1]) - aspect_ratio)) |
|
return closest_ratio |
|
|
|
|
|
|
|
def process_image(image): |
|
""" |
|
Processes an image by converting it to grayscale and detecting edges. |
|
Returns the edge-detected image. |
|
""" |
|
gray_image = image.convert('L') |
|
np_image = np.array(gray_image) |
|
edges = cv2.Canny(np_image, 100, 200) |
|
edges_rgb = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB) |
|
return Image.fromarray(edges_rgb) |
|
|
|
def draw_crop_preview(image, x, y, width, height): |
|
"""Draws a red rectangle on the image to preview a crop region.""" |
|
draw = ImageDraw.Draw(image) |
|
draw.rectangle([x, y, x + width, y + height], outline="red", width=2) |
|
return image |
|
|
|
def wrap_text(text, max_chars): |
|
"""Wraps text to a specified number of characters per line.""" |
|
return "\n".join(textwrap.fill(line, width=max_chars) for line in text.split("\n")) |
|
|
|
|
|
|
|
|
|
|
|
def add_custom_message(image, message, font_path, font_size, position_vertical, position_horizontal, max_chars, bg_color, font_color, alpha): |
|
""" |
|
Adds a custom message to the image with specified font, positioning, and background color. |
|
Supports text wrapping and transparent background behind the text. |
|
""" |
|
|
|
try: |
|
font = ImageFont.truetype(font_path, font_size) |
|
except IOError: |
|
font = ImageFont.load_default() |
|
|
|
|
|
if image.mode != "RGBA": |
|
image = image.convert("RGBA") |
|
|
|
|
|
overlay = Image.new("RGBA", image.size, (255, 255, 255, 0)) |
|
draw = ImageDraw.Draw(overlay) |
|
|
|
|
|
message = wrap_text(message, max_chars) |
|
|
|
img_width, img_height = image.size |
|
text_lines = message.split("\n") |
|
line_height = draw.textbbox((0, 0), "A", font=font)[3] |
|
total_text_height = line_height * len(text_lines) |
|
text_width = max([draw.textbbox((0, 0), line, font=font)[2] for line in text_lines]) |
|
|
|
|
|
if position_horizontal == "Left": |
|
x_pos = 10 |
|
elif position_horizontal == "Center": |
|
x_pos = (img_width - text_width) // 2 |
|
else: |
|
x_pos = img_width - text_width - 10 |
|
|
|
|
|
if position_vertical == "Top": |
|
y_pos = 10 |
|
elif position_vertical == "Center": |
|
y_pos = (img_height - total_text_height) // 2 |
|
else: |
|
y_pos = img_height - total_text_height - 10 |
|
|
|
|
|
padding = 10 |
|
bg_color_rgba = (*ImageColor.getrgb(bg_color), alpha) |
|
draw.rectangle([x_pos - padding, y_pos - padding, x_pos + text_width + padding, y_pos + total_text_height + padding], fill=bg_color_rgba) |
|
|
|
|
|
for i, line in enumerate(text_lines): |
|
draw.text((x_pos, y_pos + i * line_height), line, font=font, fill=font_color) |
|
|
|
|
|
combined = Image.alpha_composite(image, overlay) |
|
|
|
return combined.convert("RGB") |
|
|