commit 7145c8c0003d72ebfb4e78f347050d614af343a7
parent 2e1dd4c208e31daaedb7f20e3a66f835dd40ba27
Author: aditya-samal <samaladitya2004@gmail.com>
Date: Sat, 29 Nov 2025 12:25:45 +0530
Changed Layout Model
Diffstat:
5 files changed, 333 insertions(+), 123 deletions(-)
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
@@ -5,7 +5,7 @@ buildscript {
maven { url = uri("https://chaquo.com/maven") }
}
dependencies {
- classpath("com.android.tools.build:gradle:8.7.0")
+ classpath("com.android.tools.build:gradle:8.9.1")
classpath("com.chaquo.python:gradle:15.0.1")
}
}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
@@ -1 +1,12 @@
-keep class ai.onnxruntime.** { *; }
+
+# Google ML Kit Text Recognition - Keep all text recognizer options
+-keep class com.google.mlkit.vision.text.** { *; }
+-dontwarn com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions$Builder
+-dontwarn com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions
+-dontwarn com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions$Builder
+-dontwarn com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions
+-dontwarn com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions$Builder
+-dontwarn com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions
+-dontwarn com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions$Builder
+-dontwarn com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions
+\ No newline at end of file
diff --git a/android/app/src/main/python/analyze_layout.py b/android/app/src/main/python/analyze_layout.py
@@ -4,6 +4,19 @@ import os
import json
from math import hypot
+# Custom JSON encoder for numpy types
+class NumpyEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, (np.integer, int)):
+ return int(obj)
+ elif isinstance(obj, (np.floating, float)):
+ return float(obj)
+ elif isinstance(obj, np.ndarray):
+ return obj.tolist()
+ elif isinstance(obj, np.bool_):
+ return bool(obj)
+ return super(NumpyEncoder, self).default(obj)
+
# ==========================================
# 1. HELPER FUNCTIONS
# ==========================================
@@ -24,7 +37,6 @@ def compute_saliency_gray(img):
except Exception:
sal = None
if sal is None:
- # Fallback: Simple Gaussian Difference
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur1 = cv2.GaussianBlur(gray, (3, 3), 0)
blur2 = cv2.GaussianBlur(gray, (21, 21), 0)
@@ -52,14 +64,262 @@ def detect_lines(gray, canny_thresh1=50, canny_thresh2=150, hough_thresh=50):
lines = [tuple(l[0]) for l in lines]
return edges, lines
+def project_sal_along_axis(sal, axis=0, smooth_k=15):
+ proj = sal.sum(axis=1-axis) if axis == 0 else sal.sum(axis=0)
+ k = max(3, smooth_k)
+ kernel = np.ones(k) / k
+ proj_s = np.convolve(proj, kernel, mode='same')
+ if proj_s.max() > 0:
+ proj_s = proj_s / proj_s.max()
+ return proj_s
+
+def count_saliency_peaks_along_y(sal, min_sep_fraction=0.1, threshold=0.2):
+ h, w = sal.shape
+ proj = project_sal_along_axis(sal, axis=0, smooth_k=max(5, int(h*0.03)))
+ peaks = []
+ for i in range(1, len(proj)-1):
+ if proj[i] > proj[i-1] and proj[i] > proj[i+1] and proj[i] > threshold:
+ peaks.append(i)
+ min_sep = int(min_sep_fraction * h)
+ filtered = []
+ for p in peaks:
+ if not filtered or p - filtered[-1] >= min_sep:
+ filtered.append(p)
+ return len(filtered), filtered
+
+def band_saliency_fractions(sal, bands=3):
+ h, w = sal.shape
+ sums = []
+ total = sal.sum() + 1e-9
+ for i in range(bands):
+ band = sal[:, i*w//bands:(i+1)*w//bands]
+ sums.append(band.sum() / total)
+ return sums
+
+def sal_bbox_and_margins(sal, thresh=0.3):
+ mask = sal > thresh
+ if mask.sum() == 0:
+ return (0,0,0,0), None
+ ys, xs = np.where(mask)
+ minx, maxx = int(xs.min()), int(xs.max())
+ miny, maxy = int(ys.min()), int(ys.max())
+ return (minx, miny, maxx, maxy), mask
+
+def component_bboxes_from_mask(mask):
+ mask_u8 = (mask.astype(np.uint8) * 255)
+ contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+ bboxes = []
+ for c in contours:
+ x,y,w,h = cv2.boundingRect(c)
+ bboxes.append((x,y,x+w-1,y+h-1))
+ return bboxes
+
+def bbox_laplacian_variance(gray, bbox):
+ x1,y1,x2,y2 = bbox
+ roi = gray[y1:y2+1, x1:x2+1]
+ if roi.size == 0: return 0.0
+ lap = cv2.Laplacian(roi, cv2.CV_64F)
+ return float(np.var(lap))
+
# ==========================================
-# 2. SCORING FUNCTIONS
+# 2. ROBUST CONTINUOUS LAYOUT SCORERS
# ==========================================
-def saturation_score(img_bgr):
- hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32)
- sat = hsv[:, :, 1] / 255.0
- return float(np.mean(sat))
+def _proj_and_peaks(sal, axis=0, smooth_k=15):
+ proj = sal.sum(axis=1-axis) if axis==0 else sal.sum(axis=0)
+ k = max(3, smooth_k)
+ kernel = np.ones(k) / k
+ proj_s = np.convolve(proj, kernel, mode='same')
+ if proj_s.max() > 0: proj_s = proj_s / proj_s.max()
+ return proj_s
+
+def vertical_stack_score(sal, lines, min_peaks=2):
+ h,w = sal.shape
+ proj = _proj_and_peaks(sal, axis=0, smooth_k=max(5, int(h*0.03)))
+ peaks = [i for i in range(1,len(proj)-1) if proj[i]>proj[i-1] and proj[i]>proj[i+1] and proj[i]>0.18]
+ if len(peaks)==0:
+ return 0.0, {'peaks':0}
+ avg_peak = float(np.mean([proj[p] for p in peaks]))
+ peak_count_score = min(1.0, len(peaks) / max(1.0, min_peaks))
+ peak_prom_score = avg_peak
+ h_lines = 0
+ for (x1,y1,x2,y2) in lines:
+ dy = abs(y2-y1); dx = abs(x2-x1)+1e-9
+ slope = dy/dx
+ if slope < 0.3 and abs(x2-x1) > 0.6*w:
+ h_lines += 1
+ sep_score = min(1.0, h_lines / 2.0)
+ span_ok = 0
+ for p in peaks:
+ row = sal[max(0,p-2):min(h,p+3), :]
+ if row.sum()<=1e-9: continue
+ col_sum = row.sum(axis=0)
+ frac_nonzero = (col_sum > (col_sum.max()*0.05)).sum() / float(w)
+ if frac_nonzero > 0.65:
+ span_ok += 1
+ span_score = min(1.0, span_ok / max(1.0, len(peaks)))
+ score = 0.45*peak_count_score + 0.25*peak_prom_score + 0.15*sep_score + 0.15*span_score
+ return float(np.clip(score,0.0,1.0)), {'peaks': len(peaks), 'avg_peak': avg_peak, 'h_lines': h_lines, 'span_ok': span_ok}
+
+def triptych_score(sal, lines):
+ h,w = sal.shape
+ total = sal.sum() + 1e-9
+ bands = [sal[:, i*w//3:(i+1)*w//3].sum() / total for i in range(3)]
+ mn, mx = min(bands), max(bands)
+ balance = 1.0 - (mx - mn)
+ balance = np.clip(balance, 0.0, 1.0)
+ v_hits = 0
+ for (x1,y1,x2,y2) in lines:
+ dx = abs(x2-x1)+1e-9; dy = abs(y2-y1)
+ slope = dy / dx if dx>1 else 1e9
+ if slope > 3 and dy > 0.6*h:
+ cx = (x1+x2)/2
+ if abs(cx - w/3) < 0.08*w or abs(cx - 2*w/3) < 0.08*w:
+ v_hits += 1
+ line_score = np.tanh(v_hits/2.0)
+ band_floor = min(1.0, mn / 0.15)
+ score = 0.55*balance + 0.25*line_score + 0.20*band_floor
+ return float(np.clip(score,0.0,1.0)), {'band_fracs': bands, 'v_line_hits': v_hits}
+
+def full_bleed_score(sal, img, edge_margin_frac=0.03,
+ edge_sal_thresh=0.25,
+ sal_bbox_thresh=0.2,
+ white_thresh=245,
+ white_frac_thresh=0.80,
+ edge_sal_frac_thresh=0.50,
+ bbox_touch_required=0.75):
+ """
+ Binary full-bleed detector (returns 0 or 1, plus info).
+ - Rejects as full-bleed if a white margin/border is detected.
+ - Uses saliency near edges and whether saliency bbox touches edges.
+ """
+ h, w = sal.shape
+ m = max(1, int(min(h, w) * edge_margin_frac))
+
+ # 1) fraction of edge band pixels that are salient (sal > edge_sal_thresh)
+ mask_edge = np.zeros_like(sal, dtype=bool)
+ mask_edge[:m, :] = True
+ mask_edge[-m:, :] = True
+ mask_edge[:, :m] = True
+ mask_edge[:, -m:] = True
+ edge_sal_frac = float((sal[mask_edge] > edge_sal_thresh).sum()) / (mask_edge.sum() + 1e-9)
+
+ # 2) compute saliency bbox touch (based on sal > sal_bbox_thresh)
+ mask2 = sal > sal_bbox_thresh
+ if mask2.sum() == 0:
+ bbox_touch = 0.0
+ else:
+ ys, xs = np.where(mask2)
+ minx, maxx = int(xs.min()), int(xs.max())
+ miny, maxy = int(ys.min()), int(ys.max())
+ touches = int(minx <= 1) + int(maxx >= w - 2) + int(miny <= 1) + int(maxy >= h - 2)
+ bbox_touch = touches / 4.0
+
+ # 3) check for white margin/border
+ # sample the same border band from the color image and compute fraction of nearly-white pixels
+ # ensure img is uint8 BGR
+ band_top = img[:m, :, :] if m > 0 else np.zeros((0, w, 3), dtype=img.dtype)
+ band_bottom = img[-m:, :, :] if m > 0 else np.zeros((0, w, 3), dtype=img.dtype)
+ band_left = img[:, :m, :] if m > 0 else np.zeros((h, 0, 3), dtype=img.dtype)
+ band_right = img[:, -m:, :] if m > 0 else np.zeros((h, 0, 3), dtype=img.dtype)
+
+ # stack all border samples into one array (may contain duplicates at corners but that's fine)
+ border_pixels = np.concatenate([band_top.reshape(-1, 3),
+ band_bottom.reshape(-1, 3),
+ band_left.reshape(-1, 3),
+ band_right.reshape(-1, 3)], axis=0)
+ if border_pixels.size == 0:
+ white_border_frac = 0.0
+ else:
+ # white if all channels >= white_thresh
+ white_mask = np.all(border_pixels >= white_thresh, axis=1)
+ white_border_frac = float(white_mask.sum()) / float(border_pixels.shape[0])
+
+ # Also detect a uniform border (low std) which sometimes signals a margin even if not white
+ border_std = float(np.std(border_pixels)) if border_pixels.size > 0 else 0.0
+ border_uniform = border_std < 6.0 # small std => uniform border
+
+ # 4) Decide binary score
+ # Conditions favoring full-bleed:
+ cond_edge_sal = edge_sal_frac >= edge_sal_frac_thresh
+ cond_bbox_touch = bbox_touch >= bbox_touch_required # e.g. >=0.75 means touching 3 or 4 edges
+
+ # Final logic: require (edge saliency OR bbox touch) AND NOT white border
+ is_full_bleed = (cond_edge_sal or cond_bbox_touch) and (white_border_frac < white_frac_thresh) and (not (border_uniform and white_border_frac > 0.25))
+
+ score = 1 if is_full_bleed else 0
+
+ info = {
+ 'edge_margin_px': m,
+ 'edge_sal_frac': edge_sal_frac,
+ 'edge_sal_thresh': edge_sal_thresh,
+ 'cond_edge_sal': cond_edge_sal,
+ 'bbox_touch': bbox_touch,
+ 'bbox_touch_required': bbox_touch_required,
+ 'white_border_frac': white_border_frac,
+ 'white_thresh': white_thresh,
+ 'white_frac_thresh': white_frac_thresh,
+ 'border_std': border_std,
+ 'border_uniform': border_uniform,
+ 'decision_full_bleed': bool(is_full_bleed)
+ }
+ return score, info
+
+
+def tight_crop_score(sal, gray, sal_thresh=0.1):
+ h,w = sal.shape
+ mask = sal > sal_thresh
+ if mask.sum()==0:
+ return 0.0, {'area_frac':0.0, 'tightness':1.0}
+ ys,xs = np.where(mask)
+ minx,maxx = int(xs.min()), int(xs.max())
+ miny,maxy = int(ys.min()), int(ys.max())
+ bbox_area = (maxx-minx+1)*(maxy-miny+1)
+ area_frac = bbox_area / float(w*h)
+ left = minx; right = w-1-maxx; top = miny; bottom = h-1-maxy
+ tightness = min(left,right,top,bottom) / float(min(w,h)+1e-9)
+ area_score = np.clip((area_frac - 0.15) / (0.6 - 0.15), 0.0, 1.0)
+ tight_score = 1.0 - np.clip(tightness / 0.08, 0.0, 1.0)
+ score = 0.6*area_score + 0.4*tight_score
+ return float(np.clip(score,0.0,1.0)), {'area_frac': area_frac, 'tightness': tightness, 'margins': (left,right,top,bottom)}
+
+def layered_foreground_score(sal, gray):
+ mask = (sal > 0.25).astype(np.uint8)
+ kernel = np.ones((5,5), np.uint8)
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
+ contours, _ = cv2.findContours((mask*255).astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+ bboxes=[]; sharpness=[]
+ for c in contours:
+ x,y,w,h = cv2.boundingRect(c)
+ if w*h < 0.005*sal.size: continue
+ bboxes.append((x,y,x+w-1,y+h-1))
+ roi = gray[y:y+h, x:x+w]
+ lap = cv2.Laplacian(roi, cv2.CV_64F)
+ sharpness.append(float(np.var(lap)))
+ n = len(bboxes)
+ if n < 2:
+ return 0.0, {'num_components': n}
+ s = np.array(sharpness) + 1e-9
+ s_norm = (s - s.mean()) / (s.std()+1e-9)
+ sharp_std = float(np.std(s_norm))
+ overlaps = 0
+ for i in range(len(bboxes)):
+ for j in range(i+1, len(bboxes)):
+ a=bboxes[i]; b=bboxes[j]
+ ix1 = max(a[0], b[0]); iy1 = max(a[1], b[1])
+ ix2 = min(a[2], b[2]); iy2 = min(a[3], b[3])
+ if ix2 >= ix1 and iy2 >= iy1:
+ inter = (ix2-ix1+1)*(iy2-iy1+1)
+ area_a = (a[2]-a[0]+1)*(a[3]-a[1]+1)
+ if inter > 0.05*area_a:
+ overlaps += 1
+ comp_score = np.clip((n-1)/4.0, 0, 1)
+ sharp_score = np.tanh(sharp_std)
+ overlap_score = np.tanh(overlaps/2.0)
+ score = 0.45*comp_score + 0.35*sharp_score + 0.20*overlap_score
+ return float(np.clip(score,0.0,1.0)), {'num_components': n, 'sharp_std': sharp_std, 'overlaps': overlaps}
+
def balance_score(img_bgr, sal):
img_lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB).astype(np.float32)
@@ -78,50 +338,6 @@ def balance_score(img_bgr, sal):
dist = np.sqrt(dx*dx + dy*dy) / np.sqrt(2)
return float(max(0.0, 1.0 - dist*1.4))
-def depth_score(img_gray, sal):
- mask = sal > 0.3
- if mask.sum() == 0: return 0.0
- ys,xs = np.where(mask)
- minx, maxx = xs.min(), xs.max()
- miny, maxy = ys.min(), ys.max()
- lap = cv2.Laplacian(img_gray, cv2.CV_64F)
- inside = lap[miny:maxy+1, minx:maxx+1]
- outside_mask = np.ones_like(img_gray, dtype=bool)
- outside_mask[miny:maxy+1, minx:maxx+1] = False
- outside = lap[outside_mask]
- var_in = float(np.var(inside)) if inside.size>0 else 0.0
- var_out = float(np.var(outside)) if outside.size>0 else 0.0
- diff = var_in - var_out
- score = (diff / (abs(var_out) + 1e-6)) if var_out>1e-6 else (1.0 if diff>0 else 0.0)
- score = np.tanh(score)
- return float(max(0.0, min(1.0, (score+1)/2)))
-
-def diagonals_triangles_score(lines, contours, centroid, img_shape):
- if not lines:
- diag_strength = 0.0
- else:
- angles = []
- for (x1,y1,x2,y2) in lines:
- dx = x2 - x1; dy = y2 - y1
- ang = abs(np.arctan2(dy, dx))
- ang = min(ang, np.pi - ang)
- angles.append(1.0 - abs(ang - np.pi/4) / (np.pi/4))
- diag_strength = float(np.mean(angles)) if angles else 0.0
- h,w = img_shape[0], img_shape[1]
- cx,cy = int(centroid[0]), int(centroid[1])
- tri_score = 0.0
- for c in contours:
- area = cv2.contourArea(c)
- if area < 0.01*w*h: continue
- peri = cv2.arcLength(c, True)
- approx = cv2.approxPolyDP(c, 0.04 * peri, True)
- if len(approx) == 3:
- inside = cv2.pointPolygonTest(c, (cx,cy), False)
- dist_to_centroid = 0 if inside>=0 else min([np.linalg.norm(np.array(pt[0]) - np.array([cx,cy])) for pt in approx])
- tri_score = max(tri_score, max(0.0, 1.0 - dist_to_centroid / max(w,h)))
- combined = 0.6*diag_strength + 0.4*tri_score
- return float(max(0.0, min(1.0, combined)))
-
def symmetry_score(img_gray):
h,w = img_gray.shape
left = img_gray[:, :w//2]
@@ -134,19 +350,6 @@ def symmetry_score(img_gray):
corr = np.corrcoef(leftf.flatten(), rightf.flatten())[0,1]
return float(max(0.0, min(1.0, (corr + 1)/2)))
-def fill_frame_score(sal, threshold=0.5):
- mask = sal > threshold
- if mask.sum() == 0: return 0.0
- ys, xs = np.where(mask)
- h,w = sal.shape
- box_area = (xs.max()-xs.min()+1)*(ys.max()-ys.min()+1)
- frac = box_area / (w*h)
- return float(min(1.0, frac*2.0))
-
-def negative_space_score(sal):
- low = (sal < 0.15).sum() / sal.size
- return float(min(1.0, low))
-
def thirds_score(sal, centroid):
h,w = sal.shape
cx, cy = centroid
@@ -168,6 +371,10 @@ def golden_ratio_score(sal, centroid):
maxd = hypot(w, h)
return float(max(0.0, min(1.0, 1.0 - min(dists)/ (maxd*0.9))))
+def negative_space_score(sal):
+ low = (sal < 0.15).sum() / sal.size
+ return float(min(1.0, low))
+
def center_score(sal, centroid):
h,w = sal.shape
cx, cy = centroid
@@ -177,65 +384,56 @@ def center_score(sal, centroid):
return float(max(0.0, 1.0 - dist*1.2))
-# ==========================================
-# 3. SINGLE IMAGE ANALYZER
-# ==========================================
-
def analyze_single_image(img_path):
if not os.path.exists(img_path):
- return json.dumps({
- "error": f"File not found at {img_path}",
- "success": False
- })
+ return json.dumps({'scores': {}, 'error': f"File not found at {img_path}"}, cls=NumpyEncoder)
- # Load Image
img_full = cv2.imread(img_path)
if img_full is None:
- return json.dumps({
- "error": "Could not load image. Check format.",
- "success": False
- })
+ return json.dumps({'scores': {}, 'error': f"Could not load image. Check format."}, cls=NumpyEncoder)
- try:
- # 1. Preprocessing
- img, scale = resize_for_fast_processing(img_full, max_side=640)
- h, w = img.shape[:2]
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
-
- # 2. Saliency Map
- sal = compute_saliency_gray(img)
- if sal.shape != gray.shape:
- sal = cv2.resize(sal, (w, h), interpolation=cv2.INTER_LINEAR)
- centroid = saliency_centroid(sal)
-
- # 3. Geometric Features (Lines & Contours)
- edges, lines = detect_lines(gray)
- _, thr = cv2.threshold((gray).astype(np.uint8), 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)
- contours, _ = cv2.findContours(thr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
-
- # 4. Calculate Scores
- scores = {
- 'Rule of Thirds': thirds_score(sal, centroid),
- 'Golden Ratio': golden_ratio_score(sal, centroid),
- 'Symmetry': symmetry_score(gray),
- 'Fill Frame': fill_frame_score(sal, threshold=0.45),
- 'Negative Space': negative_space_score(sal),
- 'Center Composition': center_score(sal, centroid),
- 'Visual Balance': balance_score(img, sal),
- 'Depth': depth_score(gray, sal),
- 'Saturation': saturation_score(img),
- 'Diagonals & Triangles': diagonals_triangles_score(lines, contours, centroid, img.shape)
- }
-
- return json.dumps({
- "success": True,
- "scores": scores,
- "error": None
- })
-
- except Exception as e:
- return json.dumps({
- "success": False,
- "scores": {},
- "error": str(e)
- })
-\ No newline at end of file
+ img, scale = resize_for_fast_processing(img_full, max_side=640)
+ h, w = img.shape[:2]
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
+
+ sal = compute_saliency_gray(img)
+ if sal.shape != gray.shape:
+ sal = cv2.resize(sal, (w, h), interpolation=cv2.INTER_LINEAR)
+ centroid = saliency_centroid(sal)
+
+ edges, lines = detect_lines(gray)
+
+ # ======= compute composition scores (existing + new layout scores) =======
+ composition_scores = {
+ 'Rule of Thirds': thirds_score(sal, centroid),
+ 'Golden Ratio': golden_ratio_score(sal, centroid),
+ 'Symmetry': symmetry_score(gray),
+ 'Negative Space': negative_space_score(sal),
+ 'Center Composition': center_score(sal, centroid),
+ 'Balance': balance_score(img, sal)
+ }
+
+ v_score, v_info = vertical_stack_score(sal, lines)
+ t_score, t_info = triptych_score(sal, lines)
+ fb_score, fb_info = full_bleed_score(sal, img)
+ tc_score, tc_info = tight_crop_score(sal, gray)
+ lf_score, lf_info = layered_foreground_score(sal, gray)
+
+ composition_scores['Vertical Stack'] = v_score
+ composition_scores['Triptych'] = t_score
+ composition_scores['Full Bleed'] = fb_score
+ composition_scores['Tight Crop'] = tc_score
+ composition_scores['Layered Foreground'] = lf_score
+
+ diagnostics = {
+ 'Vertical Stack_info': v_info,
+ 'Triptych_info': t_info,
+ 'Full Bleed_info': fb_info,
+ 'Tight Crop_info': tc_info,
+ 'Layered Foreground_info': lf_info
+ }
+ result = {
+ 'scores': composition_scores,
+ 'diagnostics': diagnostics
+ }
+ return json.dumps(result, cls=NumpyEncoder)
+\ No newline at end of file
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
@@ -19,7 +19,7 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
- id("com.android.application") version "8.7.0" apply false
+ id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.21" apply false
id("com.chaquo.python") version "15.0.1" apply false
}