Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add seamless tiling feature #38

Merged
merged 1 commit into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions modules/debugging/debug_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import os
import platform
import shutil
from cog import Path
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont
from modules.tiling.img_utils import expand_canvas_tiling

def save_output_img(pil_img, filename, info_text="", add_debug_info=True):
filepath = Path(filename) ## NOTE this is a cog.Path!!

## make a copy of the image.
img_copy = pil_img.copy()

if info_text:
img_copy = burn_text_into_image(img_copy, info_text)

if add_debug_info:
img_copy = draw_debug_info_into_img(img_copy)

img_copy.save(filepath)

return filepath

def burn_text_into_image(pil_img, text_str, pos_x=50, pos_y=50, font_size=22, font_name='arial.ttf'):
"""
make font size resolution independent, based on image of 1024x1024
"""
width, height = pil_img.size

## lets just base it on the width of the image for now.
font_size = calc_relative(width, font_size)

draw = ImageDraw.Draw(pil_img, 'RGBA')

## replace the font_name in the case we're on linux as we have to
## pass a full path to the font file.
if platform.system()=='Linux':
font_name = '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf'
font = ImageFont.truetype(font_name, size=font_size)
## now lets make these positions relative.
text_pos_x = calc_relative(width, pos_x)
text_pos_y = calc_relative(width, pos_y)
text_position = (text_pos_x, text_pos_y)
text_color = 'rgb(255, 255, 255)'

## draw a black backdrop to make text more visible
expand_size = calc_relative(width, 10)
draw_text_backdrop(text_str, draw, font, text_position, expand=expand_size)

## now draw the text onto the image
draw.text(text_position, text_str, fill=text_color, font=font)

return pil_img

def draw_text_backdrop(text_str, draw_obj, font_obj, pos, expand=10):
text_pos_x, text_pos_y = pos
# Determine the size of the text
bbox_x1, bbox_y1, bbox_x2, bbox_y2 = font_obj.getbbox(text_str)
bbox_x1 += text_pos_x - expand
bbox_y1 += text_pos_y - expand
bbox_x2 += text_pos_x + expand
bbox_y2 += text_pos_y + expand
text_bbox = (bbox_x1, bbox_y1, bbox_x2, bbox_y2)
# Draw the semi-transparent rectangle
draw_obj.rectangle(text_bbox, fill=(0, 0, 0, 128)) # 128 out of 255 for 50% opacity


def draw_border(img, color='red', offset=1, thickness=1, darken=True):
draw = ImageDraw.Draw(img, 'RGBA')
width, height = img.size

## if darken is turned on, we draw a dark rectangle in the middle of the border.
if darken:
draw.rectangle([(offset, offset), (width - offset, height - offset)],
fill=(0, 0, 0, 128)) # 128 out of 255 for 50% opacity

## now draw the border and return the image
# Draw multiple rectangles for thicker borders
for i in range(thickness):
draw.rectangle([(offset - i, offset - i), (width - offset - i, height - offset - i)], outline=color)

return img


def calc_relative(width, abs_value):
"""
function can be used to calculate any kind of relative sizes,
pixel offsets or font sizes
uses 1024 as the guidance size, and will increase or decrease
the relative size according to the provided width of the image.
"""
scale_ratio = float(width) / 1024.0
relative_value= int(abs_value * scale_ratio)
return relative_value

def debug_tiling_image(img):
debug_img = img.copy()
border_offset = calc_relative(debug_img.width, 100)
thickness = calc_relative(debug_img.width, 5)
draw_border(debug_img, color='black', offset=border_offset, thickness=thickness)
## first lets make these images tile..
border_size = calc_relative(debug_img.width, 256)
tiling_img = expand_canvas_tiling(debug_img, darken=False)
return tiling_img

def draw_debug_info_into_img(img):
width, height = img.size
img = burn_text_into_image(img, f'res: {width}x{height}px', pos_y=100)
return img

def move_files_to_timestamped_subdirectory(source_directory):
# Get the current date and time formatted as YYYY-MM-DD_HH-MM-SS
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# Create a new directory name with this timestamp
destination_directory = os.path.join(source_directory, timestamp)

# Create the directory if it does not exist
if not os.path.exists(destination_directory):
os.makedirs(destination_directory)

# List all files in the source directory
files = [f for f in os.listdir(source_directory) if os.path.isfile(os.path.join(source_directory, f))]

# Move each file to the new directory
for file in files:
shutil.move(os.path.join(source_directory, file), os.path.join(destination_directory, file))

2 changes: 2 additions & 0 deletions modules/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,8 @@ def init(self, all_prompts, all_seeds, all_subseeds):
if image_mask is not None:
# image_mask is passed in as RGBA by Gradio to support alpha masks,
# but we still want to support binary masks.
if isinstance(image_mask, np.ndarray):
image_mask = Image.fromarray(image_mask) ## mega hack, make sure mask is a Pil image..
image_mask = create_binary_mask(image_mask)

if self.inpainting_mask_invert:
Expand Down
246 changes: 246 additions & 0 deletions modules/tiling/img_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import base64
import numpy as np
from io import BytesIO
from PIL import ImageOps, ImageEnhance, Image, ImageDraw, ImageFilter

def add_border(img, border_size):
img = img.copy()
# Define the border size: (left, top, right, bottom)
border = (border_size, border_size, border_size, border_size)

# Create a new image with the border
# The expand argument in the border method adds the border on the outside of the image
bordered_img = ImageOps.expand(img, border=border, fill='black')

return bordered_img

def crop_corner(img, crop_size, corner='bottom_right', darken=False):
# Open the original image
img = img.copy()
width, height = img.size

# Initialize coordinates
left, top, right, bottom = 0, 0, 0, 0

if corner == 'bottom_right':
left = width - crop_size
top = height - crop_size
right = width
bottom = height
elif corner == 'bottom_left':
left = 0
top = height - crop_size
right = crop_size
bottom = height
elif corner == 'top_right':
left = width - crop_size
top = 0
right = width
bottom = crop_size
elif corner == 'top_left':
left = 0
top = 0
right = crop_size
bottom = crop_size
else:
raise ValueError("Invalid corner. Choose from 'bottom_right', 'bottom_left', 'top_right', or 'top_left'.")

# Crop the image
cropped_img = img.crop((left, top, right, bottom))

if darken:
cropped_img = darken_image(cropped_img, 0.8)

return cropped_img

def crop_side(img, crop_size, side, darken=False):
# Open the original image
img = img.copy()
width, height = img.size

# Initialize coordinates
left, top, right, bottom = 0, 0, width, height

if side == 'left':
right = crop_size
elif side == 'right':
left = width - crop_size
elif side == 'bottom':
top = height - crop_size
elif side == 'top':
bottom = crop_size
else:
raise ValueError("Invalid side. Choose from 'left', 'right', 'bottom', or 'top'.")

# Crop the image
cropped_img = img.crop((left, top, right, bottom))

if darken:
cropped_img = darken_image(cropped_img, 0.4)

return cropped_img

def darken_image(img, factor=0.5):
# Create a brightness enhancer
enhancer = ImageEnhance.Brightness(img)

# Apply the enhancer with the given factor (0.5 to darken the image by 50%)
darkened_img = enhancer.enhance(factor)

return darkened_img

def calculate_border_size(img, border_pct=5, mode='scaleup'):
"""
** function assumes square image **
mode can be either 'scaleup' or 'cropback'
- scale up calculates border size based on percentage of current image
- crop back calculates the border size based on a pre-expanded image,
thus will give us the border size we want to crop back from.
"""
if isinstance(img, bytes):
img = convert_binary_img_to_pil(img)

width, height = img.size
border_size = 0 ## init to 0
if mode=='scaleup':
border_size = int(width * (border_pct / 100.0))
elif mode=='cropback':
border_size = int(width * (border_pct / (100.0 + border_pct + border_pct)))

return border_size

def expand_canvas_tiling(img, div=8, darken=True):
width, height = img.size
org_size = width
border_size = int(org_size / div)

## first add a black border around the image to expand the canvas
expanded_img = add_border(img, border_size)

## now crop the bottom right, which will become the top left
tl_img = crop_corner(img, border_size, corner='bottom_right', darken=darken)
tl_x = 0
tl_y = 0
## and the top left which will be place on the bottom right
br_img = crop_corner(img, border_size, corner='top_left', darken=darken)
br_x = org_size + border_size
br_y = org_size + border_size
## and the bottom left which will be placed to the top right
tr_img = crop_corner(img, border_size, corner='bottom_left', darken=darken)
tr_x = org_size + border_size
tr_y = 0
## and the top right which will be placed to the bottom left
bl_img = crop_corner(img, border_size, corner='top_right', darken=darken)
bl_x = 0
bl_y = org_size + border_size

## now crop the sides.
left_img = crop_side(img, border_size, 'left', darken=darken)
left_x = org_size + border_size
left_y = border_size
## and the right
right_img = crop_side(img, border_size, 'right', darken=darken)
right_x = 0
right_y = border_size
## and the top
top_img = crop_side(img, border_size, 'top', darken=darken)
top_x = border_size
top_y = org_size + border_size
## and the bottom
bottom_img = crop_side(img, border_size, 'bottom', darken=darken)
bottom_x = border_size
bottom_y = 0

expanded_img.paste(tl_img, (tl_x, tl_y))
expanded_img.paste(br_img, (br_x, br_y))
expanded_img.paste(tr_img, (tr_x, tr_y))
expanded_img.paste(bl_img, (bl_x, bl_y))

expanded_img.paste(left_img, (left_x, left_y))
expanded_img.paste(right_img, (right_x, right_y))
expanded_img.paste(top_img, (top_x, top_y))
expanded_img.paste(bottom_img, (bottom_x, bottom_y))

return expanded_img

def convert_binary_img_to_pil(binary_img):
pil_image = Image.open(BytesIO(binary_img))
return pil_image

def convert_pil_img_to_binary(pil_img):
imgbuffer = BytesIO()
pil_img.save(imgbuffer, format='PNG')
binary_img = imgbuffer.getvalue()
return binary_img

def convert_pil_img_to_base64_depr(pil_img):
imgbuffer = BytesIO()
pil_img.save(imgbuffer, format='PNG')
# Encode the bytes buffer to base64
img_base64 = base64.b64encode(imgbuffer.getvalue()).decode('utf-8')
return img_base64

def convert_pil_img_to_base64(pil_img, force_rgb=True):
if pil_img.mode == 'RGBA':
pil_img = pil_img.convert('RGB')
buffered = BytesIO()
pil_img.save(buffered, format="PNG")
base64_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
return base64_str

def shift_image(img, x, y):
"""shifts the pixels, wrapping around itself"""
np_img = np.array(img) ### convert to a numpy array, as we receive a PIL image.
np_img = np.roll(np_img, shift=x, axis=1)
np_img = np.roll(np_img, shift=y, axis=0)
return_img = Image.fromarray(np_img) ## convert numpy array back to PIL image.
return return_img

def draw_center_cross_image(img=None, thickness_mult=1.0, blur_mult=1.0, offset_x=0, offset_y=0,
x_start=0, x_end=0, y_start=0, y_end=0, boost=True):
if img:
width, height = img.size
img = Image.new('RGB', (width, height), (0, 0, 0))
else:
img = Image.new('RGB', (1280, 720), (0, 0, 0))

draw = ImageDraw.Draw(img, 'RGB')
width, height = img.size

## now calculate the line thickness based on the resolution of the image
thickness = int((width / 25) * thickness_mult)
## and calculate the blur_radius based on the thickness of the line.
blur_radius = thickness * 0.4 * blur_mult
print(f'blur radius is {blur_radius}')

## now prepare some other values we will use
half_thickness = int(thickness / 2.0)
half_width = int(width / 2.0)
half_height = int(height / 2.0)
offset = thickness / 2 ## the offset of the line from the edge of the image.

## first lets draw it vertically.
for i in range(thickness):
x1 = half_width - half_thickness + i + offset_x
y1 = y_start or (0 + offset)
x2 = half_width - half_thickness + i + offset_x
y2 = y_end or (height - offset)
draw.rectangle([(x1, y1), (x2, y2)], outline='white')

## and now lets draw the horizontal cross bar.
for i in range(thickness):
x1 = x_start or (0 + offset)
y1 = half_height - half_thickness + i + offset_y
x2 = x_end or (width - offset)
y2 = half_height - half_thickness + i + offset_y
draw.rectangle([(x1, y1), (x2, y2)], outline='white')

img_blur = img.filter(ImageFilter.GaussianBlur(radius=blur_radius))
img_blur.show()

if boost:
## finally lets lift up the whites a little that might have been affected by the blur.
lifted_img = ImageOps.autocontrast(img_blur, cutoff=2)
return lifted_img

return img_blur
Loading