Spaces:
Running
on
Zero
Running
on
Zero
Fix for pngs and add warnings
Browse files- .gitignore +1 -0
- app.py +54 -26
- requirements.txt +4 -2
- utils/constants.py +23 -9
- utils/image_utils.py +160 -50
.gitignore
CHANGED
@@ -164,3 +164,4 @@ cython_debug/
|
|
164 |
/src/__pycache__
|
165 |
/utils/__pycache__
|
166 |
/__pycache__
|
|
|
|
164 |
/src/__pycache__
|
165 |
/utils/__pycache__
|
166 |
/__pycache__
|
167 |
+
/temp_models
|
app.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
|
2 |
from PIL import Image, ImageDraw, ImageChops, ImageColor
|
3 |
from haishoku.haishoku import Haishoku
|
4 |
import os
|
@@ -298,6 +298,9 @@ with gr.Blocks(css_paths="style_20250128.css", title="HexaGrid Creator", theme='
|
|
298 |
</details>
|
299 |
""", elem_classes="intro")
|
300 |
with gr.Row():
|
|
|
|
|
|
|
301 |
with gr.Column(scale=2):
|
302 |
input_image = gr.Image(
|
303 |
label="Input Image",
|
@@ -305,21 +308,36 @@ with gr.Blocks(css_paths="style_20250128.css", title="HexaGrid Creator", theme='
|
|
305 |
interactive=True,
|
306 |
elem_classes="centered solid imgcontainer",
|
307 |
key="imgInput",
|
308 |
-
image_mode=
|
309 |
-
format="PNG"
|
|
|
310 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
311 |
with gr.Column():
|
312 |
with gr.Accordion("Hex Coloring and Exclusion", open = False):
|
313 |
with gr.Row():
|
314 |
with gr.Column():
|
315 |
color_picker = gr.ColorPicker(label="Pick a color to exclude",value="#505050")
|
316 |
with gr.Column():
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
with gr.Accordion("Image Filters", open = False):
|
324 |
with gr.Row():
|
325 |
with gr.Column():
|
@@ -364,7 +382,12 @@ with gr.Blocks(css_paths="style_20250128.css", title="HexaGrid Creator", theme='
|
|
364 |
|
365 |
lut_file.change(get_filename, inputs=[lut_file], outputs=[lut_filename])
|
366 |
lut_filename.change(show_lut, inputs=[lut_filename, lut_example_image], outputs=[lut_example_image])
|
367 |
-
apply_lut_button.click(
|
|
|
|
|
|
|
|
|
|
|
368 |
|
369 |
with gr.Row():
|
370 |
with gr.Accordion("Generative AI", open = False):
|
@@ -460,30 +483,29 @@ with gr.Blocks(css_paths="style_20250128.css", title="HexaGrid Creator", theme='
|
|
460 |
with gr.Row():
|
461 |
rotation = gr.Slider(-90, 180, 0.0, 0.1, label="Hexagon Rotation (degree)")
|
462 |
add_hex_text = gr.Dropdown(label="Add Text to Hexagons", choices=[None, "Row-Column Coordinates", "Sequential Numbers", "Playing Cards Sequential", "Playing Cards Alternate Red and Black", "Custom List"], value=None)
|
463 |
-
|
464 |
custom_text_list = gr.TextArea(label="Custom Text List", value=constants.cards_alternating, visible=False,)
|
465 |
custom_text_color_list = gr.TextArea(label="Custom Text Color List", value=constants.card_colors_alternating, visible=False)
|
466 |
-
|
467 |
hex_text_info = gr.Markdown("""
|
468 |
### Text Color uses the Border Color and Border Opacity, unless you use a custom list.
|
469 |
### The Custom Text List and Custom Text Color List are comma separated lists.
|
470 |
### The custom color list is a comma separated list of hex colors.
|
471 |
#### Example: "A,2,3,4,5,6,7,8,9,10,J,Q,K", "red,#0000FF,#00FF00,red,#FFFF00,#00FFFF,#FF8000,#FF00FF,#FF0080,#FF8000,#FF0080,lightblue"
|
472 |
""", elem_id="hex_text_info", visible=False)
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
with gr.Row():
|
483 |
hex_size = gr.Number(label="Hexagon Size", value=32, minimum=1, maximum=768)
|
484 |
border_size = gr.Slider(-5,25,value=0,step=1,label="Border Size")
|
485 |
-
with gr.Row():
|
486 |
-
rotation = gr.Slider(-90, 180, 0.0, 0.1, label="deg. Rotation")
|
487 |
background_color = gr.ColorPicker(label="Background Color", value="#000000", interactive=True)
|
488 |
background_opacity = gr.Slider(0,100,0,1,label="Background Opacity %")
|
489 |
border_color = gr.ColorPicker(label="Border Color", value="#7b7b7b", interactive=True)
|
@@ -519,7 +541,7 @@ with gr.Blocks(css_paths="style_20250128.css", title="HexaGrid Creator", theme='
|
|
519 |
depth_image_source = gr.Radio(label="Depth Image Source", choices=["Input Image", "Output Image", "Overlay Image","Image with Margins"], value="Input Image")
|
520 |
with gr.Row():
|
521 |
generate_depth_button = gr.Button("Generate Depth Map and 3D Model From Selected Image", elem_classes="solid", variant="secondary")
|
522 |
-
|
523 |
depth_map_output = gr.Image(label="Depth Map", image_mode="L", elem_classes="centered solid imgcontainer", format="PNG", type="filepath", key="ImgDepth")
|
524 |
model_output = gr.Model3D(label="3D Model", clear_color=[1.0, 1.0, 1.0, 0.25], key="Img3D", elem_classes="centered solid imgcontainer")
|
525 |
with gr.Row():
|
@@ -540,7 +562,13 @@ with gr.Blocks(css_paths="style_20250128.css", title="HexaGrid Creator", theme='
|
|
540 |
|
541 |
delete_button.click(fn=delete_color, inputs=[selected_row, color_display], outputs=[color_display])
|
542 |
exclude_color_button.click(fn=add_color, inputs=[color_picker, gr.State(excluded_color_list)], outputs=[color_display, gr.State(excluded_color_list)])
|
543 |
-
hex_button.click(
|
|
|
|
|
|
|
|
|
|
|
|
|
544 |
generate_input_image.click(
|
545 |
fn=generate_input_image_click,
|
546 |
inputs=[map_options, prompt_textbox, negative_prompt_textbox, model_textbox, gr.State(False), gr.State(0.5), image_size_ratio],
|
@@ -567,7 +595,7 @@ with gr.Blocks(css_paths="style_20250128.css", title="HexaGrid Creator", theme='
|
|
567 |
outputs=prompt_notes_label
|
568 |
)
|
569 |
composite_button.click(
|
570 |
-
fn=change_color,
|
571 |
inputs=[input_image, composite_color, composite_opacity],
|
572 |
outputs=[input_image]
|
573 |
)
|
|
|
1 |
+
import gradio as gr
|
2 |
from PIL import Image, ImageDraw, ImageChops, ImageColor
|
3 |
from haishoku.haishoku import Haishoku
|
4 |
import os
|
|
|
298 |
</details>
|
299 |
""", elem_classes="intro")
|
300 |
with gr.Row():
|
301 |
+
from utils.image_utils import convert_to_rgba_png
|
302 |
+
|
303 |
+
# Existing code
|
304 |
with gr.Column(scale=2):
|
305 |
input_image = gr.Image(
|
306 |
label="Input Image",
|
|
|
308 |
interactive=True,
|
309 |
elem_classes="centered solid imgcontainer",
|
310 |
key="imgInput",
|
311 |
+
image_mode=None,
|
312 |
+
format="PNG",
|
313 |
+
show_download_button=True,
|
314 |
)
|
315 |
+
|
316 |
+
# New code to convert input image to RGBA PNG
|
317 |
+
def on_input_image_change(image_path):
|
318 |
+
if image_path is None:
|
319 |
+
gr.Warning("Please upload an Input Image to get started.")
|
320 |
+
return None
|
321 |
+
img, img_path = convert_to_rgba_png(image_path)
|
322 |
+
return img_path
|
323 |
+
|
324 |
+
input_image.change(
|
325 |
+
fn=on_input_image_change,
|
326 |
+
inputs=[input_image],
|
327 |
+
outputs=[input_image], scroll_to_output=True,
|
328 |
+
)
|
329 |
with gr.Column():
|
330 |
with gr.Accordion("Hex Coloring and Exclusion", open = False):
|
331 |
with gr.Row():
|
332 |
with gr.Column():
|
333 |
color_picker = gr.ColorPicker(label="Pick a color to exclude",value="#505050")
|
334 |
with gr.Column():
|
335 |
+
filter_color = gr.Checkbox(label="Filter Excluded Colors from Sampling", value=False,)
|
336 |
+
exclude_color_button = gr.Button("Exclude Color", elem_id="exlude_color_button", elem_classes="solid")
|
337 |
+
color_display = gr.DataFrame(label="List of Excluded RGBA Colors", headers=["R", "G", "B", "A"], elem_id="excluded_colors", type="array", value=build_dataframe(excluded_color_list), interactive=True, elem_classes="solid centered")
|
338 |
+
selected_row = gr.Number(0, label="Selected Row", visible=False)
|
339 |
+
delete_button = gr.Button("Delete Row", elem_id="delete_exclusion_button", elem_classes="solid")
|
340 |
+
fill_hex = gr.Checkbox(label="Fill Hex with color from Image", value=True)
|
341 |
with gr.Accordion("Image Filters", open = False):
|
342 |
with gr.Row():
|
343 |
with gr.Column():
|
|
|
382 |
|
383 |
lut_file.change(get_filename, inputs=[lut_file], outputs=[lut_filename])
|
384 |
lut_filename.change(show_lut, inputs=[lut_filename, lut_example_image], outputs=[lut_example_image])
|
385 |
+
apply_lut_button.click(
|
386 |
+
lambda lut_filename, input_image: gr.Warning("Please upload an Input Image to get started.") if input_image is None else apply_lut_to_image_path(lut_filename, input_image)[0],
|
387 |
+
inputs=[lut_filename, input_image],
|
388 |
+
outputs=[input_image],
|
389 |
+
scroll_to_output=True
|
390 |
+
)
|
391 |
|
392 |
with gr.Row():
|
393 |
with gr.Accordion("Generative AI", open = False):
|
|
|
483 |
with gr.Row():
|
484 |
rotation = gr.Slider(-90, 180, 0.0, 0.1, label="Hexagon Rotation (degree)")
|
485 |
add_hex_text = gr.Dropdown(label="Add Text to Hexagons", choices=[None, "Row-Column Coordinates", "Sequential Numbers", "Playing Cards Sequential", "Playing Cards Alternate Red and Black", "Custom List"], value=None)
|
486 |
+
with gr.Row():
|
487 |
custom_text_list = gr.TextArea(label="Custom Text List", value=constants.cards_alternating, visible=False,)
|
488 |
custom_text_color_list = gr.TextArea(label="Custom Text Color List", value=constants.card_colors_alternating, visible=False)
|
489 |
+
with gr.Row():
|
490 |
hex_text_info = gr.Markdown("""
|
491 |
### Text Color uses the Border Color and Border Opacity, unless you use a custom list.
|
492 |
### The Custom Text List and Custom Text Color List are comma separated lists.
|
493 |
### The custom color list is a comma separated list of hex colors.
|
494 |
#### Example: "A,2,3,4,5,6,7,8,9,10,J,Q,K", "red,#0000FF,#00FF00,red,#FFFF00,#00FFFF,#FF8000,#FF00FF,#FF0080,#FF8000,#FF0080,lightblue"
|
495 |
""", elem_id="hex_text_info", visible=False)
|
496 |
+
add_hex_text.change(
|
497 |
+
fn=lambda x: (
|
498 |
+
gr.update(visible=(x == "Custom List")),
|
499 |
+
gr.update(visible=(x == "Custom List")),
|
500 |
+
gr.update(visible=(x != None))
|
501 |
+
),
|
502 |
+
inputs=add_hex_text,
|
503 |
+
outputs=[custom_text_list, custom_text_color_list, hex_text_info]
|
504 |
+
)
|
505 |
with gr.Row():
|
506 |
hex_size = gr.Number(label="Hexagon Size", value=32, minimum=1, maximum=768)
|
507 |
border_size = gr.Slider(-5,25,value=0,step=1,label="Border Size")
|
508 |
+
with gr.Row():
|
|
|
509 |
background_color = gr.ColorPicker(label="Background Color", value="#000000", interactive=True)
|
510 |
background_opacity = gr.Slider(0,100,0,1,label="Background Opacity %")
|
511 |
border_color = gr.ColorPicker(label="Border Color", value="#7b7b7b", interactive=True)
|
|
|
541 |
depth_image_source = gr.Radio(label="Depth Image Source", choices=["Input Image", "Output Image", "Overlay Image","Image with Margins"], value="Input Image")
|
542 |
with gr.Row():
|
543 |
generate_depth_button = gr.Button("Generate Depth Map and 3D Model From Selected Image", elem_classes="solid", variant="secondary")
|
544 |
+
with gr.Row():
|
545 |
depth_map_output = gr.Image(label="Depth Map", image_mode="L", elem_classes="centered solid imgcontainer", format="PNG", type="filepath", key="ImgDepth")
|
546 |
model_output = gr.Model3D(label="3D Model", clear_color=[1.0, 1.0, 1.0, 0.25], key="Img3D", elem_classes="centered solid imgcontainer")
|
547 |
with gr.Row():
|
|
|
562 |
|
563 |
delete_button.click(fn=delete_color, inputs=[selected_row, color_display], outputs=[color_display])
|
564 |
exclude_color_button.click(fn=add_color, inputs=[color_picker, gr.State(excluded_color_list)], outputs=[color_display, gr.State(excluded_color_list)])
|
565 |
+
hex_button.click(
|
566 |
+
fn=lambda hex_size, border_size, input_image, start_x, start_y, end_x, end_y, rotation, background_color, background_opacity, border_color, border_opacity, fill_hex, color_display, filter_color, x_spacing, y_spacing, add_hex_text, custom_text_list, custom_text_color_list:
|
567 |
+
gr.Warning("Please upload an Input Image to get started.") if input_image is None else hex_create(hex_size, border_size, input_image, start_x, start_y, end_x, end_y, rotation, background_color, background_opacity, border_color, border_opacity, fill_hex, color_display, filter_color, x_spacing, y_spacing, add_hex_text, custom_text_list, custom_text_color_list),
|
568 |
+
inputs=[hex_size, border_size, input_image, start_x, start_y, end_x, end_y, rotation, background_color, background_opacity, border_color, border_opacity, fill_hex, color_display, filter_color, x_spacing, y_spacing, add_hex_text, custom_text_list, custom_text_color_list],
|
569 |
+
outputs=[output_image, overlay_image],
|
570 |
+
scroll_to_output=True
|
571 |
+
)
|
572 |
generate_input_image.click(
|
573 |
fn=generate_input_image_click,
|
574 |
inputs=[map_options, prompt_textbox, negative_prompt_textbox, model_textbox, gr.State(False), gr.State(0.5), image_size_ratio],
|
|
|
595 |
outputs=prompt_notes_label
|
596 |
)
|
597 |
composite_button.click(
|
598 |
+
fn=lambda input_image, composite_color, composite_opacity: gr.Warning("Please upload an Input Image to get started.") if input_image is None else change_color(input_image, composite_color, composite_opacity),
|
599 |
inputs=[input_image, composite_color, composite_opacity],
|
600 |
outputs=[input_image]
|
601 |
)
|
requirements.txt
CHANGED
@@ -13,7 +13,7 @@ huggingface_hub
|
|
13 |
# git+https://github.com/huggingface/transformers@v4.48.1#egg=transformers
|
14 |
transformers==4.48.1
|
15 |
gradio[oauth]
|
16 |
-
Pillow
|
17 |
numpy
|
18 |
requests
|
19 |
# git+https://github.com/huggingface/diffusers
|
@@ -34,4 +34,6 @@ pycairo
|
|
34 |
cairocffi
|
35 |
pangocffi
|
36 |
pangocairocffi
|
37 |
-
tensorflow
|
|
|
|
|
|
13 |
# git+https://github.com/huggingface/transformers@v4.48.1#egg=transformers
|
14 |
transformers==4.48.1
|
15 |
gradio[oauth]
|
16 |
+
Pillow>=11.1.0
|
17 |
numpy
|
18 |
requests
|
19 |
# git+https://github.com/huggingface/diffusers
|
|
|
34 |
cairocffi
|
35 |
pangocffi
|
36 |
pangocairocffi
|
37 |
+
tensorflow
|
38 |
+
cairosvg
|
39 |
+
python-dotenv
|
utils/constants.py
CHANGED
@@ -1,22 +1,36 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
#Set the environment variables
|
3 |
-
os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
|
4 |
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:256,expandable_segments:True"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
IS_SHARED_SPACE = "Surn/HexaGrid" in os.environ.get('SPACE_ID', '')
|
6 |
|
|
|
|
|
|
|
|
|
7 |
# Set the temporary folder location
|
8 |
#os.environ['TEMP'] = r'e:\\TMP'
|
9 |
#os.environ['TMPDIR'] = r'e:\\TMP'
|
10 |
#os.environ['XDG_CACHE_HOME'] = r'E:\\cache'
|
11 |
-
os.environ['USE_FLASH_ATTENTION'] = '1'
|
12 |
-
#os.environ['XFORMERS_FORCE_DISABLE_TRITON']= '1'
|
13 |
-
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
|
14 |
-
os.environ["PYTORCH_NVML_BASED_CUDA_CHECK"] = "1"
|
15 |
-
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3"
|
16 |
|
17 |
-
# constants.py contains all the constants used in the project
|
18 |
-
#os.environ["HF_TOKEN"] = ""
|
19 |
HF_API_TOKEN = os.getenv("HF_TOKEN")
|
|
|
|
|
|
|
20 |
default_lut_example_img = "./LUT/daisy.jpg"
|
21 |
|
22 |
PROMPTS = {
|
|
|
1 |
+
# utils/constants.py
|
2 |
+
# constants.py contains all the constants used in the project such as the default LUT example image, prompts, negative prompts, pre-rendered maps, models, LoRA weights, and more.
|
3 |
+
# execptions made for some environmental variables
|
4 |
+
import os
|
5 |
+
from pathlib import Path
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
|
8 |
#Set the environment variables
|
|
|
9 |
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:256,expandable_segments:True"
|
10 |
+
os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
|
11 |
+
#os.environ["TF_CPP_MIN_LOG_LEVEL"] = '2'
|
12 |
+
os.environ['USE_FLASH_ATTENTION'] = '1'
|
13 |
+
#os.environ['XFORMERS_FORCE_DISABLE_TRITON']= '1'
|
14 |
+
#os.environ['XFORMERS_FORCE_DISABLE_TORCHSCRIPT']= '1'
|
15 |
+
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
|
16 |
+
os.environ["PYTORCH_NVML_BASED_CUDA_CHECK"] = "1"
|
17 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3"
|
18 |
+
|
19 |
IS_SHARED_SPACE = "Surn/HexaGrid" in os.environ.get('SPACE_ID', '')
|
20 |
|
21 |
+
# Load environment variables from .env file
|
22 |
+
dotenv_path = Path(__file__).parent.parent / '.env'
|
23 |
+
load_dotenv(dotenv_path)
|
24 |
+
|
25 |
# Set the temporary folder location
|
26 |
#os.environ['TEMP'] = r'e:\\TMP'
|
27 |
#os.environ['TMPDIR'] = r'e:\\TMP'
|
28 |
#os.environ['XDG_CACHE_HOME'] = r'E:\\cache'
|
|
|
|
|
|
|
|
|
|
|
29 |
|
|
|
|
|
30 |
HF_API_TOKEN = os.getenv("HF_TOKEN")
|
31 |
+
if not HF_API_TOKEN:
|
32 |
+
raise ValueError("HF_TOKEN is not set. Please check your .env file.")
|
33 |
+
|
34 |
default_lut_example_img = "./LUT/daisy.jpg"
|
35 |
|
36 |
PROMPTS = {
|
utils/image_utils.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
# utils/image_utils.py
|
2 |
import os
|
3 |
from io import BytesIO
|
|
|
4 |
import base64
|
5 |
import numpy as np
|
6 |
#from decimal import ROUND_CEILING
|
@@ -8,6 +9,7 @@ from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter, ImageDr
|
|
8 |
from typing import List, Union
|
9 |
#import numpy as np
|
10 |
#import math
|
|
|
11 |
from utils.constants import default_lut_example_img
|
12 |
from utils.color_utils import (
|
13 |
detect_color_format,
|
@@ -18,13 +20,14 @@ from utils.misc import (pause)
|
|
18 |
def open_image(image_path):
|
19 |
"""
|
20 |
Opens an image from a file path or URL, or decodes a DataURL string into an image.
|
21 |
-
|
|
|
22 |
Parameters:
|
23 |
image_path (str): The file path, URL, or DataURL string of the image to open.
|
24 |
-
|
25 |
Returns:
|
26 |
Image: A PIL Image object of the opened image.
|
27 |
-
|
28 |
Raises:
|
29 |
Exception: If there is an error opening the image.
|
30 |
"""
|
@@ -33,17 +36,32 @@ def open_image(image_path):
|
|
33 |
# Strip leading and trailing double quotation marks, if present
|
34 |
image_path = image_path.strip('"')
|
35 |
if image_path.startswith('http'):
|
36 |
-
# If the image path is a URL, download the image using requests
|
37 |
response = requests.get(image_path)
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
elif image_path.startswith('data'):
|
40 |
-
# If the image path is a DataURL, decode the base64 string
|
41 |
encoded_data = image_path.split(',')[1]
|
42 |
decoded_data = base64.b64decode(encoded_data)
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
else:
|
45 |
-
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
47 |
except Exception as e:
|
48 |
raise Exception(f'Error opening image: {e}')
|
49 |
return img
|
@@ -75,15 +93,16 @@ def build_encoded_images(images_list):
|
|
75 |
def image_to_base64(image):
|
76 |
"""
|
77 |
Encodes an image to a base64 string.
|
78 |
-
|
|
|
79 |
Parameters:
|
80 |
image (str or PIL.Image.Image): The file path, URL, DataURL string, or PIL Image object of the image to encode.
|
81 |
-
|
82 |
Returns:
|
83 |
str: A base64-encoded string of the image.
|
84 |
"""
|
85 |
buffered = BytesIO()
|
86 |
-
if
|
87 |
image = open_image(image)
|
88 |
image.save(buffered, format="PNG")
|
89 |
return "data:image/png;base64," + base64.b64encode(buffered.getvalue()).decode()
|
@@ -449,11 +468,9 @@ def read_lut(path_lut: Union[str, os.PathLike], num_channels: int = 3) -> ImageF
|
|
449 |
"""
|
450 |
with open(path_lut) as f:
|
451 |
lut_raw = f.read().splitlines()
|
452 |
-
|
453 |
size = round(len(lut_raw) ** (1 / 3))
|
454 |
row2val = lambda row: tuple([float(val) for val in row.split(" ")])
|
455 |
lut_table = [row2val(row) for row in lut_raw if is_3dlut_row(row.split(" "))]
|
456 |
-
|
457 |
return ImageFilter.Color3DLUT(size, lut_table, num_channels)
|
458 |
|
459 |
def apply_lut(img: Image, lut_path: str = "", lut: ImageFilter.Color3DLUT = None) -> Image:
|
@@ -477,7 +494,6 @@ def apply_lut(img: Image, lut_path: str = "", lut: ImageFilter.Color3DLUT = None
|
|
477 |
if lut_path == "":
|
478 |
raise ValueError("Either lut_path or lut argument must be provided.")
|
479 |
lut = read_lut(lut_path)
|
480 |
-
|
481 |
return img.filter(lut)
|
482 |
|
483 |
def show_lut(lut_filename: str, lut_example_image: Image = default_lut_example_img) -> Image:
|
@@ -490,6 +506,8 @@ def show_lut(lut_filename: str, lut_example_image: Image = default_lut_example_i
|
|
490 |
lut_example_image = open_image(default_lut_example_img)
|
491 |
return lut_example_image
|
492 |
|
|
|
|
|
493 |
def convert_rgb_to_rgba_safe(image: Image) -> Image:
|
494 |
"""
|
495 |
Converts an RGB image to RGBA by adding an alpha channel.
|
@@ -509,70 +527,162 @@ def convert_rgb_to_rgba_safe(image: Image) -> Image:
|
|
509 |
image = image.convert('RGB')
|
510 |
else:
|
511 |
raise ValueError("Unsupported image mode for conversion to RGBA.")
|
512 |
-
|
513 |
# Create a copy of the image to avoid modifying the original
|
514 |
rgba_image = image.copy()
|
515 |
-
|
516 |
# Optionally, set a default alpha value (e.g., fully opaque)
|
517 |
alpha = Image.new('L', rgba_image.size, 255) # 255 for full opacity
|
518 |
-
rgba_image.putalpha(alpha)
|
519 |
-
|
520 |
return rgba_image
|
521 |
|
522 |
-
def apply_lut_to_image_path(lut_filename: str, image_path: str) -> Image:
|
523 |
"""
|
524 |
Apply a LUT to an image and return the result.
|
525 |
-
|
|
|
526 |
Args:
|
527 |
lut_filename: A string representing the path to the LUT file.
|
528 |
image_path: A string representing the path to the input image.
|
529 |
-
|
530 |
Returns:
|
531 |
-
A PIL Image object with the LUT applied.
|
532 |
"""
|
|
|
|
|
|
|
|
|
533 |
img = open_image(image_path)
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
|
|
|
|
544 |
if lut_filename is not None:
|
545 |
try:
|
546 |
img = apply_lut(img, lut_filename)
|
547 |
except Exception as e:
|
548 |
print(f"BAD LUT: Error applying LUT {str(e)}.")
|
549 |
-
|
|
|
550 |
|
|
|
551 |
|
552 |
-
|
|
|
|
|
553 |
"""
|
554 |
-
|
555 |
|
556 |
Args:
|
557 |
-
|
558 |
|
559 |
Raises:
|
560 |
-
|
561 |
-
|
|
|
|
|
|
|
|
|
562 |
"""
|
563 |
try:
|
564 |
-
#
|
565 |
-
|
|
|
|
|
|
|
|
|
|
|
566 |
|
567 |
-
#
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
-
|
574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
575 |
except ValueError as ve:
|
576 |
print(f"ValueError: {ve}")
|
577 |
except Exception as e:
|
578 |
-
print(f"Error converting image: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# utils/image_utils.py
|
2 |
import os
|
3 |
from io import BytesIO
|
4 |
+
import cairosvg
|
5 |
import base64
|
6 |
import numpy as np
|
7 |
#from decimal import ROUND_CEILING
|
|
|
9 |
from typing import List, Union
|
10 |
#import numpy as np
|
11 |
#import math
|
12 |
+
from pathlib import Path
|
13 |
from utils.constants import default_lut_example_img
|
14 |
from utils.color_utils import (
|
15 |
detect_color_format,
|
|
|
20 |
def open_image(image_path):
|
21 |
"""
|
22 |
Opens an image from a file path or URL, or decodes a DataURL string into an image.
|
23 |
+
Supports SVG and ICO by converting them to PNG.
|
24 |
+
|
25 |
Parameters:
|
26 |
image_path (str): The file path, URL, or DataURL string of the image to open.
|
27 |
+
|
28 |
Returns:
|
29 |
Image: A PIL Image object of the opened image.
|
30 |
+
|
31 |
Raises:
|
32 |
Exception: If there is an error opening the image.
|
33 |
"""
|
|
|
36 |
# Strip leading and trailing double quotation marks, if present
|
37 |
image_path = image_path.strip('"')
|
38 |
if image_path.startswith('http'):
|
|
|
39 |
response = requests.get(image_path)
|
40 |
+
if image_path.lower().endswith('.svg'):
|
41 |
+
png_data = cairosvg.svg2png(bytestring=response.content)
|
42 |
+
img = Image.open(BytesIO(png_data))
|
43 |
+
elif image_path.lower().endswith('.ico'):
|
44 |
+
img = Image.open(BytesIO(response.content)).convert('RGBA')
|
45 |
+
else:
|
46 |
+
img = Image.open(BytesIO(response.content))
|
47 |
elif image_path.startswith('data'):
|
|
|
48 |
encoded_data = image_path.split(',')[1]
|
49 |
decoded_data = base64.b64decode(encoded_data)
|
50 |
+
if image_path.lower().endswith('.svg'):
|
51 |
+
png_data = cairosvg.svg2png(bytestring=decoded_data)
|
52 |
+
img = Image.open(BytesIO(png_data))
|
53 |
+
elif image_path.lower().endswith('.ico'):
|
54 |
+
img = Image.open(BytesIO(decoded_data)).convert('RGBA')
|
55 |
+
else:
|
56 |
+
img = Image.open(BytesIO(decoded_data))
|
57 |
else:
|
58 |
+
if image_path.lower().endswith('.svg'):
|
59 |
+
png_data = cairosvg.svg2png(url=image_path)
|
60 |
+
img = Image.open(BytesIO(png_data))
|
61 |
+
elif image_path.lower().endswith('.ico'):
|
62 |
+
img = Image.open(image_path).convert('RGBA')
|
63 |
+
else:
|
64 |
+
img = Image.open(image_path)
|
65 |
except Exception as e:
|
66 |
raise Exception(f'Error opening image: {e}')
|
67 |
return img
|
|
|
93 |
def image_to_base64(image):
|
94 |
"""
|
95 |
Encodes an image to a base64 string.
|
96 |
+
Supports ICO files by converting them to PNG with RGBA channels.
|
97 |
+
|
98 |
Parameters:
|
99 |
image (str or PIL.Image.Image): The file path, URL, DataURL string, or PIL Image object of the image to encode.
|
100 |
+
|
101 |
Returns:
|
102 |
str: A base64-encoded string of the image.
|
103 |
"""
|
104 |
buffered = BytesIO()
|
105 |
+
if isinstance(image, str):
|
106 |
image = open_image(image)
|
107 |
image.save(buffered, format="PNG")
|
108 |
return "data:image/png;base64," + base64.b64encode(buffered.getvalue()).decode()
|
|
|
468 |
"""
|
469 |
with open(path_lut) as f:
|
470 |
lut_raw = f.read().splitlines()
|
|
|
471 |
size = round(len(lut_raw) ** (1 / 3))
|
472 |
row2val = lambda row: tuple([float(val) for val in row.split(" ")])
|
473 |
lut_table = [row2val(row) for row in lut_raw if is_3dlut_row(row.split(" "))]
|
|
|
474 |
return ImageFilter.Color3DLUT(size, lut_table, num_channels)
|
475 |
|
476 |
def apply_lut(img: Image, lut_path: str = "", lut: ImageFilter.Color3DLUT = None) -> Image:
|
|
|
494 |
if lut_path == "":
|
495 |
raise ValueError("Either lut_path or lut argument must be provided.")
|
496 |
lut = read_lut(lut_path)
|
|
|
497 |
return img.filter(lut)
|
498 |
|
499 |
def show_lut(lut_filename: str, lut_example_image: Image = default_lut_example_img) -> Image:
|
|
|
506 |
lut_example_image = open_image(default_lut_example_img)
|
507 |
return lut_example_image
|
508 |
|
509 |
+
|
510 |
+
|
511 |
def convert_rgb_to_rgba_safe(image: Image) -> Image:
|
512 |
"""
|
513 |
Converts an RGB image to RGBA by adding an alpha channel.
|
|
|
527 |
image = image.convert('RGB')
|
528 |
else:
|
529 |
raise ValueError("Unsupported image mode for conversion to RGBA.")
|
|
|
530 |
# Create a copy of the image to avoid modifying the original
|
531 |
rgba_image = image.copy()
|
|
|
532 |
# Optionally, set a default alpha value (e.g., fully opaque)
|
533 |
alpha = Image.new('L', rgba_image.size, 255) # 255 for full opacity
|
534 |
+
rgba_image.putalpha(alpha)
|
|
|
535 |
return rgba_image
|
536 |
|
537 |
+
def apply_lut_to_image_path(lut_filename: str, image_path: str) -> tuple[Image, str]:
|
538 |
"""
|
539 |
Apply a LUT to an image and return the result.
|
540 |
+
Supports ICO files by converting them to PNG with RGBA channels.
|
541 |
+
|
542 |
Args:
|
543 |
lut_filename: A string representing the path to the LUT file.
|
544 |
image_path: A string representing the path to the input image.
|
545 |
+
|
546 |
Returns:
|
547 |
+
tuple: A tuple containing the PIL Image object with the LUT applied and the new image path as a string.
|
548 |
"""
|
549 |
+
if image_path is None:
|
550 |
+
raise UserWarning("No image provided.")
|
551 |
+
return None, None
|
552 |
+
path = Path(image_path)
|
553 |
img = open_image(image_path)
|
554 |
+
if not ((path.suffix.lower() == '.png' and img.mode == 'RGBA')):
|
555 |
+
if image_path.lower().endswith(('.jpg', '.jpeg')):
|
556 |
+
img, new_image_path = convert_jpg_to_rgba(path)
|
557 |
+
elif image_path.lower().endswith('.ico'):
|
558 |
+
img, new_image_path = convert_to_rgba_png(image_path)
|
559 |
+
elif image_path.lower().endswith(('.gif', '.webp')):
|
560 |
+
img, new_image_path = convert_to_rgba_png(image_path)
|
561 |
+
else:
|
562 |
+
img, new_image_path = convert_to_rgba_png(image_path)
|
563 |
+
delete_image(image_path)
|
564 |
+
else:
|
565 |
+
new_image_path = image_path
|
566 |
if lut_filename is not None:
|
567 |
try:
|
568 |
img = apply_lut(img, lut_filename)
|
569 |
except Exception as e:
|
570 |
print(f"BAD LUT: Error applying LUT {str(e)}.")
|
571 |
+
img.save(new_image_path, format='PNG')
|
572 |
+
return img, str(new_image_path)
|
573 |
|
574 |
+
############################################# RGBA ###########################################################
|
575 |
|
576 |
+
# Example usage
|
577 |
+
# convert_jpg_to_rgba('input.jpg', 'output.png')
|
578 |
+
def convert_jpg_to_rgba(input_path) -> tuple[Image, str]:
|
579 |
"""
|
580 |
+
Convert a JPG image to RGBA format and save it as a PNG.
|
581 |
|
582 |
Args:
|
583 |
+
input_path (str or Path): Path to the input JPG image file.
|
584 |
|
585 |
Raises:
|
586 |
+
FileNotFoundError: If the input file does not exist.
|
587 |
+
ValueError: If the input file is not a JPG.
|
588 |
+
OSError: If there's an error reading or writing the file.
|
589 |
+
|
590 |
+
Returns:
|
591 |
+
tuple: A tuple containing the RGBA image and the output path as a string.
|
592 |
"""
|
593 |
try:
|
594 |
+
# Convert input_path to Path object if it's a string
|
595 |
+
input_path = Path(input_path)
|
596 |
+
output_path = input_path.with_suffix('.png')
|
597 |
+
|
598 |
+
# Check if the input file exists
|
599 |
+
if not input_path.exists():
|
600 |
+
raise FileNotFoundError(f"The file {input_path} does not exist.")
|
601 |
|
602 |
+
# Check file extension first to skip unnecessary processing
|
603 |
+
if input_path.suffix.lower() not in ('.jpg', '.jpeg'):
|
604 |
+
print(f"Skipping conversion: {input_path} is not a JPG or JPEG file.")
|
605 |
+
return None, str(output_path)
|
606 |
+
|
607 |
+
print(f"Converting to PNG: {input_path} is a JPG or JPEG file.")
|
608 |
+
|
609 |
+
# Open the image file
|
610 |
+
with Image.open(input_path) as img:
|
611 |
+
# Convert the image to RGBA mode
|
612 |
+
rgba_img = img.convert('RGBA')
|
613 |
+
|
614 |
+
# Ensure the directory exists for the output file
|
615 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
616 |
+
|
617 |
+
# Save the image with RGBA mode as PNG
|
618 |
+
rgba_img.save(output_path)
|
619 |
+
|
620 |
+
except FileNotFoundError as e:
|
621 |
+
print(f"Error: {e}")
|
622 |
+
except ValueError as e:
|
623 |
+
print(f"Error: {e}")
|
624 |
+
except OSError as e:
|
625 |
+
print(f"Error: An OS error occurred while processing the image - {e}")
|
626 |
+
except Exception as e:
|
627 |
+
print(f"An unexpected error occurred: {e}")
|
628 |
+
return rgba_img, str(output_path)
|
629 |
+
|
630 |
+
|
631 |
+
def convert_to_rgba_png(file_path: str) -> tuple[Image, str]:
|
632 |
+
"""
|
633 |
+
Converts an image to RGBA PNG format and saves it with the same base name and a .png extension.
|
634 |
+
Supports ICO files.
|
635 |
+
|
636 |
+
Args:
|
637 |
+
file_path (str): The path to the input image file.
|
638 |
+
|
639 |
+
Returns:
|
640 |
+
tuple: A tuple containing the RGBA image and the new file path as a string.
|
641 |
+
"""
|
642 |
+
new_file_path = None
|
643 |
+
rgba_img = None
|
644 |
+
img = None
|
645 |
+
if file_path is None:
|
646 |
+
raise UserWarning("No image provided.")
|
647 |
+
return None, None
|
648 |
+
try:
|
649 |
+
img = open_image(file_path)
|
650 |
+
print(f"Opened image: {file_path}\n")
|
651 |
+
# Handle ICO files
|
652 |
+
if file_path.lower().endswith('.ico'):
|
653 |
+
rgba_img = img.convert('RGBA')
|
654 |
+
new_file_path = Path(file_path).with_suffix('.png')
|
655 |
+
rgba_img.save(new_file_path, format='PNG')
|
656 |
+
print(f"Converted ICO to PNG: {new_file_path}")
|
657 |
+
else:
|
658 |
+
rgba_img, new_file_path = convert_jpg_to_rgba(file_path)
|
659 |
+
if rgba_img is None:
|
660 |
+
rgba_img = convert_rgb_to_rgba_safe(img)
|
661 |
+
new_file_path = Path(file_path).with_suffix('.png')
|
662 |
+
rgba_img.save(new_file_path, format='PNG')
|
663 |
+
print(f"Image saved as {new_file_path}")
|
664 |
except ValueError as ve:
|
665 |
print(f"ValueError: {ve}")
|
666 |
except Exception as e:
|
667 |
+
print(f"Error converting image: {e}")
|
668 |
+
return rgba_img if rgba_img else img, str(new_file_path)
|
669 |
+
|
670 |
+
def delete_image(file_path: str) -> None:
|
671 |
+
"""
|
672 |
+
Deletes the specified image file.
|
673 |
+
|
674 |
+
Parameters:
|
675 |
+
file_path (str): The path to the image file to delete.
|
676 |
+
|
677 |
+
Raises:
|
678 |
+
FileNotFoundError: If the file does not exist.
|
679 |
+
Exception: If there is an error deleting the file.
|
680 |
+
"""
|
681 |
+
try:
|
682 |
+
path = Path(file_path)
|
683 |
+
path.unlink()
|
684 |
+
print(f"Deleted original image: {file_path}")
|
685 |
+
except FileNotFoundError:
|
686 |
+
print(f"File not found: {file_path}")
|
687 |
+
except Exception as e:
|
688 |
+
print(f"Error deleting image: {e}")
|