Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
| 2 |
import os
|
| 3 |
import io
|
| 4 |
import json
|
|
|
|
|
|
|
| 5 |
import base64
|
| 6 |
import logging
|
| 7 |
import uuid
|
|
@@ -216,29 +218,70 @@ def upload_b64_to_firebase(base64_str: str, path: str, content_type="image/jpeg"
|
|
| 216 |
raise
|
| 217 |
|
| 218 |
# ---------- Image helpers (with EXIF transpose) ----------
|
|
|
|
|
|
|
|
|
|
| 219 |
def read_image_bytes(file_storage) -> Tuple[np.ndarray, int, int, bytes]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
data = file_storage.read()
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
try:
|
| 223 |
img = ImageOps.exif_transpose(img)
|
| 224 |
except Exception:
|
|
|
|
| 225 |
pass
|
|
|
|
|
|
|
| 226 |
img = img.convert("RGB")
|
| 227 |
w, h = img.size
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
def crop_and_b64(bgr_img: np.ndarray, x: int, y: int, w: int, h: int, max_side=512) -> str:
|
|
|
|
|
|
|
|
|
|
| 232 |
h_img, w_img = bgr_img.shape[:2]
|
| 233 |
x = max(0, int(x)); y = max(0, int(y))
|
| 234 |
x2 = min(w_img, int(x + w)); y2 = min(h_img, int(y + h))
|
| 235 |
crop = bgr_img[y:y2, x:x2]
|
| 236 |
if crop.size == 0:
|
| 237 |
return ""
|
|
|
|
| 238 |
max_dim = max(crop.shape[0], crop.shape[1])
|
| 239 |
if max_dim > max_side:
|
| 240 |
scale = max_side / max_dim
|
| 241 |
crop = cv2.resize(crop, (int(crop.shape[1] * scale), int(crop.shape[0] * scale)), interpolation=cv2.INTER_AREA)
|
|
|
|
| 242 |
_, jpeg = cv2.imencode(".jpg", crop, [int(cv2.IMWRITE_JPEG_QUALITY), 82])
|
| 243 |
return base64.b64encode(jpeg.tobytes()).decode("ascii")
|
| 244 |
|
|
|
|
| 2 |
import os
|
| 3 |
import io
|
| 4 |
import json
|
| 5 |
+
from io import BytesIO
|
| 6 |
+
|
| 7 |
import base64
|
| 8 |
import logging
|
| 9 |
import uuid
|
|
|
|
| 218 |
raise
|
| 219 |
|
| 220 |
# ---------- Image helpers (with EXIF transpose) ----------
|
| 221 |
+
|
| 222 |
+
# Replace existing read_image_bytes and crop_and_b64 with this block
|
| 223 |
+
|
| 224 |
def read_image_bytes(file_storage) -> Tuple[np.ndarray, int, int, bytes]:
|
| 225 |
+
"""
|
| 226 |
+
Read bytes, apply EXIF orientation, return BGR numpy, width, height and re-encoded JPEG bytes.
|
| 227 |
+
This ensures the bytes we pass to Gemini / upload to storage are physically upright
|
| 228 |
+
(EXIF orientation is applied and not left in metadata).
|
| 229 |
+
"""
|
| 230 |
data = file_storage.read()
|
| 231 |
+
try:
|
| 232 |
+
img = Image.open(io.BytesIO(data))
|
| 233 |
+
except Exception as e:
|
| 234 |
+
# fallback: try to decode raw bytes via OpenCV
|
| 235 |
+
try:
|
| 236 |
+
arr_np = np.frombuffer(data, np.uint8)
|
| 237 |
+
cv_img = cv2.imdecode(arr_np, cv2.IMREAD_COLOR)
|
| 238 |
+
if cv_img is None:
|
| 239 |
+
raise
|
| 240 |
+
h, w = cv_img.shape[:2]
|
| 241 |
+
# re-encode to jpeg bytes to have consistent format
|
| 242 |
+
_, jpeg = cv2.imencode(".jpg", cv_img, [int(cv2.IMWRITE_JPEG_QUALITY), 92])
|
| 243 |
+
return cv_img, w, h, jpeg.tobytes()
|
| 244 |
+
except Exception as ee:
|
| 245 |
+
raise
|
| 246 |
+
|
| 247 |
+
# physically apply EXIF rotation if present
|
| 248 |
try:
|
| 249 |
img = ImageOps.exif_transpose(img)
|
| 250 |
except Exception:
|
| 251 |
+
# ignore failures here; proceed with original image
|
| 252 |
pass
|
| 253 |
+
|
| 254 |
+
# ensure RGB and get size
|
| 255 |
img = img.convert("RGB")
|
| 256 |
w, h = img.size
|
| 257 |
+
|
| 258 |
+
# re-encode to JPEG bytes to strip EXIF orientation tag (important!)
|
| 259 |
+
buf = BytesIO()
|
| 260 |
+
# We intentionally omit any EXIF bytes when saving so orientation is cleared.
|
| 261 |
+
img.save(buf, format="JPEG", quality=92, optimize=True)
|
| 262 |
+
jpeg_bytes = buf.getvalue()
|
| 263 |
+
|
| 264 |
+
# convert to BGR numpy for OpenCV operations
|
| 265 |
+
arr = np.array(img)[:, :, ::-1] # RGB -> BGR
|
| 266 |
+
return arr, w, h, jpeg_bytes
|
| 267 |
+
|
| 268 |
|
| 269 |
def crop_and_b64(bgr_img: np.ndarray, x: int, y: int, w: int, h: int, max_side=512) -> str:
|
| 270 |
+
"""
|
| 271 |
+
Crop from BGR image (already upright), optionally resize, encode as JPEG and return base64 string.
|
| 272 |
+
"""
|
| 273 |
h_img, w_img = bgr_img.shape[:2]
|
| 274 |
x = max(0, int(x)); y = max(0, int(y))
|
| 275 |
x2 = min(w_img, int(x + w)); y2 = min(h_img, int(y + h))
|
| 276 |
crop = bgr_img[y:y2, x:x2]
|
| 277 |
if crop.size == 0:
|
| 278 |
return ""
|
| 279 |
+
# resize if too large
|
| 280 |
max_dim = max(crop.shape[0], crop.shape[1])
|
| 281 |
if max_dim > max_side:
|
| 282 |
scale = max_side / max_dim
|
| 283 |
crop = cv2.resize(crop, (int(crop.shape[1] * scale), int(crop.shape[0] * scale)), interpolation=cv2.INTER_AREA)
|
| 284 |
+
# encode to JPEG (this will be upright because bgr_img was exif_transposed)
|
| 285 |
_, jpeg = cv2.imencode(".jpg", crop, [int(cv2.IMWRITE_JPEG_QUALITY), 82])
|
| 286 |
return base64.b64encode(jpeg.tobytes()).decode("ascii")
|
| 287 |
|