Spaces:
Runtime error
Runtime error
Update main.py
Browse files
main.py
CHANGED
@@ -1,154 +1,229 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
import
|
6 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
-
from model import predict
|
9 |
-
|
10 |
-
app = Flask(__name__)
|
11 |
-
CORS(app)
|
12 |
-
|
13 |
-
# global variables
|
14 |
-
threshold = 0.02
|
15 |
-
diagnosis = ''
|
16 |
-
pc_source = False
|
17 |
-
|
18 |
-
@app.route('/api/process-annotation', methods=['POST'])
|
19 |
-
def process_annotation():
|
20 |
try:
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
outline="red", width=3
|
62 |
-
)
|
63 |
-
|
64 |
-
buffered = io.BytesIO()
|
65 |
-
cropped.save(buffered, format="PNG")
|
66 |
-
processed_b64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
67 |
-
processed_data_url = f"data:image/png;base64,{processed_b64}"
|
68 |
-
|
69 |
-
print(f"Results: Error-{error}, Comment-{label}, Confidence-{confidence}, Diagnosis-{diagnosis}\n")
|
70 |
-
print(f"Rendering results to: {source} \n")
|
71 |
-
|
72 |
-
return jsonify({
|
73 |
-
"processed_image": processed_data_url,
|
74 |
-
"category": category,
|
75 |
-
"comment": label,
|
76 |
-
"error": error,
|
77 |
-
"confidence": confidence,
|
78 |
-
"threshold": threshold,
|
79 |
-
"diagnosis": diagnosis
|
80 |
-
})
|
81 |
-
|
82 |
-
elif source == "telegram":
|
83 |
-
category = request.form.get('category')
|
84 |
-
view = request.form.get('view')
|
85 |
-
source_field = request.form.get('source')
|
86 |
-
image_file = request.files.get('image')
|
87 |
-
|
88 |
-
if not image_file:
|
89 |
-
return jsonify({"error": "No image file provided"}), 400
|
90 |
-
|
91 |
-
if not category or not view:
|
92 |
-
return jsonify({"error": "Missing category or view"}), 400
|
93 |
-
|
94 |
-
try:
|
95 |
-
img = Image.open(image_file)
|
96 |
-
if img.mode != 'RGB':
|
97 |
-
img = img.convert('RGB')
|
98 |
-
except Exception as e:
|
99 |
-
return jsonify({"error": f"Invalid image file: {str(e)}"}), 400
|
100 |
-
|
101 |
-
print(f"\nParameters:[Category-{category}, View-{view}, Image size-{img.size}]\n")
|
102 |
-
print("Processing...\n")
|
103 |
-
|
104 |
-
error = predict(img, view, category)
|
105 |
-
label = get_label(error)
|
106 |
-
confidence = 100 - (100 * float(error))
|
107 |
-
diagnosis = build_diagnosis(error, confidence)
|
108 |
-
|
109 |
-
buffered = io.BytesIO()
|
110 |
-
img.save(buffered, format="PNG")
|
111 |
-
processed_b64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
112 |
-
processed_data_url = f"data:image/png;base64,{processed_b64}"
|
113 |
-
|
114 |
-
response_data = {
|
115 |
-
"processed_image": processed_data_url,
|
116 |
-
"category": category,
|
117 |
-
"view": view,
|
118 |
-
"comment": label,
|
119 |
-
"error": error,
|
120 |
-
"confidence": confidence,
|
121 |
-
"threshold": threshold,
|
122 |
-
"diagnosis": diagnosis,
|
123 |
-
"source": "telegram"
|
124 |
-
}
|
125 |
-
|
126 |
-
print(f"Results: Error-{error}, Comment-{label}, Confidence-{confidence}, Diagnosis-{diagnosis}\n")
|
127 |
-
print(f"Rendering results to: {source} \n")
|
128 |
-
|
129 |
-
return jsonify(response_data)
|
130 |
-
|
131 |
-
else:
|
132 |
-
return jsonify({"error": "Unsupported source"}), 400
|
133 |
-
|
134 |
except Exception as e:
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import aiohttp
|
3 |
+
import os,time
|
4 |
+
|
5 |
+
from aiohttp import FormData
|
6 |
+
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
7 |
+
|
8 |
+
from telegram.ext import (
|
9 |
+
Application, CommandHandler, MessageHandler,
|
10 |
+
filters, CallbackQueryHandler, ContextTypes
|
11 |
+
)
|
12 |
+
|
13 |
+
from dotenv import load_dotenv
|
14 |
+
load_dotenv(dotenv_path='env.env')
|
15 |
+
|
16 |
+
|
17 |
+
#config
|
18 |
+
TOKEN = "8320924107:AAH505mhHkOxeY3aLk0GObIpO_KCtY9hhLM"
|
19 |
+
API_ENDPOINT = os.getenv("API_ENDPOINT")
|
20 |
+
logging.basicConfig(level=logging.INFO)
|
21 |
+
|
22 |
+
#category mappings
|
23 |
+
VIEW_CATEGORIES = {
|
24 |
+
"crl": {
|
25 |
+
"Maxilla": "mx",
|
26 |
+
"Mandible-MDS": "mds",
|
27 |
+
"Mandible-MLS": "mls",
|
28 |
+
"Lateral ventricle": "lv",
|
29 |
+
"Head": "head",
|
30 |
+
"Gestational sac": "gsac",
|
31 |
+
"Thorax": "thorax",
|
32 |
+
"Abdomen": "ab",
|
33 |
+
"Body(Biparietal diameter)": "bd",
|
34 |
+
"Rhombencephalon": "rbp",
|
35 |
+
"Diencephalon": "dp",
|
36 |
+
"NTAPS": "ntaps",
|
37 |
+
"Nasal bone": "nb"
|
38 |
+
},
|
39 |
+
"nt": {
|
40 |
+
"Maxilla": "mx",
|
41 |
+
"Mandible-MDS": "mds",
|
42 |
+
"Mandible-MLS": "mls",
|
43 |
+
"Lateral ventricle": "lv",
|
44 |
+
"Head": "head",
|
45 |
+
"Thorax": "thorax",
|
46 |
+
"Abdomen": "ab",
|
47 |
+
"Rhombencephalon": "rbp",
|
48 |
+
"Diencephalon": "dp",
|
49 |
+
"Nuchal translucency": "nt",
|
50 |
+
"NTAPS": "ntaps",
|
51 |
+
"Nasal bone": "nb"
|
52 |
+
}
|
53 |
+
}
|
54 |
+
|
55 |
+
#start
|
56 |
+
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
57 |
+
await update.message.reply_text("Send an ultrasound image (JPG/png only) to begin. See /instructions first")
|
58 |
+
|
59 |
+
async def instructions(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
60 |
+
instruction_message = """
|
61 |
+
π **Instructions**
|
62 |
+
|
63 |
+
πΈ **Step 1:** Send a cropped ultrasound image of a structure you want to analyze. See all structures with /list command
|
64 |
+
πΈ **Step 2:** Select the ultrasound view (CRL or NT)
|
65 |
+
πΈ **Step 3:** Choose the anatomical category to analyze
|
66 |
+
πΈ **Step 4:** Wait for the AI analysis results
|
67 |
+
|
68 |
+
π **Important notes:**
|
69 |
+
β’ Only JPG/PNG images are supported
|
70 |
+
β’ Ensure the ultrasound image is clear and properly oriented
|
71 |
+
β’ Results are for reference only - always consult a medical professional
|
72 |
+
β’ Processing may take a few seconds
|
73 |
+
β’ Stop bot with /stop
|
74 |
+
|
75 |
+
π‘ **Tips:**
|
76 |
+
β’ Use high-quality, well-lit images for better accuracy
|
77 |
+
β’ Make sure the anatomical structure is clearly visible
|
78 |
+
β’ Different views (CRL/NT) have different category options
|
79 |
+
|
80 |
+
π **Need help?** Contact support if you encounter any issues @d3ikshr.
|
81 |
+
"""
|
82 |
+
|
83 |
+
await update.message.reply_text(instruction_message, parse_mode='Markdown')
|
84 |
+
|
85 |
+
async def list_categories(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
86 |
+
list_message = """
|
87 |
+
π **Available Categories by View**
|
88 |
+
|
89 |
+
π **CRL view categories:**
|
90 |
+
β’ Maxilla β’ Mandible-MDS β’ Mandible-MLS
|
91 |
+
β’ Lateral ventricle β’ Head β’ Gestational sac
|
92 |
+
β’ Thorax β’ Abdomen β’ Body(Biparietal diameter)
|
93 |
+
β’ Rhombencephalon β’ Diencephalon β’ NTAPS
|
94 |
+
β’ Nasal bone
|
95 |
+
|
96 |
+
π **NT view categories:**
|
97 |
+
β’ Maxilla β’ Mandible-MDS β’ Mandible-MLS
|
98 |
+
β’ Lateral ventricle β’ Head β’ Thorax
|
99 |
+
β’ Abdomen β’ Rhombencephalon β’ Diencephalon
|
100 |
+
β’ Nuchal translucency β’ NTAPS β’ Nasal bone
|
101 |
+
|
102 |
+
π‘ **Note:** Categories will be shown automatically based on your selected view during analysis.
|
103 |
+
"""
|
104 |
+
|
105 |
+
await update.message.reply_text(list_message, parse_mode='Markdown')
|
106 |
+
|
107 |
+
async def stop(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
108 |
+
await update.message.reply_text("π Bot stopped for this chat. Use /start to begin again.")
|
109 |
+
# Clear user data
|
110 |
+
context.user_data.clear()
|
111 |
+
|
112 |
+
# Receive image
|
113 |
+
async def handle_image(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
114 |
+
photo = update.message.photo[-1]
|
115 |
+
file = await photo.get_file()
|
116 |
+
file_path = f"{update.message.from_user.id}_ultrasound.jpg"
|
117 |
+
await file.download_to_drive(file_path)
|
118 |
+
context.user_data["image_path"] = file_path
|
119 |
+
|
120 |
+
# get view
|
121 |
+
buttons = [
|
122 |
+
[InlineKeyboardButton("CRL", callback_data="view:crl"),
|
123 |
+
InlineKeyboardButton("NT", callback_data="view:nt")]
|
124 |
+
]
|
125 |
+
await update.message.reply_text(
|
126 |
+
"Select the ultrasound view:",
|
127 |
+
reply_markup=InlineKeyboardMarkup(buttons)
|
128 |
+
)
|
129 |
+
|
130 |
+
#selcet view
|
131 |
+
async def handle_view(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
132 |
+
query = update.callback_query
|
133 |
+
await query.answer()
|
134 |
+
view = query.data.split(":")[1]
|
135 |
+
context.user_data["selected_view"] = view
|
136 |
+
|
137 |
+
#get categories for the selected view
|
138 |
+
categories = list(VIEW_CATEGORIES[view].keys())
|
139 |
+
buttons = [[InlineKeyboardButton(cat, callback_data=f"category:{cat}")]
|
140 |
+
for cat in categories]
|
141 |
+
await query.edit_message_text(
|
142 |
+
"Select anatomical category:",
|
143 |
+
reply_markup=InlineKeyboardMarkup(buttons)
|
144 |
+
)
|
145 |
+
|
146 |
+
#get category then upload
|
147 |
+
async def handle_category(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
148 |
+
query = update.callback_query
|
149 |
+
await query.answer()
|
150 |
+
category_display = query.data.split(":")[1]
|
151 |
+
context.user_data["selected_category"] = category_display
|
152 |
+
|
153 |
+
image_path = context.user_data.get("image_path")
|
154 |
+
view = context.user_data.get("selected_view")
|
155 |
+
|
156 |
+
if not image_path or not view:
|
157 |
+
await query.edit_message_text("Missing image or view.")
|
158 |
+
return
|
159 |
+
|
160 |
+
await query.edit_message_text("π Processing image...")
|
161 |
+
time.sleep(3)
|
162 |
+
await query.edit_message_text("π₯ Building diagnosis, please wait...")
|
163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
try:
|
165 |
+
#read the image file into memory first
|
166 |
+
with open(image_path, "rb") as f:
|
167 |
+
image_data = f.read()
|
168 |
+
|
169 |
+
#mapping
|
170 |
+
category_value = VIEW_CATEGORIES[view][category_display]
|
171 |
+
|
172 |
+
#create form data
|
173 |
+
form = FormData()
|
174 |
+
form.add_field("view", view)
|
175 |
+
form.add_field("category", category_value)
|
176 |
+
form.add_field("source", "telegram")
|
177 |
+
form.add_field("image", image_data, filename="image.jpg", content_type="image/jpeg")
|
178 |
+
|
179 |
+
#send request
|
180 |
+
async with aiohttp.ClientSession() as session:
|
181 |
+
async with session.post(API_ENDPOINT, data=form) as resp:
|
182 |
+
if resp.status == 200:
|
183 |
+
result = await resp.json()
|
184 |
+
|
185 |
+
message = f"""
|
186 |
+
π **Analysis Results**
|
187 |
+
|
188 |
+
π **View:** {result.get('view', view).upper()}
|
189 |
+
π₯ **Category:** {category_display}
|
190 |
+
|
191 |
+
π **Confidence:** {result.get('confidence', 0):.2f}%
|
192 |
+
β οΈ **Reconstruction error:** {result.get('error', 0):.5f}
|
193 |
+
|
194 |
+
π **Status:** {result.get('comment', 'No comment')}
|
195 |
+
|
196 |
+
π©Ί **Diagnosis:** {result.get('diagnosis', 'No diagnosis')}*
|
197 |
+
"""
|
198 |
+
|
199 |
+
await query.edit_message_text(message, parse_mode='Markdown')
|
200 |
+
|
201 |
+
else:
|
202 |
+
error_text = await resp.text()
|
203 |
+
await query.edit_message_text(f"β Upload failed. Status: {resp.status}\nError: {error_text}")
|
204 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
205 |
except Exception as e:
|
206 |
+
logging.error(f"Error processing request: {e}")
|
207 |
+
await query.edit_message_text(f"β Error: {str(e)}")
|
208 |
+
|
209 |
+
finally:
|
210 |
+
try:
|
211 |
+
if os.path.exists(image_path):
|
212 |
+
os.remove(image_path)
|
213 |
+
logging.info(f"Cleaned up image file: {image_path}")
|
214 |
+
except Exception as e:
|
215 |
+
logging.error(f"Error cleaning up file: {e}")
|
216 |
+
|
217 |
+
def main():
|
218 |
+
app = Application.builder().token(TOKEN).build()
|
219 |
+
app.add_handler(CommandHandler("start", start))
|
220 |
+
app.add_handler(CommandHandler("instructions", instructions))
|
221 |
+
app.add_handler(CommandHandler("list", list_categories))
|
222 |
+
app.add_handler(CommandHandler("stop", stop))
|
223 |
+
app.add_handler(MessageHandler(filters.PHOTO, handle_image))
|
224 |
+
app.add_handler(CallbackQueryHandler(handle_view, pattern="^view:"))
|
225 |
+
app.add_handler(CallbackQueryHandler(handle_category, pattern="^category:"))
|
226 |
+
app.run_polling()
|
227 |
+
|
228 |
+
if __name__ == "__main__":
|
229 |
+
main()
|