mahan_ym commited on
Commit
e608228
·
1 Parent(s): 697792b

Refactor color transformation functions to use RGB values instead of HSV.

Browse files
Files changed (3) hide show
  1. src/app.py +13 -13
  2. src/modal_app.py +49 -21
  3. 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", "Hue", "Saturation Scale"],
23
- datatype=["str", "number", "number"],
24
- col_count=(3, "fixed"),
25
  show_row_numbers=True,
26
- label="Target Objects and New Settings",
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 hue and saturation scale for each object.""", # noqa: E501
50
  examples=[
51
  [
52
  "https://raw.githubusercontent.com/mahan-ym/ImageAlfred/main/src/assets/examples/test_1.jpg",
53
- [["pants", 128, 1]],
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/test_5.jpg",
61
- [["suits", 60, 1.5], ["pants", 10, 0.8]],
 
 
 
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) == 3
118
  and isinstance(target[0], str)
119
- and isinstance(target[1], (int, float))
120
- and isinstance(target[2], (int, float))
121
- and 0 <= target[1] <= 179
 
 
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
- hue = targets_config[input_label_idx][1]
150
- saturation_scale = targets_config[input_label_idx][2]
151
-
152
-
153
- mask = langsam_results[0]["masks"][idx]
154
- mask_bool = mask.astype(bool)
155
-
156
- img_hsv[mask_bool, 0] = float(hue)
157
- img_hsv[mask_bool, 1] = np.minimum(
158
- img_hsv[mask_bool, 1] * saturation_scale,
159
- 255.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- This function segments objects in the image based on a user-provided text prompt, then
95
- modifies their hue and saturation in the HSV (Hue, Saturation, Value) space. HSV is intuitive
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
- - Performing broad color changes or visual effects (e.g., turning a shirt from red to blue).
101
- - Needing intuitive control over color categories (e.g., shifting everything that's red to purple).
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 three elements in the following order: 1. target_object (str) - A short, human-readable description of the object to be modified.Multi-word 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. hue (int) - Desired hue value in the HSV color space, ranging from 0 to 179. Represents the color angle on the HSV color wheel (e.g., 0 = red, 60 = green, 120 = blue)3. saturation_scale (float) - A multiplicative scale factor applied to the current saturation of the object (must be > 0). For example, 1.0 preserves current saturation, 1.2 increases vibrancy, and 0.8 slightly desaturates. Each target object must be uniquely defined in the list to avoid conflicting transformations.Example: [["hair", 30, 1.2], ["right person shirt", 60, 1.0]]
117
 
118
  Returns:
119
  Base64-encoded string.
120
 
121
  Raises:
122
- ValueError: If user_input format is invalid, hue values are outside [0, 179] range, saturation_scale is not positive, or image format is invalid or corrupted.
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) != 3:
136
  raise gr.Error(
137
- "Each item in user_input must be a list of [object, hue, saturation_scale]" # noqa: E501
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
- if not item[1]:
147
- raise gr.Error("Hue must be set and cannot be empty.")
148
- if not isinstance(item[1], (int, float)):
149
- try:
150
- item[1] = int(item[1])
151
- except ValueError:
152
- raise gr.Error("Hue must be an integer.")
153
- if item[1] < 0 or item[1] > 179:
154
- raise gr.Error("Hue must be in the range [0, 179]")
155
- if not item[2]:
156
- raise gr.Error("Saturation scale must be set and cannot be empty.")
157
- if not isinstance(item[2], (int, float)):
158
- try:
159
- item[2] = float(item[2])
160
- except ValueError:
161
- raise gr.Error("Saturation scale must be a float number.")
162
- if item[2] <= 0:
163
- raise gr.Error("Saturation scale must be greater than 0")
 
 
 
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