mjwong commited on
Commit
7713f64
·
verified ·
1 Parent(s): 58452af

Upload 12 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/boar.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/crow.jpg filter=lfs diff=lfs merge=lfs -text
38
+ examples/dragonfly.jpg filter=lfs diff=lfs merge=lfs -text
39
+ examples/macque.jpg filter=lfs diff=lfs merge=lfs -text
40
+ examples/otter.jpg filter=lfs diff=lfs merge=lfs -text
41
+ examples/parrot.jpg filter=lfs diff=lfs merge=lfs -text
42
+ examples/squirrel.jpg filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import json
4
+ from typing import Optional, Tuple
5
+
6
+ import gradio as gr
7
+ import numpy as np
8
+ from open_clip import create_model_and_transforms, get_tokenizer
9
+ from PIL import Image
10
+ import requests
11
+ import torch
12
+
13
+ from logging_config import logger
14
+ from helpers import l2_normalize, encode_image
15
+
16
+ # Set your API Gateway URL below.
17
+ API_GATEWAY_URL = os.getenv(
18
+ "API_GATEWAY_URL",
19
+ ""
20
+ )
21
+
22
+ API_GATEWAY_API_KEY = os.getenv(
23
+ "API_GATEWAY_API_KEY",
24
+ ""
25
+ )
26
+
27
+ MODEL_NAME = os.getenv(
28
+ "MODEL_NAME",
29
+ "hf-hub:imageomics/bioclip"
30
+ )
31
+
32
+ # Load BioCLIP Model from Hugging Face
33
+ logger.info("Loading model from Hugging Face...")
34
+ model, _, preprocess = create_model_and_transforms(MODEL_NAME)
35
+ device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
36
+ tokenizer = get_tokenizer(MODEL_NAME)
37
+ model = model.to(device)
38
+ logger.info(f"Model loaded on device successfully: {device}")
39
+
40
+ # Gradio App Function
41
+ def app_function(uploaded_image: Optional[np.ndarray]) -> Tuple[str, Optional[str], Optional[str], str]:
42
+ """Main function for the Gradio app.
43
+
44
+ Processes the uploaded image, performs semantic search, and returns a summary, species information, and HTML output.
45
+
46
+ Args:
47
+ uploaded_image (Optional[np.ndarray]): Uploaded image as a NumPy array.
48
+
49
+ Returns:
50
+ Tuple[str, Optional[str], Optional[str], str]: Summary, proposed scientific name, proposed common name, and HTML output.
51
+ """
52
+ if uploaded_image is None:
53
+ logger.error("app_function: No image uploaded.")
54
+ return "No image uploaded", None, None, ""
55
+
56
+ try:
57
+ image = Image.fromarray(uploaded_image)
58
+ except Exception as e:
59
+ logger.exception("app_function: Error processing image. Check if a valid image array is provided. Exception: %s", e)
60
+ return f"Error processing image: {e}", None, None, ""
61
+
62
+ try:
63
+ query_embedding = np.array(encode_image(image=image, preprocess=preprocess, model=model, device=device))
64
+ query_embedding = l2_normalize(query_embedding).tolist()
65
+ logger.info("app_function: Image encoded successfully. Embedding length: %d", len(query_embedding))
66
+ except Exception as e:
67
+ logger.exception("app_function: Error encoding image. Uploaded image shape: %s. Exception: %s", getattr(uploaded_image, 'shape', 'N/A'), e)
68
+ return f"Error encoding image: {e}", None, None, ""
69
+
70
+ payload = {"query_embedding": query_embedding}
71
+ headers = {"x-api-key": API_GATEWAY_API_KEY}
72
+ logger.info("app_function: Calling API Gateway with payload (embedding sample: %s...)", query_embedding[:5])
73
+
74
+ # Print the query embedding for debugging
75
+ # print(query_embedding)
76
+
77
+ try:
78
+ response = requests.post(API_GATEWAY_URL, json=payload, headers=headers)
79
+ logger.info("app_function: API Gateway responded with status code %d", response.status_code)
80
+ except Exception as e:
81
+ logger.exception("app_function: Exception during API Gateway call with payload: %s. Exception: %s", payload, e)
82
+ return f"Error calling API: {e}", None, None, ""
83
+
84
+ if response.status_code != 200:
85
+ logger.error("app_function: API Gateway returned error %d - %s", response.status_code, response.text)
86
+ return f"API error: {response.status_code} - {response.text}", None, None, ""
87
+
88
+ try:
89
+ body = response.json()
90
+ logger.info("app_function: Successfully parsed API Gateway response as JSON.")
91
+
92
+ # Print the response for debugging
93
+ # print(response.text)
94
+ # print(response.status_code)
95
+
96
+ # If body is a string with a list, try to load it
97
+ if isinstance(body, str):
98
+ try:
99
+ results = json.loads(body)
100
+ except Exception:
101
+ results = body
102
+ else:
103
+ results = body
104
+ except Exception as e:
105
+ logger.exception("app_function: Error decoding API Gateway response. Exception: %s", e)
106
+ return f"Error decoding response: {e}", None, None, ""
107
+
108
+ urls = []
109
+ image_urls = []
110
+ scientific_names = []
111
+ common_names = []
112
+ similarity_scores = []
113
+
114
+ for res in results:
115
+ urls.append(res.get("url", ""))
116
+ image_urls.append(res.get("image_url", ""))
117
+ scientific_names.append(res.get("scientific_name", "N/A"))
118
+ common_names.append(res.get("common_name", "N/A"))
119
+ similarity_scores.append(res.get("similarity", 0))
120
+
121
+ proposed_scientific = scientific_names[0]
122
+ proposed_common = common_names[0]
123
+ summary = "Found top 5 similar wildlife images."
124
+
125
+ # Build HTML output for the 5 boxes in horizontal arrangement.
126
+ boxes_html = "<div style='display: flex; justify-content: space-around; flex-wrap: nowrap;'>"
127
+ for url, image_url, sci, com, similarity_score in zip(urls, image_urls, scientific_names, common_names, similarity_scores):
128
+ try:
129
+ r = requests.get(image_url, timeout=5)
130
+ if r.status_code == 200:
131
+ encoded_img = base64.b64encode(r.content).decode("utf-8")
132
+ # Wrap the image in a container to keep it within fixed dimensions.
133
+ img_tag = f"""
134
+ <div style="width:200px; height:150px; overflow:hidden; display:flex; align-items:center; justify-content:center;">
135
+ <img src='data:image/jpeg;base64,{encoded_img}' style='max-width:100%; max-height:100%; object-fit: contain;'/>
136
+ </div>
137
+ """
138
+ else:
139
+ img_tag = """
140
+ <div style="width:200px; height:150px; background:#eee; display:flex; align-items:center; justify-content:center;">
141
+ Error loading image
142
+ </div>
143
+ """
144
+ except Exception as e:
145
+ logger.exception("app_function: Error loading image from URL: %s. Exception: %s", image_url, e)
146
+ img_tag = """
147
+ <div style="width:200px; height:150px; background:#eee; display:flex; align-items:center; justify-content:center;">
148
+ Error loading image
149
+ </div>
150
+ """
151
+
152
+ box = f"""
153
+ <div style='text-align: center; margin: 10px; flex: 1; border: 1px solid #ccc; min-height: 250px; display: flex; flex-direction: column; align-items: center; justify-content: center;'>
154
+ {img_tag}
155
+ <div style='font-size: 12px; margin-top: 5px;'>
156
+ <div><a href="{url}" target="_blank">View on iNaturalist</a></div>
157
+ <div>Scientific: {sci}</div>
158
+ <div>Common: {com}</div>
159
+ <div>Similarity: {similarity_score:.2f}</div>
160
+ </div>
161
+ </div>
162
+ """
163
+ boxes_html += box
164
+ boxes_html += "</div>"
165
+
166
+ logger.info("app_function: Results processed and returned to Gradio interface successfully.")
167
+ return summary, proposed_scientific, proposed_common, boxes_html
168
+
169
+ # Gradio Interface Using Blocks Layout
170
+ with gr.Blocks(title="Wildlife Semantic Search with BioCLIP") as demo:
171
+ # Custom CSS to fix the display size of the uploaded image.
172
+ gr.HTML(
173
+ """
174
+ <style>
175
+ /* Force the uploaded image to fit within 300x300px while preserving aspect ratio */
176
+ #fixedImage img {
177
+ object-fit: contain;
178
+ width: 300px;
179
+ height: 300px;
180
+ }
181
+ /* Style the logo to remove whitespace */
182
+ .logo-image {
183
+ object-fit: cover;
184
+ object-position: center;
185
+ width: 100%;
186
+ height: 100%;
187
+ display: block;
188
+ margin: 0;
189
+ padding: 0;
190
+ }
191
+ /* Custom style for the submit button */
192
+ .submit-button {
193
+ background: linear-gradient(90deg, green 0%, green 70%, orange 100%) !important;
194
+ color: white !important;
195
+ font-weight: bold !important;
196
+ }
197
+ </style>
198
+ """
199
+ )
200
+
201
+ # Row 1: Logo and Description in two columns.
202
+ with gr.Row(variant="panel"):
203
+ with gr.Column(scale=1):
204
+ gr.Image("logo/logo.jpg", elem_classes=["logo-image"], show_label=False)
205
+ with gr.Column(scale=30):
206
+ gr.Markdown(
207
+ """
208
+ ### Welcome to Ecologist – Singapore's AI-powered biodiversity explorer!
209
+
210
+ **Ecologist** identifies wildlife species found in Singapore from an uploaded photo.
211
+
212
+ Powered by multimodal image retrieval and visual encoding with [BioCLIP](https://huggingface.co/imageomics/bioclip), the system extracts features from the image and matches them against a specialized database of Singapore's diverse flora and fauna.
213
+
214
+ Both scientific and common names are provided within seconds, along with visually similar images that offer context about Singapore's rich natural heritage.
215
+
216
+ Ecologist is a step towards celebrating and preserving the island country’s unique wildlife through AI.
217
+ """
218
+ )
219
+
220
+ # Row 2: Image Upload with a fixed display container.
221
+ with gr.Row(variant="panel"):
222
+ with gr.Column():
223
+ image_input = gr.Image(type="numpy", label="Upload Wildlife Image", elem_id="fixedImage")
224
+
225
+ # Row 3: Submit Button.
226
+ submit_button = gr.Button("Submit", elem_classes=["submit-button"])
227
+
228
+ with gr.Row(variant="panel"):
229
+ with gr.Column():
230
+ gr.Examples(
231
+ examples=[
232
+ ["examples/boar.jpg"],
233
+ ["examples/crow.jpg"],
234
+ ["examples/dragonfly.jpg"],
235
+ ["examples/macque.jpg"],
236
+ ["examples/otter.jpg"],
237
+ ["examples/parrot.jpg"],
238
+ ["examples/squirrel.jpg"],
239
+ ],
240
+ inputs=image_input,
241
+ outputs=None,
242
+ label="Example Wildlife Images",
243
+ )
244
+
245
+ # Row 4: Proposed Species Output.
246
+ with gr.Row(variant="panel"):
247
+ with gr.Column():
248
+ gr.Markdown("## Identified Species")
249
+
250
+ with gr.Row(variant="panel"):
251
+ with gr.Column():
252
+ proposed_scientific_output = gr.Textbox(label="Scientific Name", placeholder="No name yet")
253
+ with gr.Column():
254
+ proposed_common_output = gr.Textbox(label="Common Name", placeholder="No name yet")
255
+
256
+ # Row 5: Pre-populated placeholder for 5 columns with borders.
257
+ with gr.Row(variant="panel"):
258
+ with gr.Column():
259
+ gr.Markdown("## Most Similar Wildlife Images from Database")
260
+
261
+ placeholder_boxes = "<div style='display: flex; justify-content: space-around; flex-wrap: nowrap;'>"
262
+ for _ in range(5):
263
+ placeholder_boxes += """
264
+ <div style='text-align: center; margin: 10px; flex: 1; border: 1px solid #ccc; min-height: 250px; display: flex; align-items: center; justify-content: center;'>
265
+ No image yet
266
+ </div>
267
+ """
268
+ placeholder_boxes += "</div>"
269
+
270
+ with gr.Row(variant="panel"):
271
+ with gr.Column():
272
+ html_output = gr.HTML(value=placeholder_boxes, container=True)
273
+
274
+ with gr.Row(variant="panel"):
275
+ with gr.Column():
276
+ gr.Markdown(
277
+ """
278
+ **Disclaimer:**
279
+ Not intended for commercial use, no user data is stored or used for training purposes, and all retrieval data is sourced from [iNaturalist](https://inaturalist.org/). Results may vary depending on the input image.
280
+
281
+ **References:**
282
+ This project is inspired by the work on [Biome](https://huggingface.co/spaces/govtech/Biome) from GovTech Singapore.
283
+
284
+ **Acknowledgments:**
285
+ Gratitude to [Dylan Chan](https://www.pexels.com/@dylan-chan-2880813/), [Jesper](https://www.pexels.com/@jesper-425001880/), [Mark Baldovino](https://www.pexels.com/@odlab2/), [Sane Noor](https://www.pexels.com/@norsan/), [Soumen Chakraborty](https://www.pexels.com/@soumen-chakraborty-363019169/), [Tony Wu](https://www.pexels.com/@tonywuphotography/) and [Zett Foto](https://www.pexels.com/@zett-foto-194587/) for their wildlife images in [Pexels](https://www.pexels.com/).
286
+ """
287
+ )
288
+
289
+ # Wrapping the function to only forward the necessary outputs.
290
+ def wrapper(uploaded_image):
291
+ summary, proposed_scientific, proposed_common, boxes_html = app_function(uploaded_image)
292
+
293
+ # Print the summary for debugging
294
+ # print(summary)
295
+
296
+ return proposed_scientific, proposed_common, boxes_html
297
+
298
+ submit_button.click(fn=wrapper, inputs=image_input, outputs=[proposed_scientific_output, proposed_common_output, html_output])
299
+
300
+ if __name__ == "__main__":
301
+ demo.launch()
examples/.DS_Store ADDED
Binary file (6.15 kB). View file
 
examples/boar.jpg ADDED

Git LFS Details

  • SHA256: 16c1dd82e469fab1e7bbb0a2ebb6ae3262a8e1be55d36ee683af94fc4925f685
  • Pointer size: 132 Bytes
  • Size of remote file: 1.66 MB
examples/crow.jpg ADDED

Git LFS Details

  • SHA256: 0dc318f4821703dc8279d6a2963977656d4bb489cfa5c6dac93346cafa2b663c
  • Pointer size: 132 Bytes
  • Size of remote file: 2.01 MB
examples/dragonfly.jpg ADDED

Git LFS Details

  • SHA256: 5cce0411944c9339c57458357803154cfd09d199d020f2a4356708855e05eba6
  • Pointer size: 131 Bytes
  • Size of remote file: 489 kB
examples/macque.jpg ADDED

Git LFS Details

  • SHA256: f0be7ec2c8a08226b3ca33044e1e120884abd1897191bf8478137868ddc05ab9
  • Pointer size: 131 Bytes
  • Size of remote file: 433 kB
examples/otter.jpg ADDED

Git LFS Details

  • SHA256: 5a0ea9ee570baba30f1fc7386b981cf00e3de7dd296b3c16ff550a3e05bd168f
  • Pointer size: 132 Bytes
  • Size of remote file: 2.01 MB
examples/parrot.jpg ADDED

Git LFS Details

  • SHA256: efa5b21b2923f3190f752e6f376ffedc7314ae9791d6c7028162aa9be729eb1e
  • Pointer size: 132 Bytes
  • Size of remote file: 2.7 MB
examples/squirrel.jpg ADDED

Git LFS Details

  • SHA256: a916ea6ce25d8311be14458440ddbd46434ae511946a33dec941859bc08631a8
  • Pointer size: 132 Bytes
  • Size of remote file: 2.47 MB
logging_config.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ # Configure logging
4
+ logging.basicConfig(
5
+ level=logging.INFO, # Set the default logging level
6
+ format="%(asctime)s - %(levelname)s - %(message)s",
7
+ handlers=[
8
+ logging.StreamHandler(), # Log to the console
9
+ logging.FileHandler("app.log", mode="a") # Log to a file
10
+ ]
11
+ )
12
+
13
+ # Create a logger instance
14
+ logger = logging.getLogger(__name__)
logo/logo.jpg ADDED
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ open-clip-torch==2.30.0
2
+ torch==2.6.0
3
+ gradio==5.15.0
4
+ requests==2.31.0