plantslens / app.py
pavan1221's picture
Upload app.py
16f23a5 verified
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 ────────────────────────────────────────────────────────────────────
@app.route("/")
def index():
return send_file("index.html")
@app.route("/health")
def health():
return jsonify({"status": "ok"})
@app.route("/analyze", methods=["POST"])
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)