creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
models/composition.py (16861B)
1"""
2Layout
3Original file: https://colab.research.google.com/drive/1NzX6rAQSJkPTpdWWkOowDFlBpypoKl2U
4"""
5
6!pip install opencv-python numpy
7
8import cv2
9import numpy as np
10import os
11from math import hypot
12
13
14def resize_for_fast_processing(img, max_side=640):
15 h, w = img.shape[:2]
16 scale = max_side / max(h, w) if max(h, w) > max_side else 1.0
17 if scale != 1.0:
18 img = cv2.resize(
19 img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA
20 )
21 return img, scale
22
23
24def compute_saliency_gray(img):
25 sal = None
26 try:
27 salience = cv2.saliency.StaticSaliencySpectralResidual_create()
28 success, sal = salience.computeSaliency(img)
29 if not success:
30 sal = None
31 except Exception:
32 sal = None
33 if sal is None:
34 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
35 blur1 = cv2.GaussianBlur(gray, (3, 3), 0)
36 blur2 = cv2.GaussianBlur(gray, (21, 21), 0)
37 sal = cv2.absdiff(blur1, blur2)
38 sal = sal.astype(np.float32) / (sal.max() + 1e-9)
39 else:
40 sal = sal.astype(np.float32)
41 if sal.max() > 0:
42 sal /= sal.max()
43 return sal
44
45
46def saliency_centroid(sal):
47 h, w = sal.shape
48 M = sal.sum()
49 if M <= 1e-6:
50 return (w / 2, h / 2)
51 ys, xs = np.indices(sal.shape)
52 cx = (xs * sal).sum() / M
53 cy = (ys * sal).sum() / M
54 return (float(cx), float(cy))
55
56
57def detect_lines(gray, canny_thresh1=50, canny_thresh2=150, hough_thresh=50):
58 edges = cv2.Canny(gray, canny_thresh1, canny_thresh2)
59 lines = cv2.HoughLinesP(
60 edges,
61 rho=1,
62 theta=np.pi / 180,
63 threshold=hough_thresh,
64 minLineLength=min(gray.shape) // 10,
65 maxLineGap=10,
66 )
67 if lines is None:
68 return edges, []
69 lines = [tuple(l[0]) for l in lines]
70 return edges, lines
71
72
73def project_sal_along_axis(sal, axis=0, smooth_k=15):
74 proj = sal.sum(axis=1 - axis) if axis == 0 else sal.sum(axis=0)
75 k = max(3, smooth_k)
76 kernel = np.ones(k) / k
77 proj_s = np.convolve(proj, kernel, mode="same")
78 if proj_s.max() > 0:
79 proj_s = proj_s / proj_s.max()
80 return proj_s
81
82
83def count_saliency_peaks_along_y(sal, min_sep_fraction=0.1, threshold=0.2):
84 h, w = sal.shape
85 proj = project_sal_along_axis(sal, axis=0, smooth_k=max(5, int(h * 0.03)))
86 peaks = []
87 for i in range(1, len(proj) - 1):
88 if proj[i] > proj[i - 1] and proj[i] > proj[i + 1] and proj[i] > threshold:
89 peaks.append(i)
90 min_sep = int(min_sep_fraction * h)
91 filtered = []
92 for p in peaks:
93 if not filtered or p - filtered[-1] >= min_sep:
94 filtered.append(p)
95 return len(filtered), filtered
96
97
98def band_saliency_fractions(sal, bands=3):
99 h, w = sal.shape
100 sums = []
101 total = sal.sum() + 1e-9
102 for i in range(bands):
103 band = sal[:, i * w // bands : (i + 1) * w // bands]
104 sums.append(band.sum() / total)
105 return sums
106
107
108def sal_bbox_and_margins(sal, thresh=0.3):
109 mask = sal > thresh
110 if mask.sum() == 0:
111 return (0, 0, 0, 0), None
112 ys, xs = np.where(mask)
113 minx, maxx = int(xs.min()), int(xs.max())
114 miny, maxy = int(ys.min()), int(ys.max())
115 return (minx, miny, maxx, maxy), mask
116
117
118def component_bboxes_from_mask(mask):
119 mask_u8 = mask.astype(np.uint8) * 255
120 contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
121 bboxes = []
122 for c in contours:
123 x, y, w, h = cv2.boundingRect(c)
124 bboxes.append((x, y, x + w - 1, y + h - 1))
125 return bboxes
126
127
128def bbox_laplacian_variance(gray, bbox):
129 x1, y1, x2, y2 = bbox
130 roi = gray[y1 : y2 + 1, x1 : x2 + 1]
131 if roi.size == 0:
132 return 0.0
133 lap = cv2.Laplacian(roi, cv2.CV_64F)
134 return float(np.var(lap))
135
136
137def _proj_and_peaks(sal, axis=0, smooth_k=15):
138 proj = sal.sum(axis=1 - axis) if axis == 0 else sal.sum(axis=0)
139 k = max(3, smooth_k)
140 kernel = np.ones(k) / k
141 proj_s = np.convolve(proj, kernel, mode="same")
142 if proj_s.max() > 0:
143 proj_s = proj_s / proj_s.max()
144 return proj_s
145
146
147def vertical_stack_score(sal, lines, min_peaks=2):
148 h, w = sal.shape
149 proj = _proj_and_peaks(sal, axis=0, smooth_k=max(5, int(h * 0.03)))
150 peaks = [
151 i
152 for i in range(1, len(proj) - 1)
153 if proj[i] > proj[i - 1] and proj[i] > proj[i + 1] and proj[i] > 0.18
154 ]
155 if len(peaks) == 0:
156 return 0.0, {"peaks": 0}
157 avg_peak = float(np.mean([proj[p] for p in peaks]))
158 peak_count_score = min(1.0, len(peaks) / max(1.0, min_peaks))
159 peak_prom_score = avg_peak
160 h_lines = 0
161 for x1, y1, x2, y2 in lines:
162 dy = abs(y2 - y1)
163 dx = abs(x2 - x1) + 1e-9
164 slope = dy / dx
165 if slope < 0.3 and abs(x2 - x1) > 0.6 * w:
166 h_lines += 1
167 sep_score = min(1.0, h_lines / 2.0)
168 span_ok = 0
169 for p in peaks:
170 row = sal[max(0, p - 2) : min(h, p + 3), :]
171 if row.sum() <= 1e-9:
172 continue
173 col_sum = row.sum(axis=0)
174 frac_nonzero = (col_sum > (col_sum.max() * 0.05)).sum() / float(w)
175 if frac_nonzero > 0.65:
176 span_ok += 1
177 span_score = min(1.0, span_ok / max(1.0, len(peaks)))
178 score = (
179 0.45 * peak_count_score
180 + 0.25 * peak_prom_score
181 + 0.15 * sep_score
182 + 0.15 * span_score
183 )
184 return float(np.clip(score, 0.0, 1.0)), {
185 "peaks": len(peaks),
186 "avg_peak": avg_peak,
187 "h_lines": h_lines,
188 "span_ok": span_ok,
189 }
190
191
192def triptych_score(sal, lines):
193 h, w = sal.shape
194 total = sal.sum() + 1e-9
195 bands = [sal[:, i * w // 3 : (i + 1) * w // 3].sum() / total for i in range(3)]
196 mn, mx = min(bands), max(bands)
197 balance = 1.0 - (mx - mn)
198 balance = np.clip(balance, 0.0, 1.0)
199 v_hits = 0
200 for x1, y1, x2, y2 in lines:
201 dx = abs(x2 - x1) + 1e-9
202 dy = abs(y2 - y1)
203 slope = dy / dx if dx > 1 else 1e9
204 if slope > 3 and dy > 0.6 * h:
205 cx = (x1 + x2) / 2
206 if abs(cx - w / 3) < 0.08 * w or abs(cx - 2 * w / 3) < 0.08 * w:
207 v_hits += 1
208 line_score = np.tanh(v_hits / 2.0)
209 band_floor = min(1.0, mn / 0.15)
210 score = 0.55 * balance + 0.25 * line_score + 0.20 * band_floor
211 return float(np.clip(score, 0.0, 1.0)), {"band_fracs": bands, "v_line_hits": v_hits}
212
213
214def full_bleed_score(
215 sal,
216 img,
217 edge_margin_frac=0.03,
218 edge_sal_thresh=0.25,
219 sal_bbox_thresh=0.2,
220 white_thresh=245,
221 white_frac_thresh=0.80,
222 edge_sal_frac_thresh=0.50,
223 bbox_touch_required=0.75,
224):
225 """
226 Binary full-bleed detector (returns 0 or 1, plus info).
227 - Rejects as full-bleed if a white margin/border is detected.
228 - Uses saliency near edges and whether saliency bbox touches edges.
229 """
230 h, w = sal.shape
231 m = max(1, int(min(h, w) * edge_margin_frac))
232
233 mask_edge = np.zeros_like(sal, dtype=bool)
234 mask_edge[:m, :] = True
235 mask_edge[-m:, :] = True
236 mask_edge[:, :m] = True
237 mask_edge[:, -m:] = True
238 edge_sal_frac = float((sal[mask_edge] > edge_sal_thresh).sum()) / (
239 mask_edge.sum() + 1e-9
240 )
241
242 mask2 = sal > sal_bbox_thresh
243 if mask2.sum() == 0:
244 bbox_touch = 0.0
245 else:
246 ys, xs = np.where(mask2)
247 minx, maxx = int(xs.min()), int(xs.max())
248 miny, maxy = int(ys.min()), int(ys.max())
249 touches = (
250 int(minx <= 1) + int(maxx >= w - 2) + int(miny <= 1) + int(maxy >= h - 2)
251 )
252 bbox_touch = touches / 4.0
253
254 band_top = img[:m, :, :] if m > 0 else np.zeros((0, w, 3), dtype=img.dtype)
255 band_bottom = img[-m:, :, :] if m > 0 else np.zeros((0, w, 3), dtype=img.dtype)
256 band_left = img[:, :m, :] if m > 0 else np.zeros((h, 0, 3), dtype=img.dtype)
257 band_right = img[:, -m:, :] if m > 0 else np.zeros((h, 0, 3), dtype=img.dtype)
258
259 border_pixels = np.concatenate(
260 [
261 band_top.reshape(-1, 3),
262 band_bottom.reshape(-1, 3),
263 band_left.reshape(-1, 3),
264 band_right.reshape(-1, 3),
265 ],
266 axis=0,
267 )
268 if border_pixels.size == 0:
269 white_border_frac = 0.0
270 else:
271 white_mask = np.all(border_pixels >= white_thresh, axis=1)
272 white_border_frac = float(white_mask.sum()) / float(border_pixels.shape[0])
273
274 border_std = float(np.std(border_pixels)) if border_pixels.size > 0 else 0.0
275 border_uniform = border_std < 6.0 # small std => uniform border
276
277 cond_edge_sal = edge_sal_frac >= edge_sal_frac_thresh
278 cond_bbox_touch = bbox_touch >= bbox_touch_required
279
280 is_full_bleed = (
281 (cond_edge_sal or cond_bbox_touch)
282 and (white_border_frac < white_frac_thresh)
283 and (not (border_uniform and white_border_frac > 0.25))
284 )
285
286 score = 1 if is_full_bleed else 0
287
288 info = {
289 "edge_margin_px": m,
290 "edge_sal_frac": edge_sal_frac,
291 "edge_sal_thresh": edge_sal_thresh,
292 "cond_edge_sal": cond_edge_sal,
293 "bbox_touch": bbox_touch,
294 "bbox_touch_required": bbox_touch_required,
295 "white_border_frac": white_border_frac,
296 "white_thresh": white_thresh,
297 "white_frac_thresh": white_frac_thresh,
298 "border_std": border_std,
299 "border_uniform": border_uniform,
300 "decision_full_bleed": bool(is_full_bleed),
301 }
302 return score, info
303
304
305def tight_crop_score(sal, gray, sal_thresh=0.1):
306 h, w = sal.shape
307 mask = sal > sal_thresh
308 if mask.sum() == 0:
309 return 0.0, {"area_frac": 0.0, "tightness": 1.0}
310 ys, xs = np.where(mask)
311 minx, maxx = int(xs.min()), int(xs.max())
312 miny, maxy = int(ys.min()), int(ys.max())
313 bbox_area = (maxx - minx + 1) * (maxy - miny + 1)
314 area_frac = bbox_area / float(w * h)
315 left = minx
316 right = w - 1 - maxx
317 top = miny
318 bottom = h - 1 - maxy
319 tightness = min(left, right, top, bottom) / float(min(w, h) + 1e-9)
320 area_score = np.clip((area_frac - 0.15) / (0.6 - 0.15), 0.0, 1.0)
321 tight_score = 1.0 - np.clip(tightness / 0.08, 0.0, 1.0)
322 score = 0.6 * area_score + 0.4 * tight_score
323 return float(np.clip(score, 0.0, 1.0)), {
324 "area_frac": area_frac,
325 "tightness": tightness,
326 "margins": (left, right, top, bottom),
327 }
328
329
330def layered_foreground_score(sal, gray):
331 mask = (sal > 0.25).astype(np.uint8)
332 kernel = np.ones((5, 5), np.uint8)
333 mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
334 mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
335 contours, _ = cv2.findContours(
336 (mask * 255).astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
337 )
338 bboxes = []
339 sharpness = []
340 for c in contours:
341 x, y, w, h = cv2.boundingRect(c)
342 if w * h < 0.005 * sal.size:
343 continue
344 bboxes.append((x, y, x + w - 1, y + h - 1))
345 roi = gray[y : y + h, x : x + w]
346 lap = cv2.Laplacian(roi, cv2.CV_64F)
347 sharpness.append(float(np.var(lap)))
348 n = len(bboxes)
349 if n < 2:
350 return 0.0, {"num_components": n}
351 s = np.array(sharpness) + 1e-9
352 s_norm = (s - s.mean()) / (s.std() + 1e-9)
353 sharp_std = float(np.std(s_norm))
354 overlaps = 0
355 for i in range(len(bboxes)):
356 for j in range(i + 1, len(bboxes)):
357 a = bboxes[i]
358 b = bboxes[j]
359 ix1 = max(a[0], b[0])
360 iy1 = max(a[1], b[1])
361 ix2 = min(a[2], b[2])
362 iy2 = min(a[3], b[3])
363 if ix2 >= ix1 and iy2 >= iy1:
364 inter = (ix2 - ix1 + 1) * (iy2 - iy1 + 1)
365 area_a = (a[2] - a[0] + 1) * (a[3] - a[1] + 1)
366 if inter > 0.05 * area_a:
367 overlaps += 1
368 comp_score = np.clip((n - 1) / 4.0, 0, 1)
369 sharp_score = np.tanh(sharp_std)
370 overlap_score = np.tanh(overlaps / 2.0)
371 score = 0.45 * comp_score + 0.35 * sharp_score + 0.20 * overlap_score
372 return float(np.clip(score, 0.0, 1.0)), {
373 "num_components": n,
374 "sharp_std": sharp_std,
375 "overlaps": overlaps,
376 }
377
378
379def balance_score(img_bgr, sal):
380 img_lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB).astype(np.float32)
381 L = img_lab[:, :, 0] / 255.0
382 img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32)
383 S = img_hsv[:, :, 1] / 255.0
384 weight = sal * (L + 0.5 * S)
385 M = weight.sum()
386 h, w = sal.shape
387 if M <= 1e-6:
388 return 0.0
389 ys, xs = np.indices(sal.shape)
390 cx = (xs * weight).sum() / M
391 cy = (ys * weight).sum() / M
392 dx = abs(cx - w / 2) / (w / 2)
393 dy = abs(cy - h / 2) / (h / 2)
394 dist = np.sqrt(dx * dx + dy * dy) / np.sqrt(2)
395 return float(max(0.0, 1.0 - dist * 1.4))
396
397
398def symmetry_score(img_gray):
399 h, w = img_gray.shape
400 left = img_gray[:, : w // 2]
401 right = img_gray[:, w - w // 2 :]
402 right_flipped = cv2.flip(right, 1)
403 if left.shape != right_flipped.shape:
404 right_flipped = cv2.resize(right_flipped, (left.shape[1], left.shape[0]))
405 leftf = left.astype(np.float32)
406 rightf = right_flipped.astype(np.float32)
407 if leftf.std() < 1e-3 or rightf.std() < 1e-3:
408 return 0.0
409 corr = np.corrcoef(leftf.flatten(), rightf.flatten())[0, 1]
410 return float(max(0.0, min(1.0, (corr + 1) / 2)))
411
412
413def thirds_score(sal, centroid):
414 h, w = sal.shape
415 cx, cy = centroid
416 thirds_pts = [
417 (w / 3, h / 3),
418 (w / 3, 2 * h / 3),
419 (2 * w / 3, h / 3),
420 (2 * w / 3, 2 * h / 3),
421 ]
422 dists = [hypot(cx - x, cy - y) for (x, y) in thirds_pts]
423 maxd = hypot(w, h)
424 score = 1.0 - min(dists) / maxd * 1.2
425 vline_dist = min(abs(cx - w / 3), abs(cx - 2 * w / 3)) / w
426 hline_dist = min(abs(cy - h / 3), abs(cy - 2 * h / 3)) / h
427 line_score = 1.0 - min(vline_dist, hline_dist)
428 return float(max(0.0, min(1.0, 0.6 * score + 0.4 * line_score)))
429
430
431def golden_ratio_score(sal, centroid):
432 h, w = sal.shape
433 gx = [0.382, 0.618]
434 gy = [0.382, 0.618]
435 cx, cy = centroid
436 pts = [(g * w, g2 * h) for g in gx for g2 in gy]
437 dists = [hypot(cx - x, cy - y) for x, y in pts]
438 maxd = hypot(w, h)
439 return float(max(0.0, min(1.0, 1.0 - min(dists) / (maxd * 0.9))))
440
441
442def negative_space_score(sal):
443 low = (sal < 0.15).sum() / sal.size
444 return float(min(1.0, low))
445
446
447def center_score(sal, centroid):
448 h, w = sal.shape
449 cx, cy = centroid
450 dx = abs(cx - w / 2) / (w / 2)
451 dy = abs(cy - h / 2) / (h / 2)
452 dist = np.sqrt(dx * dx + dy * dy) / np.sqrt(2)
453 return float(max(0.0, 1.0 - dist * 1.2))
454
455
456def analyze_single_image(img_path):
457 if not os.path.exists(img_path):
458 print(f"Error: File not found at {img_path}")
459 return
460
461 img_full = cv2.imread(img_path)
462 if img_full is None:
463 print(f"Error: Could not load image. Check format.")
464 return
465
466 print(f"Analyzing: {os.path.basename(img_path)}...")
467
468 img, scale = resize_for_fast_processing(img_full, max_side=640)
469 h, w = img.shape[:2]
470 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
471
472 sal = compute_saliency_gray(img)
473 if sal.shape != gray.shape:
474 sal = cv2.resize(sal, (w, h), interpolation=cv2.INTER_LINEAR)
475 centroid = saliency_centroid(sal)
476
477 edges, lines = detect_lines(gray)
478 _, thr = cv2.threshold(
479 (gray).astype(np.uint8), 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV
480 )
481 contours, _ = cv2.findContours(thr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
482
483 composition_scores = {
484 "Rule of Thirds": thirds_score(sal, centroid),
485 "Golden Ratio": golden_ratio_score(sal, centroid),
486 "Symmetry": symmetry_score(gray),
487 "Negative Space": negative_space_score(sal),
488 "Center Composition": center_score(sal, centroid),
489 }
490
491 v_score, v_info = vertical_stack_score(sal, lines)
492 t_score, t_info = triptych_score(sal, lines)
493 fb_score, fb_info = full_bleed_score(sal, img)
494 tc_score, tc_info = tight_crop_score(sal, gray)
495 lf_score, lf_info = layered_foreground_score(sal, gray)
496
497 composition_scores["Vertical Stack"] = v_score
498 composition_scores["Triptych"] = t_score
499 composition_scores["Full Bleed"] = fb_score
500 composition_scores["Tight Crop"] = tc_score
501 composition_scores["Layered Foreground"] = lf_score
502
503 print("-" * 40)
504 print(f"Top Composition Scores:")
505 sorted_comp = sorted(composition_scores.items(), key=lambda x: x[1], reverse=True)
506 for k, v in sorted_comp[:10]:
507 print(f" - {k}: {v:.2f}")
508
509 print("\n All Composition Scores:")
510 for k, v in sorted_comp:
511 print(f" {k:<25} : {v:.3f}")
512 print("-" * 40)
513
514 diagnostics = {
515 "Vertical Stack_info": v_info,
516 "Triptych_info": t_info,
517 "Full Bleed_info": fb_info,
518 "Tight Crop_info": tc_info,
519 "Layered Foreground_info": lf_info,
520 }
521
522
523# Runner
524
525if __name__ == "__main__":
526 target_image_path = "/content/test.png"
527 analyze_single_image(target_image_path)