Spaces:
Sleeping
Sleeping
| import os | |
| import re | |
| import json | |
| import base64 | |
| import textwrap | |
| import requests | |
| from io import BytesIO | |
| from PIL import Image | |
| import cv2 | |
| import numpy as np | |
| from flask import Flask, request, jsonify, send_file | |
| from flask_cors import CORS | |
| app = Flask(__name__) | |
| CORS(app) | |
| # ββ Groq ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_client(): | |
| from groq import Groq | |
| return Groq(api_key=os.environ.get("GROQ_API_KEY", "")) | |
| # ββ Wikipedia βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_wiki(name): | |
| try: | |
| term = name.replace(" ", "_") | |
| r = requests.get( | |
| f"https://en.wikipedia.org/api/rest_v1/page/summary/{term}", | |
| headers={"User-Agent": "PlantLens/1.0"}, | |
| timeout=6 | |
| ) | |
| d = r.json() | |
| summary = d.get("extract", "") | |
| summary = re.sub(r'\[.*?\]', '', summary) | |
| summary = re.sub(r'\s{2,}', ' ', summary).strip()[:500] | |
| url = d.get("content_urls", {}).get("desktop", {}).get("page", "") | |
| return summary, url | |
| except Exception: | |
| return "", "" | |
| # ββ Identify plants via Groq vision ββββββββββββββββββββββββββββββββββββββββββ | |
| def identify_plants(image_bytes): | |
| client = get_client() | |
| b64 = base64.b64encode(image_bytes).decode("utf-8") | |
| prompt = textwrap.dedent("""\ | |
| You are an expert botanist. Look at this image and identify EVERY plant visible. | |
| Reply with ONLY a JSON array, no markdown, no explanation: | |
| [ | |
| { | |
| "common_name": "...", | |
| "scientific_name": "...", | |
| "family": "...", | |
| "confidence": "high|medium|low", | |
| "key_features": ["...", "...", "..."], | |
| "wikipedia_search_term": "...", | |
| "bbox": {"x_pct": 10, "y_pct": 10, "w_pct": 80, "h_pct": 80} | |
| } | |
| ] | |
| bbox values are percentages (0-100) of image width/height. | |
| If no plant found, return []. | |
| """) | |
| resp = client.chat.completions.create( | |
| model="meta-llama/llama-4-scout-17b-16e-instruct", | |
| messages=[{ | |
| "role": "user", | |
| "content": [ | |
| {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}, | |
| {"type": "text", "text": prompt} | |
| ] | |
| }], | |
| temperature=0.2, | |
| max_tokens=1500 | |
| ) | |
| raw = resp.choices[0].message.content | |
| cleaned = re.sub(r'```json|```', '', raw).strip() | |
| try: | |
| result = json.loads(cleaned) | |
| return result if isinstance(result, list) else [] | |
| except Exception: | |
| return [] | |
| # ββ Draw annotations on image βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| BG = (247, 243, 238) | |
| DOT_C = (50, 50, 50) | |
| def annotate(image_bytes, plants): | |
| arr = np.frombuffer(image_bytes, np.uint8) | |
| orig = cv2.imdecode(arr, cv2.IMREAD_COLOR) | |
| OH, OW = orig.shape[:2] | |
| PAD = max(20, OW // 30) | |
| W = OW + PAD * 2 | |
| H = OH | |
| bg_bgr = (BG[2], BG[1], BG[0]) | |
| canvas = np.full((H, W, 3), bg_bgr, dtype=np.uint8) | |
| canvas[0:OH, PAD:PAD+OW] = orig | |
| DOT_R = max(12, OW // 65) | |
| sc_num = max(0.32, OW / 2400) | |
| FONT = cv2.FONT_HERSHEY_SIMPLEX | |
| dot_positions = [] | |
| for i, p in enumerate(plants): | |
| bb = p.get("bbox", {}) | |
| cx = PAD + int((bb.get("x_pct", 50) + bb.get("w_pct", 10) / 2) / 100 * OW) | |
| cy = int((bb.get("y_pct", 50) + bb.get("h_pct", 10) / 2) / 100 * OH) | |
| cx = min(max(cx, PAD + DOT_R + 2), PAD + OW - DOT_R - 2) | |
| cy = min(max(cy, DOT_R + 2), OH - DOT_R - 2) | |
| # White halo | |
| cv2.circle(canvas, (cx, cy), DOT_R + 2, (255, 255, 255), -1, cv2.LINE_AA) | |
| # Dark dot | |
| cv2.circle(canvas, (cx, cy), DOT_R, DOT_C, -1, cv2.LINE_AA) | |
| # Number | |
| num = str(i + 1) | |
| (nw, nh), _ = cv2.getTextSize(num, FONT, sc_num, 1) | |
| cv2.putText(canvas, num, (cx - nw//2, cy + nh//2), FONT, sc_num, (255,255,255), 1, cv2.LINE_AA) | |
| # Store dot position as percentage of final canvas for tooltip | |
| dot_positions.append({ | |
| "x_pct": round(cx / W * 100, 2), | |
| "y_pct": round(cy / H * 100, 2), | |
| "name": p.get("common_name", "Unknown") | |
| }) | |
| ok, buf = cv2.imencode(".png", canvas) | |
| return buf.tobytes(), dot_positions | |
| # ββ Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def index(): | |
| return send_file("index.html") | |
| def health(): | |
| return jsonify({"status": "ok"}) | |
| def analyze(): | |
| if "file" not in request.files: | |
| return jsonify({"error": "No file"}), 400 | |
| raw_bytes = request.files["file"].read() | |
| try: | |
| plants = identify_plants(raw_bytes) | |
| # Enrich with Wikipedia | |
| for p in plants: | |
| term = p.get("wikipedia_search_term") or p.get("common_name", "") | |
| summary, url = get_wiki(term) | |
| p["wiki_summary"] = summary | |
| p["wiki_url"] = url | |
| # Annotate image | |
| annotated_bytes, dot_positions = annotate(raw_bytes, plants) | |
| annotated_b64 = base64.b64encode(annotated_bytes).decode("utf-8") | |
| return jsonify({ | |
| "plants": plants, | |
| "count": len(plants), | |
| "annotated_image": f"data:image/png;base64,{annotated_b64}", | |
| "dot_positions": dot_positions | |
| }) | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc() | |
| return jsonify({"error": str(e)}), 500 | |
| if __name__ == "__main__": | |
| print("Starting PlantLens on port 7860...") | |
| app.run(host="0.0.0.0", port=7860, debug=False) | |