Update src/streamlit_app.py
Browse files- src/streamlit_app.py +86 -84
src/streamlit_app.py
CHANGED
|
@@ -55,32 +55,35 @@ class DominantColorDetector:
|
|
| 55 |
self.edge_multiplier = edge_multiplier
|
| 56 |
|
| 57 |
def _preprocess(self, img: Image.Image):
|
| 58 |
-
"""Resize, keep aspect ratio, return float32 pixels."""
|
| 59 |
if img.mode != "RGB":
|
| 60 |
img = img.convert("RGB")
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
edge_frac = 0.10
|
| 74 |
top_height = int(h * edge_frac)
|
| 75 |
bottom_height = int(h * edge_frac)
|
| 76 |
left_width = int(w * edge_frac)
|
| 77 |
right_width = int(w * edge_frac)
|
| 78 |
-
|
| 79 |
for y in range(h):
|
| 80 |
for x in range(w):
|
| 81 |
idx = y * w + x
|
| 82 |
-
if
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
mask[idx] = self.edge_multiplier
|
| 85 |
return mask
|
| 86 |
|
|
@@ -181,75 +184,74 @@ class DominantColorDetector:
|
|
| 181 |
}
|
| 182 |
|
| 183 |
def detect_properties(self, img: Image.Image, include_palette=True):
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
r, g,
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
"
|
| 247 |
-
"dominantColors": {"colors": dominant_colors}
|
| 248 |
-
}
|
| 249 |
}
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
| 253 |
|
| 254 |
# -------------------------------------------------------------------
|
| 255 |
# Helper functions (global)
|
|
|
|
| 55 |
self.edge_multiplier = edge_multiplier
|
| 56 |
|
| 57 |
def _preprocess(self, img: Image.Image):
|
|
|
|
| 58 |
if img.mode != "RGB":
|
| 59 |
img = img.convert("RGB")
|
| 60 |
+
original_w, original_h = img.size
|
| 61 |
+
aspect = original_h / original_w
|
| 62 |
+
new_h = max(1, int(self.resize_dim * aspect))
|
| 63 |
+
img = img.resize((self.resize_dim, new_h), Image.LANCZOS)
|
| 64 |
+
resized_w, resized_h = img.size
|
| 65 |
+
pixels = np.array(img).reshape(-1, 3).astype(np.float32)
|
| 66 |
+
return pixels, (resized_w, resized_h)
|
| 67 |
+
|
| 68 |
+
def _build_edge_mask(self, total_pixels, resized_dims):
|
| 69 |
+
w, h = resized_dims
|
| 70 |
+
mask = np.ones(total_pixels, dtype=np.float32)
|
| 71 |
+
|
| 72 |
edge_frac = 0.10
|
| 73 |
top_height = int(h * edge_frac)
|
| 74 |
bottom_height = int(h * edge_frac)
|
| 75 |
left_width = int(w * edge_frac)
|
| 76 |
right_width = int(w * edge_frac)
|
| 77 |
+
|
| 78 |
for y in range(h):
|
| 79 |
for x in range(w):
|
| 80 |
idx = y * w + x
|
| 81 |
+
if (
|
| 82 |
+
y < top_height
|
| 83 |
+
or y >= h - bottom_height
|
| 84 |
+
or x < left_width
|
| 85 |
+
or x >= w - right_width
|
| 86 |
+
):
|
| 87 |
mask[idx] = self.edge_multiplier
|
| 88 |
return mask
|
| 89 |
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
def detect_properties(self, img: Image.Image, include_palette=True):
|
| 187 |
+
pixels_rgb, (resized_w, resized_h) = self._preprocess(img)
|
| 188 |
+
total_pixels = len(pixels_rgb)
|
| 189 |
+
|
| 190 |
+
# Build edge mask for the full resized image
|
| 191 |
+
edge_mask = self._build_edge_mask(total_pixels, (resized_w, resized_h))
|
| 192 |
+
|
| 193 |
+
max_pixels = 10000
|
| 194 |
+
if total_pixels > max_pixels:
|
| 195 |
+
probs = edge_mask / edge_mask.sum()
|
| 196 |
+
idx = np.random.choice(total_pixels, max_pixels, replace=False, p=probs)
|
| 197 |
+
sampled_pixels = pixels_rgb[idx]
|
| 198 |
+
sampled_mask = edge_mask[idx]
|
| 199 |
+
else:
|
| 200 |
+
sampled_pixels = pixels_rgb
|
| 201 |
+
sampled_mask = edge_mask
|
| 202 |
+
|
| 203 |
+
pixels_lab = self._pixels_to_lab(sampled_pixels)
|
| 204 |
+
kmeans = KMeans(n_clusters=self.num_colors, random_state=42,
|
| 205 |
+
n_init=3, max_iter=100, algorithm="elkan")
|
| 206 |
+
kmeans.fit(pixels_lab)
|
| 207 |
+
centroids_lab = kmeans.cluster_centers_
|
| 208 |
+
labels = kmeans.labels_
|
| 209 |
+
label_counts = Counter(labels)
|
| 210 |
+
total = len(labels)
|
| 211 |
+
centroids_rgb = self._lab_to_rgb(centroids_lab)
|
| 212 |
+
|
| 213 |
+
color_list = []
|
| 214 |
+
for i in range(self.num_colors):
|
| 215 |
+
lab = centroids_lab[i]
|
| 216 |
+
rgb = centroids_rgb[i]
|
| 217 |
+
pf = label_counts[i] / total
|
| 218 |
+
if pf < self.MIN_PIXEL_FRACTION:
|
| 219 |
+
continue
|
| 220 |
+
cluster_mask = labels == i
|
| 221 |
+
edge_frac = np.mean(sampled_mask[cluster_mask] > 1.0)
|
| 222 |
+
r, g, b = int(rgb[0]), int(rgb[1]), int(rgb[2])
|
| 223 |
+
color_list.append({
|
| 224 |
+
"lab": lab,
|
| 225 |
+
"rgb": {"red": r, "green": g, "blue": b},
|
| 226 |
+
"color": rgb_to_hex((r, g, b)),
|
| 227 |
+
"pixelFraction": float(pf),
|
| 228 |
+
"score": self._score_color(lab, pf),
|
| 229 |
+
"chroma": round(lab_chroma(lab), 2),
|
| 230 |
+
"isNearBlack": bool(self._is_near_black(lab)),
|
| 231 |
+
"isNearWhite": bool(self._is_near_white(lab)),
|
| 232 |
+
"isNearGray": bool(self._is_near_gray(lab)),
|
| 233 |
+
"edgeInfluence": round(edge_frac, 3),
|
| 234 |
+
})
|
| 235 |
+
|
| 236 |
+
color_list.sort(key=lambda x: x["score"], reverse=True)
|
| 237 |
+
color_list = self._remove_near_duplicates(color_list)
|
| 238 |
+
|
| 239 |
+
dominant_colors = [
|
| 240 |
+
{k: v for k, v in c.items() if k != "lab"}
|
| 241 |
+
for c in color_list
|
| 242 |
+
]
|
| 243 |
+
for c in dominant_colors:
|
| 244 |
+
c["score"] = round(c["score"], 4)
|
| 245 |
+
c["pixelFraction"] = round(c["pixelFraction"], 4)
|
| 246 |
+
|
| 247 |
+
result = {
|
| 248 |
+
"imagePropertiesAnnotation": {
|
| 249 |
+
"dominantColors": {"colors": dominant_colors}
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
+
}
|
| 252 |
+
if include_palette:
|
| 253 |
+
result["suggestedPalette"] = self._build_adaptive_palette(color_list)
|
| 254 |
+
return result
|
| 255 |
|
| 256 |
# -------------------------------------------------------------------
|
| 257 |
# Helper functions (global)
|