Surn commited on
Commit
527fcc4
·
1 Parent(s): 444ba28

Fix for pngs and add warnings

Browse files
Files changed (5) hide show
  1. .gitignore +1 -0
  2. app.py +54 -26
  3. requirements.txt +4 -2
  4. utils/constants.py +23 -9
  5. 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
- import gradio as gr
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="RGBA",
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
- filter_color = gr.Checkbox(label="Filter Excluded Colors from Sampling", value=False,)
318
- exclude_color_button = gr.Button("Exclude Color", elem_id="exlude_color_button", elem_classes="solid")
319
- 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")
320
- selected_row = gr.Number(0, label="Selected Row", visible=False)
321
- delete_button = gr.Button("Delete Row", elem_id="delete_exclusion_button", elem_classes="solid")
322
- fill_hex = gr.Checkbox(label="Fill Hex with color from Image", value=True)
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(apply_lut_to_image_path, inputs=[lut_filename, input_image], outputs=[input_image],scroll_to_output=True)
 
 
 
 
 
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
- with gr.Row():
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
- with gr.Row():
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
- add_hex_text.change(
474
- fn=lambda x: (
475
- gr.update(visible=(x == "Custom List")),
476
- gr.update(visible=(x == "Custom List")),
477
- gr.update(visible=(x != None))
478
- ),
479
- inputs=add_hex_text,
480
- outputs=[custom_text_list, custom_text_color_list, hex_text_info]
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
- with gr.Row():
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(hex_create, 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], outputs=[output_image, overlay_image], scroll_to_output=True)
 
 
 
 
 
 
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
- import os
 
 
 
 
 
 
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
- img = Image.open(BytesIO(response.content))
 
 
 
 
 
 
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
- img = Image.open(BytesIO(decoded_data))
 
 
 
 
 
 
44
  else:
45
- # Assume that the image path is a file path
46
- img = Image.open(image_path)
 
 
 
 
 
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 type(image) is str:
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
- # Handle specific file formats by converting to appropriate modes
535
- if image_path.lower().endswith(('.gif', '.webp')):
536
- # Convert to RGBA to preserve transparency
537
- img = img.convert('RGBA')
538
- elif image_path.lower().endswith(('.jpg', '.jpeg')):
539
- # Convert to RGB since JPEG doesn't support transparency
540
- img = convert_rgb_to_rgba_safe(img)
541
-
542
- # For other formats like PNG, retain the existing mode
543
- # Apply the LUT if provided
 
 
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
- return img
 
550
 
 
551
 
552
- def convert_to_rgba_png(file_path: str) -> None:
 
 
553
  """
554
- Converts an image to RGBA PNG format and saves it with the same base name and a .png extension.
555
 
556
  Args:
557
- file_path (str): The path to the input image file.
558
 
559
  Raises:
560
- ValueError: If the input file extension is not a supported image format.
561
- Exception: If there is an error during the conversion or saving process.
 
 
 
 
562
  """
563
  try:
564
- # Open the original image
565
- img = open_image(file_path)
 
 
 
 
 
566
 
567
- # Convert the image to RGBA
568
- rgba_img = convert_rgb_to_rgba_safe(img)
569
- # Generate the new file name with .png extension
570
- base_name = os.path.splitext(file_path)[0]
571
- new_file_path = f"{base_name}.png"
572
- # Save the RGBA image as PNG
573
- rgba_img.save(new_file_path, format='PNG')
574
- print(f"Image saved as {new_file_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}")