gaurinath commited on
Commit
41be7ed
·
verified ·
1 Parent(s): 72e1bbb

Upload 8 files

Browse files
README.md CHANGED
@@ -1,12 +1,109 @@
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Rishiproject
3
- emoji: 🏢
4
- colorFrom: red
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: 5.42.0
8
- app_file: app.py
9
- pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Plant Identifier – Hyderabad Locator (Python)
2
+
3
+ This project lets you:
4
+ 1. Upload a **plant JPG** photo.
5
+ 2. Identify the plant using the **Plant.id API** (AI model).
6
+ 3. Show **where the plant has been observed in Hyderabad** (using the iNaturalist public API) and plot it on an interactive map.
7
+
8
+ It ships with a **Streamlit UI** (`streamlit_app.py`) so you can run everything locally in a few minutes.
9
+
10
  ---
11
+
12
+ ## Features
13
+ - 📷 Image upload (JPG/JPEG/PNG) via Streamlit
14
+ - 🤖 AI recognition via **Plant.id** (top matches with confidence, scientific name, common names, wiki details when available)
15
+ - 📍 Hyderabad occurrences via **iNaturalist** (observations plotted on a Folium map)
16
+ - 🗺️ Interactive map embedded in the app
17
+ - 🔑 `.env` support for secrets
18
+
19
+ > **Note**: You need a free or paid **Plant.id** API key. Create an account at plant.id and copy your key into `.env` as `PLANT_ID_API_KEY`.
20
+
21
  ---
22
 
23
+ ## File structure
24
+
25
+ ```
26
+ plant-identifier-hyd/
27
+ ├─ app/
28
+ │ ├─ streamlit_app.py # Streamlit UI
29
+ │ ├─ plant_id_client.py # Plant.id API client
30
+ │ ├─ location.py # Hyderabad bbox + iNaturalist search
31
+ │ ├─ utils.py # Helpers (image, env)
32
+ │ └─ static/
33
+ │ └─ styles.css # Optional extra styles for Streamlit
34
+ ├─ data/
35
+ │ └─ sample_images/ # Put example photos here
36
+ ├─ tests/
37
+ │ └─ test_smoke.py # Minimal smoke test
38
+ ├─ .env.example # Copy to .env and fill in your API key
39
+ ├─ requirements.txt
40
+ └─ README.md
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Quickstart
46
+
47
+ 1) **Clone or unzip** this folder.
48
+ 2) Create a virtual environment (recommended):
49
+ ```bash
50
+ python -m venv .venv
51
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
52
+ ```
53
+ 3) **Install dependencies**:
54
+ ```bash
55
+ pip install -r requirements.txt
56
+ ```
57
+ 4) **Set your API key**: copy `.env.example` to `.env` and put your Plant.id key.
58
+ 5) **Run the app**:
59
+ ```bash
60
+ streamlit run app/streamlit_app.py
61
+ ```
62
+ 6) Open the printed local URL in your browser.
63
+
64
+ ---
65
+
66
+ ## How it works
67
+
68
+ ### Plant identification
69
+ We send the uploaded image to **Plant.id** (`/v3/identification`) and request the top candidates. We show:
70
+ - scientific name
71
+ - common names
72
+ - confidence score
73
+ - a short description if available
74
+
75
+ ### Hyderabad locations
76
+ We query **iNaturalist** for recent observations within a Hyderabad bounding box. We then plot markers on a **Folium** map and embed it in Streamlit.
77
+
78
+ > The map shows where people have reported observing the plant in Hyderabad. This does not guarantee the plant is wild there; it simply shows community observations.
79
+
80
+ ---
81
+
82
+ ## Environment variables
83
+
84
+ Create a `.env` file in the project root:
85
+
86
+ ```
87
+ PLANT_ID_API_KEY=your_plant_id_api_key_here
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Notes & Limits
93
+ - API quotas apply (Plant.id & iNaturalist).
94
+ - Identification quality depends on your photo (focus, lighting, background).
95
+ - iNaturalist observations depend on community contributions—some plants may have few or no reports in the Hyderabad area.
96
+
97
+ ---
98
+
99
+ ## Testing
100
+
101
+ ```bash
102
+ pytest -q
103
+ ```
104
+
105
+ ---
106
+
107
+ ## License
108
+
109
+ MIT. For learning and non-commercial demo use. Check Plant.id and iNaturalist terms of service for production use.
app/location.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from typing import Dict, Any, List, Tuple
3
+
4
+ # Rough bounding box for Hyderabad, India (approximate)
5
+ # swlat, swlng (bottom-left) and nelat, nelng (top-right)
6
+ HYD_BBOX = (17.200, 78.200, 17.650, 78.650)
7
+
8
+ INAT_ENDPOINT = "https://api.inaturalist.org/v1/observations"
9
+
10
+ def hyderabad_bbox() -> Tuple[float, float, float, float]:
11
+ return HYD_BBOX
12
+
13
+ def find_observations_in_hyd(taxon_name: str, per_page: int = 30) -> List[Dict[str, Any]]:
14
+ swlat, swlng, nelat, nelng = HYD_BBOX
15
+ params = {
16
+ "taxon_name": taxon_name,
17
+ "swlat": swlat,
18
+ "swlng": swlng,
19
+ "nelat": nelat,
20
+ "nelng": nelng,
21
+ "order": "desc",
22
+ "order_by": "created_at",
23
+ "per_page": per_page,
24
+ }
25
+ r = requests.get(INAT_ENDPOINT, params=params, timeout=30)
26
+ r.raise_for_status()
27
+ data = r.json()
28
+ return data.get("results", [])
29
+
30
+ def observations_to_markers(observations: List[Dict[str, Any]]):
31
+ markers = []
32
+ for o in observations:
33
+ coords = o.get("geojson", {}).get("coordinates")
34
+ if not coords or len(coords) != 2:
35
+ # fallback
36
+ lat = o.get("location", ",").split(",")[0] if o.get("location") else None
37
+ lng = o.get("location", ",").split(",")[1] if o.get("location") else None
38
+ if lat and lng:
39
+ try:
40
+ markers.append((float(lat), float(lng), o))
41
+ except Exception:
42
+ continue
43
+ continue
44
+ lng, lat = coords # iNat stores as [lng, lat]
45
+ markers.append((lat, lng, o))
46
+ return markers
app/plant_id_client.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from typing import Dict, Any, List, Optional
3
+
4
+ PLANT_ID_ENDPOINT = "https://api.plant.id/v3/identification"
5
+
6
+ class PlantIdClient:
7
+ def __init__(self, api_key: str):
8
+ self.api_key = api_key
9
+ self.headers = {"Api-Key": self.api_key, "Content-Type": "application/json"}
10
+
11
+ def identify(self, images_b64: List[str], latitude: Optional[float] = None, longitude: Optional[float] = None) -> Dict[str, Any]:
12
+ payload = {
13
+ "images": images_b64,
14
+ "modifiers": ["crops_fast", "similar_images"],
15
+ "plant_details": [
16
+ "common_names",
17
+ "url",
18
+ "name_authority",
19
+ "wiki_description",
20
+ "taxonomy",
21
+ "synonyms"
22
+ ],
23
+ }
24
+ if latitude is not None and longitude is not None:
25
+ payload["latitude"] = latitude
26
+ payload["longitude"] = longitude
27
+
28
+ resp = requests.post(PLANT_ID_ENDPOINT, headers=self.headers, json=payload, timeout=60)
29
+ resp.raise_for_status()
30
+ return resp.json()
31
+
32
+ @staticmethod
33
+ def parse_top_candidates(result_json: Dict[str, Any]) -> List[Dict[str, Any]]:
34
+ suggestions = result_json.get("result", {}).get("classification", {}).get("suggestions", [])
35
+ out = []
36
+ for s in suggestions:
37
+ plant = s.get("plant", {})
38
+ details = plant.get("details", {})
39
+ out.append({
40
+ "scientific_name": plant.get("scientific_name"),
41
+ "common_names": details.get("common_names", []),
42
+ "probability": s.get("probability"),
43
+ "wiki": (details.get("wiki_description", {}) or {}).get("value"),
44
+ "url": details.get("url"),
45
+ })
46
+ return out
app/static/styles.css ADDED
@@ -0,0 +1 @@
 
 
1
+ /* Optional extra styles for Streamlit components */
app/streamlit_app.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import streamlit as st
3
+ from streamlit_folium import st_folium
4
+ import folium
5
+
6
+ from app.utils import load_image, image_to_base64_jpeg, get_env
7
+ from app.plant_id_client import PlantIdClient
8
+ from app.location import find_observations_in_hyd, observations_to_markers, hyderabad_bbox
9
+
10
+ st.set_page_config(page_title="Plant Identifier – Hyderabad Locator", page_icon="🌿", layout="wide")
11
+
12
+ st.title("🌿 Plant Identifier – Hyderabad Locator")
13
+ st.write("Upload a plant photo, identify it with AI, and see where it was observed in Hyderabad.")
14
+
15
+ with st.sidebar:
16
+ st.header("Settings")
17
+ api_key = st.text_input("Plant.id API Key", value=get_env("PLANT_ID_API_KEY"), type="password")
18
+ st.caption("You can also put this in a `.env` file as `PLANT_ID_API_KEY`.")
19
+ st.divider()
20
+ st.markdown("**Tip:** Better results with a clear leaf/flower close-up, good lighting, and neutral background.")
21
+
22
+ uploaded = st.file_uploader("Upload a plant photo (JPG/PNG)", type=["jpg", "jpeg", "png"])
23
+
24
+ if uploaded and api_key:
25
+ image = load_image(uploaded)
26
+ st.image(image, caption="Uploaded image", use_column_width=True)
27
+
28
+ b64 = image_to_base64_jpeg(image)
29
+ client = PlantIdClient(api_key)
30
+
31
+ with st.spinner("Identifying plant via Plant.id..."):
32
+ try:
33
+ result = client.identify([b64])
34
+ except Exception as e:
35
+ st.error(f"Plant.id API error: {e}")
36
+ st.stop()
37
+
38
+ candidates = client.parse_top_candidates(result)
39
+ if not candidates:
40
+ st.warning("No candidates returned. Try a clearer image.")
41
+ st.stop()
42
+
43
+ st.subheader("Top Matches")
44
+ top = None
45
+ for idx, c in enumerate(candidates, start=1):
46
+ with st.expander(f"{idx}. {c['scientific_name']} (confidence: {c['probability']:.2%})", expanded=(idx == 1)):
47
+ if idx == 1:
48
+ top = c
49
+ st.write("**Scientific name:**", c["scientific_name"] or "—")
50
+ st.write("**Common names:**", ", ".join(c["common_names"]) if c["common_names"] else "—")
51
+ if c["wiki"]:
52
+ st.write("**About:**", c["wiki"])
53
+ if c["url"]:
54
+ st.write(f"[More details]({c['url']})")
55
+
56
+ # Map Hyderabad occurrences for the top candidate
57
+ if top and top.get("scientific_name"):
58
+ st.subheader(f"📍 Observations in Hyderabad: {top['scientific_name']}")
59
+
60
+ with st.spinner("Fetching observations from iNaturalist..."):
61
+ try:
62
+ observations = find_observations_in_hyd(top["scientific_name"], per_page=100)
63
+ except Exception as e:
64
+ st.error(f"iNaturalist API error: {e}")
65
+ observations = []
66
+
67
+ markers = observations_to_markers(observations)
68
+ swlat, swlng, nelat, nelng = hyderabad_bbox()
69
+
70
+ # Create a Folium map centered roughly in Hyderabad
71
+ center_lat = (swlat + nelat) / 2.0
72
+ center_lng = (swlng + nelng) / 2.0
73
+ fmap = folium.Map(location=[center_lat, center_lng], zoom_start=11)
74
+
75
+ for lat, lng, o in markers:
76
+ popup_lines = []
77
+ if o.get("species_guess"):
78
+ popup_lines.append(f"<b>{o['species_guess']}</b>")
79
+ if o.get("observed_on"):
80
+ popup_lines.append(f"Date: {o['observed_on']}")
81
+ if o.get("place_guess"):
82
+ popup_lines.append(f"Place: {o['place_guess']}")
83
+ popup_html = "<br/>".join(popup_lines) if popup_lines else "iNaturalist observation"
84
+ folium.Marker([lat, lng], popup=popup_html).add_to(fmap)
85
+
86
+ # draw bbox
87
+ folium.Rectangle(
88
+ bounds=[[swlat, swlng], [nelat, nelng]],
89
+ fill=False
90
+ ).add_to(fmap)
91
+
92
+ st_map = st_folium(fmap, width=1000, height=600)
93
+
94
+ st.caption("Map shows iNaturalist community observations within a Hyderabad bounding box.")
95
+
96
+ else:
97
+ st.info("Enter your Plant.id API key in the sidebar and upload an image to begin.")
app/utils.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ from io import BytesIO
4
+ from PIL import Image
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ def get_env(key: str, default: str = "") -> str:
10
+ return os.getenv(key, default)
11
+
12
+ def image_to_base64_jpeg(image: Image.Image) -> str:
13
+ """Convert a PIL image to base64-encoded JPEG string."""
14
+ buf = BytesIO()
15
+ image = image.convert("RGB")
16
+ image.save(buf, format="JPEG", quality=90)
17
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
18
+
19
+ def load_image(file) -> Image.Image:
20
+ return Image.open(file)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit
2
+ requests
3
+ python-dotenv
4
+ Pillow
5
+ folium
6
+ streamlit-folium
tests/test_smoke.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ def test_imports():
2
+ import app.utils
3
+ import app.plant_id_client
4
+ import app.location