mahan_ym
commited on
Commit
·
e608228
1
Parent(s):
697792b
Refactor color transformation functions to use RGB values instead of HSV.
Browse files- src/app.py +13 -13
- src/modal_app.py +49 -21
- src/tools.py +30 -41
src/app.py
CHANGED
@@ -19,13 +19,12 @@ title = """Image Alfred - Recolor and Privacy Preserving Image MCP Tools
|
|
19 |
""" # noqa: E501
|
20 |
|
21 |
hsv_df_input = gr.Dataframe(
|
22 |
-
headers=["Object", "
|
23 |
-
datatype=["str", "number", "number"],
|
24 |
-
col_count=(
|
25 |
show_row_numbers=True,
|
26 |
-
label="Target Objects and
|
27 |
type="array",
|
28 |
-
# row_count=(1, "dynamic"),
|
29 |
)
|
30 |
|
31 |
lab_df_input = gr.Dataframe(
|
@@ -46,19 +45,20 @@ change_color_objects_hsv_tool = gr.Interface(
|
|
46 |
title="Image Recolor Tool (HSV)",
|
47 |
description="""
|
48 |
This tool allows you to recolor objects in an image using the HSV color space.
|
49 |
-
You can specify the
|
50 |
examples=[
|
51 |
[
|
52 |
"https://raw.githubusercontent.com/mahan-ym/ImageAlfred/main/src/assets/examples/test_1.jpg",
|
53 |
-
[
|
54 |
-
|
55 |
-
|
56 |
-
"https://raw.githubusercontent.com/mahan-ym/ImageAlfred/main/src/assets/examples/test_4.jpg",
|
57 |
-
[["desk", 15, 0.5], ["left cup", 40, 1.1]],
|
58 |
],
|
59 |
[
|
60 |
-
"https://raw.githubusercontent.com/mahan-ym/ImageAlfred/main/src/assets/examples/
|
61 |
-
[
|
|
|
|
|
|
|
62 |
],
|
63 |
],
|
64 |
)
|
|
|
19 |
""" # noqa: E501
|
20 |
|
21 |
hsv_df_input = gr.Dataframe(
|
22 |
+
headers=["Object", "Red", "Green", "Blue"],
|
23 |
+
datatype=["str", "number", "number", "number"],
|
24 |
+
col_count=(4, "fixed"),
|
25 |
show_row_numbers=True,
|
26 |
+
label="Target Objects and Their new RGB Colors",
|
27 |
type="array",
|
|
|
28 |
)
|
29 |
|
30 |
lab_df_input = gr.Dataframe(
|
|
|
45 |
title="Image Recolor Tool (HSV)",
|
46 |
description="""
|
47 |
This tool allows you to recolor objects in an image using the HSV color space.
|
48 |
+
You can specify the RGB values for each object.""", # noqa: E501
|
49 |
examples=[
|
50 |
[
|
51 |
"https://raw.githubusercontent.com/mahan-ym/ImageAlfred/main/src/assets/examples/test_1.jpg",
|
52 |
+
[
|
53 |
+
["pants", 255, 178, 102],
|
54 |
+
],
|
|
|
|
|
55 |
],
|
56 |
[
|
57 |
+
"https://raw.githubusercontent.com/mahan-ym/ImageAlfred/main/src/assets/examples/test_8.jpg",
|
58 |
+
[
|
59 |
+
["pants", 114, 117, 34],
|
60 |
+
["shirt", 51, 51, 37],
|
61 |
+
],
|
62 |
],
|
63 |
],
|
64 |
)
|
src/modal_app.py
CHANGED
@@ -107,19 +107,20 @@ def change_image_objects_hsv(
|
|
107 |
image_pil: Image.Image,
|
108 |
targets_config: list[list[str | int | float]],
|
109 |
) -> Image.Image:
|
110 |
-
"""Changes the hue and saturation of specified objects in an image.
|
111 |
-
This function uses LangSAM to segment objects in the image based on provided prompts,
|
112 |
-
and then modifies the hue and saturation of those objects in the HSV color space.
|
113 |
-
""" # noqa: E501
|
114 |
if not isinstance(targets_config, list) or not all(
|
115 |
(
|
116 |
isinstance(target, list)
|
117 |
-
and len(target) ==
|
118 |
and isinstance(target[0], str)
|
119 |
-
and isinstance(target[1], (int
|
120 |
-
and isinstance(target[2], (int
|
121 |
-
and
|
|
|
|
|
122 |
and target[2] >= 0
|
|
|
|
|
|
|
123 |
)
|
124 |
for target in targets_config
|
125 |
):
|
@@ -140,23 +141,50 @@ def change_image_objects_hsv(
|
|
140 |
img_hsv = cv2.cvtColor(img_array, cv2.COLOR_RGB2HSV).astype(np.float32)
|
141 |
|
142 |
for idx, label in enumerate(output_labels):
|
143 |
-
if not label or label=="":
|
144 |
print("Skipping empty label.")
|
145 |
continue
|
146 |
input_label, score, _ = process.extractOne(label, input_labels)
|
147 |
input_label_idx = input_labels.index(input_label)
|
148 |
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
)
|
161 |
|
162 |
output_img = cv2.cvtColor(img_hsv.astype(np.uint8), cv2.COLOR_HSV2RGB)
|
@@ -204,7 +232,7 @@ def change_image_objects_lab(
|
|
204 |
)
|
205 |
if not langsam_results:
|
206 |
return image_pil
|
207 |
-
|
208 |
input_labels = [target[0] for target in targets_config]
|
209 |
output_labels = langsam_results[0]["labels"]
|
210 |
scores = langsam_results[0]["scores"]
|
|
|
107 |
image_pil: Image.Image,
|
108 |
targets_config: list[list[str | int | float]],
|
109 |
) -> Image.Image:
|
|
|
|
|
|
|
|
|
110 |
if not isinstance(targets_config, list) or not all(
|
111 |
(
|
112 |
isinstance(target, list)
|
113 |
+
and len(target) == 4
|
114 |
and isinstance(target[0], str)
|
115 |
+
and isinstance(target[1], (int))
|
116 |
+
and isinstance(target[2], (int))
|
117 |
+
and isinstance(target[3], (int))
|
118 |
+
and target[1] >= 0
|
119 |
+
and target[1] <= 255
|
120 |
and target[2] >= 0
|
121 |
+
and target[2] <= 255
|
122 |
+
and target[3] >= 0
|
123 |
+
and target[3] <= 255
|
124 |
)
|
125 |
for target in targets_config
|
126 |
):
|
|
|
141 |
img_hsv = cv2.cvtColor(img_array, cv2.COLOR_RGB2HSV).astype(np.float32)
|
142 |
|
143 |
for idx, label in enumerate(output_labels):
|
144 |
+
if not label or label == "":
|
145 |
print("Skipping empty label.")
|
146 |
continue
|
147 |
input_label, score, _ = process.extractOne(label, input_labels)
|
148 |
input_label_idx = input_labels.index(input_label)
|
149 |
|
150 |
+
target_rgb = targets_config[input_label_idx][1:]
|
151 |
+
target_hsv = cv2.cvtColor(np.uint8([[target_rgb]]), cv2.COLOR_RGB2HSV)[0][0]
|
152 |
+
|
153 |
+
mask = langsam_results[0]["masks"][idx].astype(bool)
|
154 |
+
h, s, v = cv2.split(img_hsv)
|
155 |
+
# Convert all channels to float32 for consistent processing
|
156 |
+
h = h.astype(np.float32)
|
157 |
+
s = s.astype(np.float32)
|
158 |
+
v = v.astype(np.float32)
|
159 |
+
|
160 |
+
# Compute original S and V means inside the mask
|
161 |
+
mean_s = np.mean(s[mask])
|
162 |
+
mean_v = np.mean(v[mask])
|
163 |
+
|
164 |
+
# Target S and V
|
165 |
+
target_hue, target_s, target_v = target_hsv
|
166 |
+
|
167 |
+
# Compute scaling factors (avoid div by zero)
|
168 |
+
scale_s = target_s / mean_s if mean_s > 0 else 1.0
|
169 |
+
scale_v = target_v / mean_v if mean_v > 0 else 1.0
|
170 |
+
|
171 |
+
scale_s = np.clip(scale_s, 0.8, 1.2)
|
172 |
+
scale_v = np.clip(scale_v, 0.8, 1.2)
|
173 |
+
|
174 |
+
# Apply changes only in mask
|
175 |
+
h[mask] = target_hue
|
176 |
+
s = s.astype(np.float32)
|
177 |
+
v = v.astype(np.float32)
|
178 |
+
s[mask] = np.clip(s[mask] * scale_s, 0, 255)
|
179 |
+
v[mask] = np.clip(v[mask] * scale_v, 0, 255)
|
180 |
+
|
181 |
+
# Merge and convert back
|
182 |
+
img_hsv = cv2.merge(
|
183 |
+
[
|
184 |
+
h.astype(np.uint8),
|
185 |
+
s.astype(np.uint8),
|
186 |
+
v.astype(np.uint8),
|
187 |
+
]
|
188 |
)
|
189 |
|
190 |
output_img = cv2.cvtColor(img_hsv.astype(np.uint8), cv2.COLOR_HSV2RGB)
|
|
|
232 |
)
|
233 |
if not langsam_results:
|
234 |
return image_pil
|
235 |
+
|
236 |
input_labels = [target[0] for target in targets_config]
|
237 |
output_labels = langsam_results[0]["labels"]
|
238 |
scores = langsam_results[0]["scores"]
|
src/tools.py
CHANGED
@@ -90,36 +90,22 @@ def change_color_objects_hsv(
|
|
90 |
) -> np.ndarray | Image.Image | str | Path | None:
|
91 |
"""
|
92 |
Changes the hue and saturation of specified objects in an image using the HSV color space.
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
for color manipulation where users think in terms of basic color categories and intensity,
|
97 |
-
making it useful for broad, vivid color shifts.
|
98 |
-
|
99 |
Use this method when:
|
100 |
-
-
|
101 |
-
-
|
102 |
-
- Saturation and vibrancy manipulation are more important than accurate perceptual matching.
|
103 |
-
|
104 |
-
OpenCV HSV Ranges:
|
105 |
-
- H: 0-179 (Hue angle on color wheel, where 0 = red, 60 = green, 120 = blue, etc.)
|
106 |
-
- S: 0-255 (Saturation)
|
107 |
-
- V: 0-255 (Brightness)
|
108 |
-
|
109 |
-
Common HSV color references:
|
110 |
-
- Red: (Hue≈0), Green: (Hue≈60), Blue: (Hue≈120), Yellow: (Hue≈30), Purple: (Hue≈150)
|
111 |
-
- Typically used with Saturation=255 for vivid colors.
|
112 |
-
|
113 |
|
114 |
Args:
|
115 |
input_img: Input image or can be URL string of the image or base64 string. Cannot be None.
|
116 |
-
user_input : A list of target specifications for color transformation. Each inner list must contain exactly
|
117 |
|
118 |
Returns:
|
119 |
Base64-encoded string.
|
120 |
|
121 |
Raises:
|
122 |
-
ValueError: If user_input format is invalid,
|
123 |
TypeError: If input_img is not a supported type or modal function returns unexpected type.
|
124 |
""" # noqa: E501
|
125 |
if len(user_input) == 0 or not isinstance(user_input, list):
|
@@ -132,9 +118,9 @@ def change_color_objects_hsv(
|
|
132 |
print("before processing input:", user_input)
|
133 |
valid_pattern = re.compile(r"^[a-zA-Z\s]+$")
|
134 |
for item in user_input:
|
135 |
-
if len(item) !=
|
136 |
raise gr.Error(
|
137 |
-
"Each item in user_input must be a list of [object,
|
138 |
)
|
139 |
if not item[0] or not valid_pattern.match(item[0]):
|
140 |
raise gr.Error(
|
@@ -143,24 +129,27 @@ def change_color_objects_hsv(
|
|
143 |
|
144 |
if not isinstance(item[0], str):
|
145 |
item[0] = str(item[0])
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
|
|
|
|
|
|
164 |
|
165 |
print("after processing input:", user_input)
|
166 |
|
|
|
90 |
) -> np.ndarray | Image.Image | str | Path | None:
|
91 |
"""
|
92 |
Changes the hue and saturation of specified objects in an image using the HSV color space.
|
93 |
+
This function segments image regions based on a user-provided text prompt and applies
|
94 |
+
color transformations in the HSV color space. HSV separates chromatic content (hue) from
|
95 |
+
intensity (value), making it more intuitive for color manipulation tasks.
|
|
|
|
|
|
|
96 |
Use this method when:
|
97 |
+
- You want to change the color of objects based on their hue and saturation.
|
98 |
+
- You want to apply color transformations that are less influenced by lighting conditions or brightness variations.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
|
100 |
Args:
|
101 |
input_img: Input image or can be URL string of the image or base64 string. Cannot be None.
|
102 |
+
user_input : A list of target specifications for color transformation. Each inner list must contain exactly four elements in the following order: 1. target_object (str) - A short, human-readable description of the object to be modified. Multi-word(not recommended) descriptions are allowed for disambiguation (e.g., "right person shirt"), but they must be at most three words and concise and free of punctuation, symbols, or special characters.2. Red (int) - Desired red value in RGB color space from 0 to 255. 3. Green (int) - Desired green value in RGB color space from 0 to 255. 4. Blue (int) - Desired blue value in RGB color space from 0 to 255. Example: user_input = [["hair", 30, 55, 255], ["shirt", 70, 0 , 157]].
|
103 |
|
104 |
Returns:
|
105 |
Base64-encoded string.
|
106 |
|
107 |
Raises:
|
108 |
+
ValueError: If user_input format is invalid, or image format is invalid or corrupted.
|
109 |
TypeError: If input_img is not a supported type or modal function returns unexpected type.
|
110 |
""" # noqa: E501
|
111 |
if len(user_input) == 0 or not isinstance(user_input, list):
|
|
|
118 |
print("before processing input:", user_input)
|
119 |
valid_pattern = re.compile(r"^[a-zA-Z\s]+$")
|
120 |
for item in user_input:
|
121 |
+
if len(item) != 4:
|
122 |
raise gr.Error(
|
123 |
+
"Each item in user_input must be a list of [object, red, green, blue]" # noqa: E501
|
124 |
)
|
125 |
if not item[0] or not valid_pattern.match(item[0]):
|
126 |
raise gr.Error(
|
|
|
129 |
|
130 |
if not isinstance(item[0], str):
|
131 |
item[0] = str(item[0])
|
132 |
+
|
133 |
+
try:
|
134 |
+
item[1] = int(item[1])
|
135 |
+
except ValueError:
|
136 |
+
raise gr.Error("Red must be an integer.")
|
137 |
+
if item[1] < 0 or item[1] > 255:
|
138 |
+
raise gr.Error("Red must be in the range [0, 255]")
|
139 |
+
|
140 |
+
try:
|
141 |
+
item[2] = int(item[2])
|
142 |
+
except ValueError:
|
143 |
+
raise gr.Error("Green must be an integer.")
|
144 |
+
if item[2] < 0 or item[2] > 255:
|
145 |
+
raise gr.Error("Green must be in the range [0, 255]")
|
146 |
+
|
147 |
+
try:
|
148 |
+
item[3] = int(item[3])
|
149 |
+
except ValueError:
|
150 |
+
raise gr.Error("Blue must be an integer.")
|
151 |
+
if item[3] < 0 or item[3] > 255:
|
152 |
+
raise gr.Error("Blue must be in the range [0, 255]")
|
153 |
|
154 |
print("after processing input:", user_input)
|
155 |
|