AsmaAILab commited on
Commit
fcafbf4
·
verified ·
1 Parent(s): 4f1dff7

Upload app_recolor.py

Browse files
Files changed (1) hide show
  1. app_recolor.py +546 -0
app_recolor.py ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """color changing of objects.ipynb
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1HsLQzlFkDmY380DOwp6Ppl_1fM5nIAlq
8
+ """
9
+
10
+ import torch
11
+ import torchvision.transforms as T
12
+ from transformers import Mask2FormerImageProcessor, Mask2FormerForUniversalSegmentation
13
+ from PIL import Image, ImageFilter
14
+ import numpy as np
15
+ from typing import List, Tuple, Dict, Union
16
+ from skimage import color # For LAB color space manipulation
17
+
18
+ # --- Determine Device ---
19
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
20
+ # For color changing, float32 is generally sufficient and avoids half-precision issues
21
+ DTYPE = torch.float32
22
+ print(f"Using device: {DEVICE} with dtype: {DTYPE}")# --- Determine Device ---
23
+
24
+ # --- Model Loading ---
25
+ # Mask2Former for semantic segmentation
26
+ print("Loading Mask2Former model for semantic segmentation...")
27
+ processor = Mask2FormerImageProcessor.from_pretrained("facebook/mask2former-swin-large-ade-semantic")
28
+ model = Mask2FormerForUniversalSegmentation.from_pretrained("facebook/mask2former-swin-large-ade-semantic")
29
+ model = model.to(DEVICE)
30
+ print(f"Mask2Former (Semantic Segmentation) loaded to {DEVICE}.")
31
+ print("All necessary AI models loaded successfully.")
32
+
33
+ COLOR_MAPPING_ = {
34
+ '#FFFFFF': 'background', "#787878": "wall", "#B47878": "building;edifice", "#06E6E6": "sky",
35
+ "#503232": "floor;flooring", "#04C803": "tree", "#787850": "ceiling", "#8C8C8C": "road;route",
36
+ "#CC05FF": "bed", "#E6E6E6": "windowpane;window", "#04FA07": "grass", "#E005FF": "cabinet",
37
+ "#EBFF07": "sidewalk;pavement", "#96053D": "person;individual;someone;somebody;mortal;soul",
38
+ "#787846": "earth;ground", "#08FF33": "door;double;door", "#FF0652": "table", "#8FFF8C": "mountain;mount",
39
+ "#CCFF04": "plant;flora;plant;life", "#FF3307": "curtain;drape;drapery;mantle;pall",
40
+ "#CC4603": "chair", "#0066C8": "car;auto;automobile;machine;motorcar", "#3DE6FA": "water",
41
+ "#FF0633": "painting;picture", "#0B66FF": "sofa;couch;lounge", "#FF0747": "shelf",
42
+ "#FF09E0": "house", "#0907E6": "sea", "#DCDCDC": "mirror", "#FF095C": "rug;carpet;carpeting",
43
+ "#7009FF": "field", "#08FFD6": "armchair", "#07FFE0": "seat", "#FFB806": "fence;fencing",
44
+ "#0AFF47": "desk", "#FF290A": "rock;stone", "#07FFFF": "wardrobe;closet;press",
45
+ "#E0FF08": "lamp", "#6608FF": "bathtub;bathing;tub;bath;tub", "#FF3D06": "railing;rail",
46
+ "#FFC207": "cushion", "#FF7A08": "base;pedestal;stand", "#00FF14": "box",
47
+ "#FF0829": "column;pillar", "#FF0599": "signboard;sign", "#0633FF": "chest;of;drawers;chest;bureau;dresser",
48
+ "#EB0CFF": "counter", "#A09614": "sand", "#00A3FF": "sink", "#8C8C8C": "skyscraper",
49
+ "#FA0A0F": "fireplace;hearth;open;fireplace", "#14FF00": "refrigerator;icebox",
50
+ "#1FFF00": "grandstand;covered;stand", "#FF1F00": "path", "#FFE000": "stairs;steps",
51
+ "#99FF00": "runway", "#0000FF": "case;display;case;showcase;vitrine",
52
+ "#FF4700": "pool;table;billiard;table;snooker;table", "#00EBFF": "pillow",
53
+ "#00ADFF": "screen;door;screen", "#1F00FF": "stairway;staircase", "#0BC8C8": "river",
54
+ "#FF5200": "bridge;span", "#00FFF5": "bookcase", "#003DFF": "blind;screen",
55
+ "#00FF70": "coffee;table;cocktail;table", "#00FF85": "toilet;can;commode;crapper;pot;potty;stool;throne",
56
+ "#FF0000": "flower", "#FFA300": "book", "#FF6600": "hill", "#C2FF00": "bench",
57
+ "#008FFF": "countertop", "#33FF00": "stove;kitchen;stove;range;kitchen;range;cooking;stove",
58
+ "#0052FF": "palm;palm;tree", "#00FF29": "kitchen;island",
59
+ "#00FFAD": "computer;computing;machine;computing;device;data;processor;electronic;computer;information;processing;system",
60
+ "#0A00FF": "swivel;chair", "#ADFF00": "boat", "#00FF99": "bar", "#FF5C00": "arcade;machine",
61
+ "#FF00FF": "hovel;hut;hutch;shack;shanty", "#FF00F5": "bus;autobus;coach;charabanc;double-decker;jitney;motorbus;motorcoach;omnibus;passenger;vehicle",
62
+ "#FF0066": "towel", "#FFAD00": "light;light;source", "#FF0014": "truck;motortruck",
63
+ "#FFB8B8": "tower", "#001FFF": "chandelier;pendant;pendent", "#00FF3D": "awning;sunshade;sunblind",
64
+ "#0047FF": "streetlight;street;lamp", "#FF00CC": "booth;cubicle;stall;kiosk",
65
+ "#00FFC2": "television;television;receiver;television;set;tv;tv;set;idiot;box;boob;tube;telly;goggle;box",
66
+ "#00FF52": "airplane;aeroplane;plane", "#000AFF": "dirt;track",
67
+ "#0070FF": "apparel;wearing;apparel;dress;clothes", "#3300FF": "pole",
68
+ "#00C2FF": "land;ground;soil", "#007AFF": "bannister;banister;balustrade;balusters;handrail",
69
+ "#00FFA3": "escalator;moving;staircase;moving;stairway",
70
+ "#FF9900": "ottoman;pouf;pouffe;puff;hassock", "#00FF0A": "bottle",
71
+ "#FF7000": "buffet;counter;sideboard", "#8FFF00": "poster;posting;placard;notice;bill;card",
72
+ "#5200FF": "stage", "#A3FF00": "van", "#FFEB00": "ship", "#08B8AA": "fountain",
73
+ "#8500FF": "conveyer;belt;conveyor;belt;conveyer;conveyor;transporter", "#00FF5C": "canopy",
74
+ "#B800FF": "washer;automatic;washer;washing;machine", "#FF001F": "plaything;toy",
75
+ "#00B8FF": "swimming;pool;swimming;bath;natatorium", "#00D6FF": "stool", "#FF0070": "barrel;cask",
76
+ "#5CFF00": "basket;handbasket", "#00E0FF": "waterfall;falls", "#70E0FF": "tent;collapsible;shelter",
77
+ "#46B8A0": "bag", "#A300FF": "minibike;motorbike", "#9900FF": "cradle", "#47FF00": "oven",
78
+ "#FF00A3": "ball", "#FFCC00": "food;solid;food", "#FF008F": "step;stair",
79
+ "#00FFEB": "tank;storage;tank", "#85FF00": "trade;name;brand;name;brand;marque",
80
+ "#FF00EB": "microwave;microwave;oven", "#F500FF": "pot;flowerpot",
81
+ "#FF007A": "animal;animate;being;beast;brute;creature;fauna", "#FFF500": "bicycle;bike;wheel;cycle",
82
+ "#0ABED4": "lake", "#D6FF00": "dishwasher;dish;washer;dishwashing;machine",
83
+ "#00CCFF": "screen;silver;screen;projection;screen", "#1400FF": "blanket;cover",
84
+ "#FFFF00": "sculpture", "#0099FF": "hood;exhaust;hood", "#0029FF": "sconce", "#00FFCC": "vase",
85
+ "#2900FF": "traffic;light;traffic;signal;stoplight", "#29FF00": "tray",
86
+ "#AD00FF": "ashcan;trash;can;garbage;can;wastebin;ash;bin;ash-bin;ashbin;dustbin;trash;barrel;trash;bin",
87
+ "#00F5FF": "fan", "#4700FF": "pier;wharf;wharfage;dock", "#7A00FF": "crt;screen",
88
+ "#00FFB8": "plate", "#005CFF": "monitor;monitoring;device", "#B8FF00": "bulletin;board;notice;board",
89
+ "#0085FF": "shower", "#FFD600": "radiator", "#19C2C2": "glass;drinking;glass",
90
+ "#66FF00": "clock", "#5C00FF": "flag",
91
+ }
92
+
93
+ def to_rgb(color_hex: str) -> Tuple[int, int, int]:
94
+ """Converts a hex color string to an RGB tuple."""
95
+ color_hex = color_hex.lstrip('#')
96
+ return tuple(int(color_hex[i:i+2], 16) for i in (0, 2, 4))
97
+
98
+ def map_colors_rgb(color_rgb_tuple: Tuple[int, int, int]) -> str:
99
+ """Maps an RGB color tuple to a semantic object name."""
100
+ # Initialize COLOR_MAPPING_RGB if not already done (should be done once at module load)
101
+ global COLOR_MAPPING_RGB
102
+ if 'COLOR_MAPPING_RGB' not in globals():
103
+ COLOR_MAPPING_RGB = {to_rgb(k): v for k, v in COLOR_MAPPING_.items()}
104
+
105
+ if color_rgb_tuple in COLOR_MAPPING_RGB:
106
+ return COLOR_MAPPING_RGB[color_rgb_tuple]
107
+ else:
108
+ # Fallback to finding the closest color name if exact match not found
109
+ closest_color_name = "unknown"
110
+ min_dist = float('inf')
111
+ for mapped_rgb, name in COLOR_MAPPING_RGB.items():
112
+ dist = np.sum((np.array(color_rgb_tuple) - np.array(mapped_rgb))**2)
113
+ if dist < min_dist:
114
+ min_dist = dist
115
+ closest_color_name = name
116
+ return closest_color_name
117
+
118
+ # Initialize COLOR_MAPPING_RGB here so it's ready when imported
119
+ COLOR_MAPPING_RGB = {to_rgb(k): v for k, v in COLOR_MAPPING_.items()}
120
+
121
+ def ade_palette() -> List[List[int]]:
122
+ """Returns the ADE20K palette for semantic segmentation visualization."""
123
+ return [[120, 120, 120], [180, 120, 120], [6, 230, 230], [80, 50, 50],
124
+ [4, 200, 3], [120, 120, 80], [140, 140, 140], [204, 5, 255],
125
+ [230, 230, 230], [4, 250, 7], [224, 5, 255], [235, 255, 7],
126
+ [150, 5, 61], [120, 120, 70], [8, 255, 51], [255, 6, 82],
127
+ [143, 255, 140], [204, 255, 4], [255, 51, 7], [204, 70, 3],
128
+ [0, 102, 200], [61, 230, 250], [255, 6, 51], [11, 102, 255],
129
+ [255, 7, 71], [255, 9, 224], [9, 7, 230], [220, 220, 220],
130
+ [255, 9, 92], [112, 9, 255], [8, 255, 214], [7, 255, 224],
131
+ [255, 184, 6], [10, 255, 71], [255, 41, 10], [7, 255, 255],
132
+ [224, 255, 8], [102, 8, 255], [255, 61, 6], [255, 194, 7],
133
+ [255, 122, 8], [0, 255, 20], [255, 8, 41], [255, 5, 153],
134
+ [6, 51, 255], [235, 12, 255], [160, 150, 20], [0, 163, 255],
135
+ [140, 140, 140], [250, 10, 15], [20, 255, 0], [31, 255, 0],
136
+ [255, 31, 0], [255, 224, 0], [153, 255, 0], [0, 0, 255],
137
+ [255, 71, 0], [0, 235, 255], [0, 173, 255], [31, 0, 255],
138
+ [11, 200, 200], [255, 82, 0], [0, 255, 245], [0, 61, 255],
139
+ [0, 255, 112], [0, 255, 133], [255, 0, 0], [255, 163, 0],
140
+ [255, 102, 0], [194, 255, 0], [0, 143, 255], [51, 255, 0],
141
+ [0, 82, 255], [0, 255, 41], [0, 255, 173], [10, 0, 255],
142
+ [173, 255, 0], [0, 255, 153], [255, 92, 0], [255, 0, 255],
143
+ [255, 0, 245], [255, 0, 102], [255, 173, 0], [255, 0, 20],
144
+ [255, 184, 184], [0, 31, 255], [0, 255, 61], [0, 71, 255],
145
+ [255, 0, 204], [0, 255, 194], [0, 255, 82], [0, 10, 255],
146
+ [0, 112, 255], [51, 0, 255], [0, 194, 255], [0, 122, 255],
147
+ [0, 255, 163], [255, 153, 0], [0, 255, 10], [255, 112, 0],
148
+ [143, 255, 0], [82, 0, 255], [163, 255, 0], [255, 235, 0],
149
+ [8, 184, 170], [133, 0, 255], [0, 255, 92], [184, 0, 255],
150
+ [255, 0, 31], [0, 184, 255], [0, 214, 255], [255, 0, 112],
151
+ [92, 255, 0], [0, 224, 255], [112, 224, 255], [70, 184, 160],
152
+ [163, 0, 255], [153, 0, 255], [71, 255, 0], [255, 0, 163],
153
+ [255, 204, 0], [255, 0, 143], [0, 255, 235], [133, 255, 0],
154
+ [255, 0, 235], [245, 0, 255], [255, 0, 122], [255, 245, 0],
155
+ [10, 190, 212], [214, 255, 0], [0, 204, 255], [20, 0, 255],
156
+ [255, 255, 0], [0, 153, 255], [0, 41, 255], [0, 255, 204],
157
+ [41, 0, 255], [41, 255, 0], [173, 0, 255], [0, 245, 255],
158
+ [71, 0, 255], [122, 0, 255], [0, 255, 184], [0, 92, 255],
159
+ [184, 255, 0], [0, 133, 255], [255, 214, 0], [25, 194, 194],
160
+ [102, 255, 0], [92, 0, 255]]
161
+
162
+ def load_and_preprocess_image(image: Union[Image.Image, np.ndarray]) -> Image.Image:
163
+ """
164
+ Loads an image (PIL or numpy array) and preprocesses it for model input.
165
+ """
166
+ if isinstance(image, np.ndarray):
167
+ if image.shape[-1] == 4: # If RGBA, convert to RGB
168
+ image = Image.fromarray(image).convert('RGB')
169
+ else: # Assume RGB
170
+ image = Image.fromarray(image)
171
+ elif not isinstance(image, Image.Image):
172
+ raise TypeError(f"load_and_preprocess_image received unexpected image type: {type(image)}")
173
+
174
+ image = image.convert("RGB")
175
+ image = image.resize((512, 512)) # Standardize size for models
176
+ return image
177
+
178
+ def get_segmentation_data(image: Image.Image) -> Tuple[np.ndarray, Image.Image, List[str], Dict[Tuple[int, int, int], str]]:
179
+ """
180
+ Performs semantic segmentation on the input image.
181
+ Returns the raw segmentation map (numpy), a colored segmentation image (PIL),
182
+ a list of detected object names, and the segment items map.
183
+ """
184
+ if not isinstance(image, Image.Image):
185
+ raise TypeError("Input 'image' must be a PIL Image object.")
186
+
187
+ with torch.inference_mode():
188
+ semantic_inputs = processor(images=image, return_tensors="pt", size={"height": 256, "width": 256})
189
+ semantic_inputs = {key: value.to(DEVICE) for key, value in semantic_inputs.items()}
190
+
191
+ semantic_outputs = model(**semantic_inputs)
192
+
193
+ if hasattr(semantic_outputs, 'logits') and torch.is_tensor(semantic_outputs.logits):
194
+ semantic_outputs.logits = semantic_outputs.logits.to("cpu")
195
+ if hasattr(semantic_outputs, 'pred_masks') and torch.is_tensor(semantic_outputs.pred_masks):
196
+ semantic_outputs.pred_masks = semantic_outputs.pred_masks.to("cpu")
197
+
198
+ segmentation_maps = processor.post_process_semantic_segmentation(semantic_outputs, target_sizes=[image.size[::-1]])
199
+ predicted_semantic_map_np = segmentation_maps[0].cpu().numpy()
200
+
201
+ if predicted_semantic_map_np.size == 0:
202
+ print("Warning: Mask2Former detected no objects in the image.")
203
+ color_seg = np.zeros((image.size[1], image.size[0], 3), dtype=np.uint8)
204
+ detected_items = []
205
+ temp_segment_map = {}
206
+ else:
207
+ color_seg = np.zeros((predicted_semantic_map_np.shape[0], predicted_semantic_map_np.shape[1], 3), dtype=np.uint8)
208
+ palette = np.array(ade_palette())
209
+ unique_labels = np.unique(predicted_semantic_map_np)
210
+
211
+ detected_items = []
212
+ temp_segment_map = {}
213
+
214
+ for label in unique_labels:
215
+ color = palette[label]
216
+ item_name = map_colors_rgb(tuple(color))
217
+ color_seg[predicted_semantic_map_np == label, :] = color
218
+ if item_name not in detected_items:
219
+ detected_items.append(item_name)
220
+ temp_segment_map[tuple(color)] = item_name
221
+
222
+ seg_image = Image.fromarray(color_seg).convert('RGB')
223
+ return predicted_semantic_map_np, seg_image, detected_items, temp_segment_map
224
+
225
+
226
+
227
+ from skimage import color # For LAB color space manipulation
228
+ from PIL import ImageFilter # Import ImageFilter here
229
+
230
+
231
+ def apply_color_change_to_objects(
232
+ original_image: Image.Image,
233
+ segmentation_np: np.ndarray,
234
+ segment_items_map: Dict[Tuple[int, int, int], str],
235
+ selected_object_names: List[str],
236
+ target_hex_color: str
237
+ ) -> Image.Image:
238
+ """
239
+ Applies a new color to selected objects in the original image, preserving texture and lighting.
240
+ Uses LAB color space for initial color shift and then blends with original luminance.
241
+ Also applies a slight blur to the mask for smoother transitions.
242
+ """
243
+ # Ensure skimage.color is available within the function scope
244
+ import skimage.color
245
+ # Ensure ImageFilter is available within the function scope
246
+ from PIL import ImageFilter
247
+
248
+
249
+ if not selected_object_names:
250
+ return original_image # No objects selected, return original
251
+
252
+ # Convert original image to NumPy array and then to LAB color space
253
+ original_np = np.array(original_image)
254
+ print(f"original_np shape: {original_np.shape}, dtype: {original_np.dtype}")
255
+ original_np_normalized = original_np / 255.0
256
+ print(f"original_np_normalized shape: {original_np_normalized.shape}, dtype: {original_np_normalized.dtype}")
257
+ original_lab = skimage.color.rgb2lab(original_np_normalized) # Normalize to 0-1 for skimage
258
+
259
+ print("hexa color to rgb")
260
+ # Convert target hex color to RGB tuple (0-255)
261
+ target_rgb_tuple = to_rgb(target_hex_color)
262
+ print("RGB to LAB")
263
+ # Convert target RGB to LAB color space (normalized for skimage)
264
+ target_lab = skimage.color.rgb2lab(np.array(target_rgb_tuple).reshape(1, 1, 3) / 255.0).flatten()
265
+
266
+ # Extract L, a, b channels from target_lab
267
+ target_L, target_a, target_b = target_lab[0], target_lab[1], target_lab[2]
268
+
269
+ # Create a mask for all selected objects based on segmentation labels
270
+ # The segmentation_np contains integer labels, not RGB colors directly.
271
+ # We need to map selected object names back to their corresponding integer labels.
272
+ combined_mask_raw = np.zeros(segmentation_np.shape, dtype=bool)
273
+
274
+ # Create a reverse mapping from object name to label
275
+ name_to_label = {}
276
+ # Iterate through unique labels present in the segmentation_np
277
+ palette = np.array(ade_palette())
278
+ unique_labels_in_segmentation = np.unique(segmentation_np)
279
+
280
+
281
+ for label in unique_labels_in_segmentation:
282
+ if label < len(palette): # Ensure label is within palette bounds
283
+ color_rgb_from_palette = tuple(palette[label].tolist())
284
+ item_name = map_colors_rgb(color_rgb_from_palette)
285
+ name_to_label[item_name] = label
286
+
287
+ for selected_name in selected_object_names:
288
+ if selected_name in name_to_label:
289
+ label = name_to_label[selected_name]
290
+ # Create mask for the current object's label
291
+ object_mask = (segmentation_np == label)
292
+ combined_mask_raw = np.logical_or(combined_mask_raw, object_mask)
293
+
294
+ # Convert raw boolean mask to uint8 for PIL Image and blurring
295
+ combined_mask_pil = Image.fromarray(combined_mask_raw.astype(np.uint8) * 255)
296
+
297
+ # Feather the mask for smoother transitions
298
+ # Adjust radius as needed for desired softness
299
+ feathered_mask_pil = combined_mask_pil.filter(ImageFilter.GaussianBlur(radius=5))
300
+ feathered_mask_np = np.array(feathered_mask_pil) / 255.0 # Normalize to 0-1
301
+
302
+ # Apply color change to the original LAB image
303
+ modified_lab = original_lab.copy()
304
+
305
+ # Create a new LAB image with the target color everywhere
306
+ target_lab_full = np.full(original_lab.shape, target_lab)
307
+
308
+ # Blend the original LAB with the target LAB using the feathered mask
309
+ # This blends the color (a, b channels) while trying to retain luminance (L channel)
310
+ # A simple linear interpolation for a and b channels
311
+ modified_lab[:, :, 1] = original_lab[:, :, 1] * (1 - feathered_mask_np) + target_lab_full[:, :, 1] * feathered_mask_np
312
+ modified_lab[:, :, 2] = original_lab[:, :, 2] * (1 - feathered_mask_np) + target_lab_full[:, :, 2] * feathered_mask_np
313
+
314
+ # For luminance (L channel), we'll keep the original luminance to preserve texture.
315
+ # We are not blending L channel here, as it often leads to a "flatter" look.
316
+ # modified_lab[:, :, 0] = original_lab[:, :, 0] * (1 - feathered_mask_np) + target_lab_full[:, :, 0] * feathered_mask_np
317
+
318
+
319
+ # Convert back to RGB and then to PIL Image
320
+ modified_rgb_normalized = skimage.color.lab2rgb(modified_lab)
321
+ modified_rgb_255 = (modified_rgb_normalized * 255).astype(np.uint8)
322
+
323
+ # Post-processing: Apply saturation boost to the masked area
324
+ # Convert the modified image to HSV for saturation adjustment
325
+ modified_hsv = skimage.color.rgb2hsv(modified_rgb_normalized)
326
+
327
+ # Define a saturation boost factor (e.g., 1.3 for 30% boost). Experiment with this value.
328
+ # A higher value will result in more intense colors.
329
+ saturation_boost_factor = 1.3
330
+
331
+ # Boost saturation only in the masked areas, gradually applying the boost based on the feathered mask
332
+ # modified_hsv[:, :, 1] is the saturation channel
333
+ modified_hsv[:, :, 1] = np.clip(
334
+ modified_hsv[:, :, 1] * (1 + feathered_mask_np * (saturation_boost_factor - 1)),
335
+ 0, 1
336
+ )
337
+
338
+ # Convert back to RGB
339
+ final_rgb_normalized = skimage.color.hsv2rgb(modified_hsv)
340
+ final_rgb_255 = (final_rgb_normalized * 255).astype(np.uint8)
341
+
342
+ return Image.fromarray(final_rgb_255)
343
+
344
+ import gradio as gr
345
+ from PIL import Image
346
+ from typing import List, Tuple, Dict, Union
347
+ import numpy as np
348
+
349
+ # --- Global State for this specific app ---
350
+ _current_input_image_pil: Union[Image.Image, None] = None
351
+ _current_segmentation_np: Union[np.ndarray, None] = None
352
+ _current_segment_items_map: Dict[Tuple[int, int, int], str] = {}
353
+ _current_detected_objects: List[str] = [] # Stores human-readable names of detected objects
354
+
355
+ def on_upload_image(input_image_upload: Image.Image) -> Tuple[gr.Image, gr.CheckboxGroup, gr.ColorPicker, gr.Button]:
356
+ """
357
+ Handles the initial image upload, processes it for segmentation,
358
+ and populates the object selection dropdown.
359
+ """
360
+ global _current_input_image_pil, _current_segmentation_np, _current_segment_items_map, _current_detected_objects
361
+
362
+ if input_image_upload is None:
363
+ return (
364
+ gr.update(value=None),
365
+ gr.update(choices=[], value=[], interactive=False),
366
+ gr.update(interactive=False),
367
+ gr.update(interactive=False)
368
+ )
369
+
370
+ try:
371
+ _current_input_image_pil = load_and_preprocess_image(input_image_upload)
372
+
373
+ # Perform semantic segmentation
374
+ segmentation_np, _, detected_objects, segment_items_map = get_segmentation_data(_current_input_image_pil)
375
+
376
+ _current_segmentation_np = segmentation_np
377
+ _current_segment_items_map = segment_items_map
378
+ _current_detected_objects = sorted(list(set(detected_objects))) # Store unique sorted names
379
+
380
+ # Prepare choices for CheckboxGroup: (value, label) pairs.
381
+ # The 'value' will be the object name (string), and 'label' will also be the object name.
382
+ formatted_choices = [(name, name) for name in _current_detected_objects]
383
+
384
+ return (
385
+ gr.update(value=_current_input_image_pil),
386
+ gr.update(choices=formatted_choices, value=[], interactive=True), # No default selection
387
+ gr.update(value="#FF0000", interactive=True), # Default to red
388
+ gr.update(interactive=True)
389
+ )
390
+ except Exception as e:
391
+ print(f"Error during image processing: {e}")
392
+ gr.Warning(f"Failed to process image: {e}")
393
+ return (
394
+ gr.update(value=None),
395
+ gr.update(choices=[], value=[], interactive=False),
396
+ gr.update(interactive=False),
397
+ gr.update(interactive=False)
398
+ )
399
+
400
+ def handle_apply_color_change(
401
+ selected_object_names: List[str], # This will be a list of strings (object names)
402
+ target_color_hex: str # This will be the hex string from the color picker
403
+ ) -> gr.Image:
404
+ """
405
+ Applies the chosen color to the selected objects in the image.
406
+ """
407
+ global _current_input_image_pil, _current_segmentation_np, _current_segment_items_map
408
+
409
+ if _current_input_image_pil is None:
410
+ gr.Error("Please upload an image first.")
411
+ return None
412
+ if not selected_object_names:
413
+ gr.Warning("Please select at least one object to change its color.")
414
+ return _current_input_image_pil # Return original if no selection
415
+ if not target_color_hex:
416
+ gr.Error("Please select a target color.")
417
+ return None
418
+
419
+ print(f"Target color received: {target_color_hex}") # Debugging print statement
420
+
421
+ # Convert RGBA string from Gradio ColorPicker to hex string if necessary
422
+ if target_color_hex.startswith('rgba'):
423
+ try:
424
+ # Extract RGBA values
425
+ rgba_values = target_color_hex.replace('rgba(', '').replace(')', '').split(',')
426
+ r, g, b, a = [float(val.strip()) for val in rgba_values]
427
+
428
+ # Convert to hex
429
+ # Note: Alpha is ignored for hex conversion as hex does not support alpha
430
+ target_hex_color = '#{:02x}{:02x}{:02x}'.format(int(r), int(g), int(b))
431
+ print(f"Converted to hex: {target_hex_color}") # Debugging print statement
432
+ except Exception as e:
433
+ gr.Error(f"Failed to parse color string: {target_color_hex}. Error: {e}")
434
+ return None
435
+ else:
436
+ target_hex_color = target_color_hex
437
+
438
+ print("applu color change strat")
439
+ try:
440
+ output_image = apply_color_change_to_objects(
441
+ original_image=_current_input_image_pil,
442
+ segmentation_np=_current_segmentation_np,
443
+ segment_items_map=_current_segment_items_map,
444
+ selected_object_names=selected_object_names,
445
+ target_hex_color=target_hex_color
446
+ )
447
+ print("applu color change end")
448
+ return output_image
449
+ except Exception as e:
450
+ print(f"Error applying color change: {e}")
451
+ gr.Error(f"Error applying color change: {e}")
452
+ return None
453
+
454
+ def move_output_to_input_for_ui(output_image_to_move: Image.Image) -> gr.Image:
455
+ """
456
+ Copies the output image back to the input image component for further editing.
457
+ """
458
+ global _current_input_image_pil, _current_segmentation_np, _current_segment_items_map, _current_detected_objects
459
+ if output_image_to_move is None:
460
+ gr.Warning("No output image to move.")
461
+ return gr.update()
462
+
463
+ _current_input_image_pil = load_and_preprocess_image(output_image_to_move)
464
+
465
+ # Re-segment the new input image to update detected objects
466
+ segmentation_np, _, detected_objects, segment_items_map = get_segmentation_data(_current_input_image_pil)
467
+ _current_segmentation_np = segmentation_np
468
+ _current_segment_items_map = segment_items_map
469
+ _current_detected_objects = sorted(list(set(detected_objects)))
470
+
471
+ formatted_choices = [(name, name) for name in _current_detected_objects]
472
+
473
+ return gr.update(value=_current_input_image_pil), gr.update(choices=formatted_choices, value=[], interactive=True)
474
+
475
+ with gr.Blocks() as demo:
476
+
477
+ gr.Markdown(
478
+ """
479
+ # Interior Design AI: Object Color Changer
480
+ Upload an image, select objects, and apply a new color while preserving texture.
481
+ """
482
+ )
483
+
484
+ with gr.Row():
485
+ with gr.Column():
486
+ input_image_component = gr.Image(
487
+ type="pil",
488
+ label="Upload an image of your room (PNG/JPG)",
489
+ sources=["upload"],
490
+ height=300,
491
+ interactive=True
492
+ )
493
+
494
+ gr.Markdown("---")
495
+ gr.Markdown("### Select Objects to Re-color")
496
+ object_selection_checkboxes = gr.CheckboxGroup(
497
+ label="Choose objects to change color",
498
+ choices=[], # Populated after image upload
499
+ interactive=False
500
+ )
501
+
502
+ gr.Markdown("---")
503
+ gr.Markdown("### Choose New Color")
504
+ color_picker = gr.ColorPicker(
505
+ label="Select New Color",
506
+ value="#FF0000", # Default to red
507
+ interactive=False
508
+ )
509
+
510
+ apply_color_button = gr.Button("🎨 Apply Color Change", interactive=False)
511
+
512
+ with gr.Column():
513
+ output_image_display = gr.Image(type="pil", label="Resulting Image", height=400)
514
+ move_to_input_btn = gr.Button("🔁 Use Result as New Input", interactive=False)
515
+
516
+ # --- Event Listeners ---
517
+
518
+ input_image_component.upload(
519
+ fn=on_upload_image,
520
+ inputs=[input_image_component],
521
+ outputs=[
522
+ input_image_component,
523
+ object_selection_checkboxes,
524
+ color_picker,
525
+ apply_color_button
526
+ ]
527
+ )
528
+
529
+ apply_color_button.click(
530
+ fn=handle_apply_color_change,
531
+ inputs=[
532
+ object_selection_checkboxes,
533
+ color_picker
534
+ ],
535
+ outputs=[output_image_display]
536
+ )
537
+
538
+ move_to_input_btn.click(
539
+ fn=move_output_to_input_for_ui,
540
+ inputs=[output_image_display],
541
+ outputs=[input_image_component, object_selection_checkboxes] # Update checkboxes too
542
+ )
543
+
544
+ # To run this app: uncomment the line below and run the file (e.g., python app_color_changer.py)
545
+ demo.launch(debug=True)
546
+