Yash goyal commited on
Commit
1a24761
·
verified ·
1 Parent(s): 70996b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +557 -492
app.py CHANGED
@@ -1,493 +1,558 @@
1
  import os
2
- os.environ['MPLCONFIGDIR'] = '/tmp/matplotlib'
3
- from flask import Flask, render_template, request, redirect, url_for, session, send_file
4
- from flask_sqlalchemy import SQLAlchemy
5
- from flask_migrate import Migrate
6
- import tensorflow as tf
7
- import numpy as np
8
- from PIL import Image
9
- import pickle
10
- import io
11
- import os
12
- import matplotlib.pyplot as plt
13
- from reportlab.lib.pagesizes import A4
14
- from reportlab.lib import colors
15
- from reportlab.pdfgen import canvas
16
- from reportlab.lib.units import inch
17
- from datetime import datetime
18
- import logging
19
- from flask_mail import Mail, Message
20
- from flask import jsonify, url_for
21
-
22
- app = Flask(__name__)
23
- app.secret_key = "e3f6f40bb8b2471b9f07c4025d845be9"
24
-
25
- # Database configuration
26
- app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/snapsin.db'
27
- app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
28
- db = SQLAlchemy(app)
29
- migrate = Migrate(app, db)
30
-
31
- # Mail configuration
32
- app.config['MAIL_SERVER'] = 'smtp.gmail.com'
33
- app.config['MAIL_PORT'] = 465
34
- app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
35
- app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
36
- app.config['MAIL_USE_TLS'] = False
37
- app.config['MAIL_USE_SSL'] = True
38
- mail = Mail(app)
39
-
40
- MODEL_PATH = "skin_lesion_model.h5"
41
- HISTORY_PATH = "training_history.pkl"
42
- PLOT_PATH = "/tmp/static/training_plot.png"
43
- LOGO_PATH = "static/logo.jpg"
44
- IMG_SIZE = (224, 224)
45
- CONFIDENCE_THRESHOLD = 0.30
46
-
47
- label_map = {
48
- 0: "Melanoma",
49
- 1: "Melanocytic nevus",
50
- 2: "Basal cell carcinoma",
51
- 3: "Actinic keratosis",
52
- 4: "Benign keratosis",
53
- 5: "Dermatofibroma",
54
- 6: "Vascular lesion",
55
- 7: "Squamous cell carcinoma"
56
- }
57
-
58
- recommendations = {
59
- "Melanoma": {
60
- "solutions": [
61
- "Consult a dermatologist immediately.",
62
- "Surgical removal is typically required.",
63
- "Regular follow-up and screening for metastasis."
64
- ],
65
- "medications": ["Interferon alfa-2b", "Vemurafenib", "Dacarbazine"]
66
- },
67
- "Melanocytic nevus": {
68
- "solutions": [
69
- "Usually benign and requires no treatment.",
70
- "Monitor for any change in shape or color."
71
- ],
72
- "medications": ["No medication necessary unless changes occur."]
73
- },
74
- "Basal cell carcinoma": {
75
- "solutions": [
76
- "Surgical excision or Mohs surgery.",
77
- "Topical treatments if superficial.",
78
- "Radiation in select cases."
79
- ],
80
- "medications": ["Imiquimod cream", "Fluorouracil cream", "Vismodegib"]
81
- },
82
- "Actinic keratosis": {
83
- "solutions": [
84
- "Cryotherapy or topical treatments.",
85
- "Avoid prolonged sun exposure.",
86
- "Use of sunscreen regularly."
87
- ],
88
- "medications": ["Fluorouracil", "Imiquimod", "Diclofenac gel"]
89
- },
90
- "Benign keratosis": {
91
- "solutions": [
92
- "Generally harmless and often left untreated.",
93
- "Can be removed for cosmetic reasons."
94
- ],
95
- "medications": ["No medication required unless infected."]
96
- },
97
- "Dermatofibroma": {
98
- "solutions": [
99
- "Benign skin growth, no treatment needed.",
100
- "Surgical removal if painful or for cosmetic reasons."
101
- ],
102
- "medications": ["No medication needed."]
103
- },
104
- "Vascular lesion": {
105
- "solutions": [
106
- "Treatment depends on type (e.g., hemangioma).",
107
- "Laser therapy is commonly used.",
108
- "Observation if no complications."
109
- ],
110
- "medications": ["Beta-blockers (e.g., propranolol for hemangioma)"]
111
- },
112
- "Squamous cell carcinoma": {
113
- "solutions": [
114
- "Surgical removal is standard.",
115
- "Follow-up for recurrence or metastasis.",
116
- "Avoid sun exposure and use sunscreen."
117
- ],
118
- "medications": ["Fluorouracil", "Cisplatin", "Imiquimod"]
119
- },
120
- "Low confidence": {
121
- "solutions": [
122
- "The image is not confidently classified.",
123
- "Please upload a clearer image or consult a doctor."
124
- ],
125
- "medications": ["Not available due to low confidence."]
126
- },
127
- "Unknown": {
128
- "solutions": ["No specific guidance available."],
129
- "medications": ["N/A"]
130
- }
131
- }
132
-
133
- # Logger
134
- logging.basicConfig(level=logging.INFO)
135
- logger = logging.getLogger(__name__)
136
-
137
- # Database Models
138
- class User(db.Model):
139
- id = db.Column(db.Integer, primary_key=True)
140
- name = db.Column(db.String(100), nullable=False)
141
- email = db.Column(db.String(120), unique=True, nullable=False)
142
- scans = db.relationship('Scan', backref='user', lazy=True)
143
-
144
- class Scan(db.Model):
145
- id = db.Column(db.Integer, primary_key=True)
146
- user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
147
- patient_name = db.Column(db.String(100), nullable=False)
148
- patient_gender = db.Column(db.String(20), nullable=False)
149
- patient_age = db.Column(db.Integer, nullable=False)
150
- prediction = db.Column(db.String(100), nullable=False)
151
- confidence = db.Column(db.String(20), nullable=False)
152
- timestamp = db.Column(db.DateTime, default=datetime.utcnow)
153
- image_filename = db.Column(db.String(100), nullable=False)
154
-
155
- # Load Model
156
- try:
157
- logger.info("Loading model from %s", MODEL_PATH)
158
- model = tf.keras.models.load_model(MODEL_PATH)
159
- except Exception as e:
160
- logger.error("Failed to load model: %s", str(e))
161
- raise
162
-
163
- # Plot training history
164
- if os.path.exists(HISTORY_PATH):
165
- try:
166
- with open(HISTORY_PATH, "rb") as f:
167
- history_dict = pickle.load(f)
168
- if "accuracy" in history_dict and "val_accuracy" in history_dict:
169
- os.makedirs("/tmp/static", exist_ok=True)
170
- plt.plot(history_dict['accuracy'], label='Train Accuracy')
171
- plt.plot(history_dict['val_accuracy'], label='Val Accuracy')
172
- plt.xlabel('Epochs')
173
- plt.ylabel('Accuracy')
174
- plt.title('Training History')
175
- plt.legend()
176
- plt.grid(True)
177
- plt.savefig(PLOT_PATH)
178
- plt.close()
179
- logger.info("Training plot saved at %s", PLOT_PATH)
180
- except Exception as e:
181
- logger.error("Training history load error: %s", str(e))
182
-
183
- def preprocess_image(image_bytes):
184
- image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
185
- image = image.resize(IMG_SIZE)
186
- image_array = tf.keras.utils.img_to_array(image)
187
- return np.expand_dims(image_array, axis=0) / 255.0
188
-
189
- def generate_pdf(report, filepath):
190
- c = canvas.Canvas(filepath, pagesize=A4)
191
- width, height = A4
192
- y = height - 60
193
-
194
- # Background
195
- c.setFillColor(colors.Color(0.98, 0.98, 0.99, alpha=1))
196
- c.rect(0, 0, width, height, fill=1, stroke=0)
197
-
198
- # Header background
199
- c.setFillColor(colors.Color(0.94, 0.96, 0.98, alpha=1))
200
- c.rect(0, height-120, width, 120, fill=1, stroke=0)
201
-
202
- # Logo from root directory - square JPG format
203
- try:
204
- logo_path = "./logo.jpg"
205
- if os.path.exists(logo_path):
206
- c.setFillColor(colors.white)
207
- c.rect(65, y-25, 50, 50, fill=1, stroke=1)
208
- c.setStrokeColor(colors.Color(0.7, 0.7, 0.7, alpha=1))
209
- c.setLineWidth(1)
210
- c.rect(65, y-25, 50, 50, fill=0, stroke=1)
211
- c.drawImage(logo_path, 67, y-23, width=46, height=46, preserveAspectRatio=True, mask='auto')
212
- except Exception as e:
213
- logger.warning("Logo error: %s", str(e))
214
-
215
- # Professional title
216
- c.setFont("Helvetica-Bold", 22)
217
- c.setFillColor(colors.Color(0.2, 0.2, 0.2, alpha=1))
218
- c.drawCentredString(width / 2, y + 5, "Medical Diagnosis Report")
219
-
220
- # Subtitle
221
- c.setFont("Helvetica", 11)
222
- c.setFillColor(colors.Color(0.5, 0.5, 0.5, alpha=1))
223
- c.drawCentredString(width / 2, y - 15, "Dermatological Analysis")
224
-
225
- # Professional line
226
- c.setStrokeColor(colors.Color(0.8, 0.8, 0.8, alpha=1))
227
- c.setLineWidth(1)
228
- c.line(80, y - 35, width - 80, y - 35)
229
-
230
- y -= 80
231
-
232
- def professional_section_box(title, fields, extra_gap=20):
233
- nonlocal y
234
-
235
- box_height = len(fields) * 20 + 40
236
- c.setFillColor(colors.Color(0.96, 0.96, 0.96, alpha=0.3))
237
- c.rect(42, y - box_height - 2, width - 84, box_height, fill=1, stroke=0)
238
-
239
- c.setFillColor(colors.white)
240
- c.rect(40, y - box_height, width - 80, box_height, fill=1, stroke=1)
241
- c.setStrokeColor(colors.Color(0.9, 0.9, 0.9, alpha=1))
242
-
243
- c.setFillColor(colors.Color(0.95, 0.95, 0.95, alpha=1))
244
- c.rect(40, y - 30, width - 80, 30, fill=1, stroke=0)
245
-
246
- c.setFont("Helvetica-Bold", 12)
247
- c.setFillColor(colors.Color(0.3, 0.3, 0.3, alpha=1))
248
- c.drawString(55, y - 20, title)
249
-
250
- y -= 45
251
- c.setFont("Helvetica", 10)
252
- c.setFillColor(colors.Color(0.2, 0.2, 0.2, alpha=1))
253
-
254
- for label, val in fields.items():
255
- c.setFont("Helvetica-Bold", 9)
256
- c.setFillColor(colors.Color(0.4, 0.4, 0.4, alpha=1))
257
- c.drawString(55, y, f"{label}:")
258
- c.setFont("Helvetica", 9)
259
- c.setFillColor(colors.Color(0.2, 0.2, 0.2, alpha=1))
260
- c.drawString(150, y, str(val))
261
- y -= 20
262
-
263
- y -= extra_gap
264
-
265
- professional_section_box("Patient Information", {
266
- "Name": report["name"],
267
- "Email": report["email"],
268
- "Gender": report["gender"],
269
- "Age": f"{report['age']} years"
270
- })
271
-
272
- confidence_val = float(report["confidence"].replace('%', ''))
273
- confidence_text = f"{report['confidence']} ({'High' if confidence_val > 85 else 'Moderate' if confidence_val > 70 else 'Low'} Confidence)"
274
-
275
- professional_section_box("Diagnostic Results", {
276
- "Condition": report["prediction"],
277
- "Confidence": confidence_text,
278
- "Notes": report["message"] if report["message"] else "No additional notes"
279
- })
280
-
281
- disease = report["prediction"]
282
- treatment = recommendations.get(disease, recommendations["Unknown"])
283
-
284
- professional_section_box("Treatment Recommendations", {
285
- f"{i+1}. {line}": "" for i, line in enumerate(treatment["solutions"])
286
- })
287
-
288
- professional_section_box("Medication Guidelines", {
289
- f"{i+1}. {line}": "" for i, line in enumerate(treatment["medications"])
290
- })
291
-
292
- c.setFillColor(colors.Color(0.98, 0.98, 0.98, alpha=1))
293
- c.rect(40, 40, width - 80, 70, fill=1, stroke=1)
294
- c.setStrokeColor(colors.Color(0.9, 0.9, 0.9, alpha=1))
295
-
296
- c.setFont("Helvetica-Bold", 10)
297
- c.setFillColor(colors.Color(0.4, 0.4, 0.4, alpha=1))
298
- c.drawString(50, 95, "Medical Disclaimer")
299
-
300
- c.setFont("Helvetica", 8)
301
- c.setFillColor(colors.Color(0.3, 0.3, 0.3, alpha=1))
302
- disclaimer_lines = [
303
- "This report is generated using AI technology for preliminary assessment purposes only.",
304
- "Results should not replace professional medical consultation and diagnosis.",
305
- "Please consult a qualified healthcare provider for comprehensive medical evaluation."
306
- ]
307
-
308
- for i, line in enumerate(disclaimer_lines):
309
- c.drawString(50, 80 - (i * 10), line)
310
-
311
- c.save()
312
-
313
- @app.route("/")
314
- def home():
315
- return redirect(url_for("form"))
316
-
317
- @app.route("/form")
318
- def form():
319
- return render_template("form.html", history_plot="/training_plot.png")
320
-
321
- @app.route("/training_plot.png")
322
- def training_plot():
323
- return send_file(PLOT_PATH, mimetype="image/png")
324
-
325
- @app.route("/api/history")
326
- def api_history():
327
- user_email = request.args.get('email')
328
- if not user_email:
329
- return jsonify({"error": "Email parameter is required"}), 400
330
-
331
- user = User.query.filter_by(email=user_email).first()
332
- if not user:
333
- return jsonify([])
334
-
335
- scans = Scan.query.filter_by(user_id=user.id).order_by(Scan.timestamp.desc()).all()
336
-
337
- history_data = []
338
- for scan in scans:
339
- image_url = url_for('uploaded_file', filename=scan.image_filename, _external=True)
340
- history_data.append({
341
- "id": scan.id,
342
- "prediction": scan.prediction,
343
- "confidence": scan.confidence,
344
- "timestamp": scan.timestamp.strftime("%B %d, %Y at %I:%M %p"),
345
- "patient_name": scan.patient_name,
346
- "image_url": image_url
347
- })
348
-
349
- return jsonify(history_data)
350
-
351
- @app.route("/api/email-report/<int:scan_id>")
352
- def email_report(scan_id):
353
- scan = Scan.query.get(scan_id)
354
- if not scan:
355
- return jsonify({"error": "Report not found"}), 404
356
-
357
- try:
358
- report_data = {
359
- "name": scan.user.name,
360
- "email": scan.user.email,
361
- "gender": scan.patient_gender,
362
- "age": scan.patient_age,
363
- "prediction": scan.prediction,
364
- "confidence": scan.confidence,
365
- "message": ""
366
- }
367
- pdf_path = f"/tmp/report_{scan_id}.pdf"
368
- generate_pdf(report_data, pdf_path)
369
-
370
- msg = Message(
371
- 'Your SnapSkin Diagnostic Report',
372
- sender=app.config['MAIL_USERNAME'],
373
- recipients=[scan.user.email]
374
- )
375
- msg.body = f"Dear {scan.user.name},\n\nPlease find your requested diagnostic report attached.\n\nThank you for using SnapSkin."
376
- with app.open_resource(pdf_path) as fp:
377
- msg.attach(f"SnapSkin_Report_{scan_id}.pdf", "application/pdf", fp.read())
378
-
379
- mail.send(msg)
380
- os.remove(pdf_path)
381
-
382
- return jsonify({"success": True, "message": f"Report sent to {scan.user.email}"})
383
-
384
- except Exception as e:
385
- logger.error(f"Failed to send email for scan {scan_id}: {e}")
386
- return jsonify({"success": False, "message": "Failed to send email."}), 500
387
-
388
- @app.route("/predict", methods=["POST"])
389
- def predict():
390
- try:
391
- if "image" not in request.files:
392
- raise ValueError("No image uploaded.")
393
- image = request.files["image"]
394
- image_bytes = image.read()
395
- img_array = preprocess_image(image_bytes)
396
- prediction = model.predict(img_array)[0]
397
- predicted_index = int(np.argmax(prediction))
398
- confidence = float(prediction[predicted_index])
399
- label = label_map.get(predicted_index, "Unknown") if confidence >= CONFIDENCE_THRESHOLD else "Low confidence"
400
- msg = "⚠ This image is not confidently recognized. Please upload a clearer image." if confidence < CONFIDENCE_THRESHOLD else ""
401
-
402
- # Save user and scan data
403
- email = request.form.get("email")
404
- user = User.query.filter_by(email=email).first()
405
- if not user:
406
- user = User(name=request.form.get("name"), email=email)
407
- db.session.add(user)
408
- db.session.commit()
409
-
410
- # Save image
411
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
412
- image_filename = f"scan_{timestamp}.jpg"
413
- image_path = os.path.join("static/uploads", image_filename)
414
- os.makedirs("static/uploads", exist_ok=True)
415
- image.seek(0)
416
- image.save(image_path)
417
-
418
- scan = Scan(
419
- user_id=user.id,
420
- patient_name=request.form.get("name"),
421
- patient_gender=request.form.get("gender"),
422
- patient_age=int(request.form.get("age")),
423
- prediction=label,
424
- confidence=f"{confidence * 100:.2f}%",
425
- image_filename=image_filename
426
- )
427
- db.session.add(scan)
428
- db.session.commit()
429
-
430
- report = {
431
- "name": request.form.get("name"),
432
- "email": email,
433
- "gender": request.form.get("gender"),
434
- "age": request.form.get("age"),
435
- "prediction": label,
436
- "confidence": f"{confidence * 100:.2f}%",
437
- "message": msg,
438
- "scan_id": scan.id
439
- }
440
- session["report"] = report
441
-
442
- # Send email automatically
443
- try:
444
- pdf_path = f"/tmp/report_{scan.id}.pdf"
445
- generate_pdf(report, pdf_path)
446
- msg = Message(
447
- 'Your SnapSkin Diagnostic Report',
448
- sender=app.config['MAIL_USERNAME'],
449
- recipients=[email]
450
- )
451
- msg.body = f"Dear {report['name']},\n\nPlease find your diagnostic report attached.\n\nThank you for using SnapSkin."
452
- with app.open_resource(pdf_path) as fp:
453
- msg.attach(f"SnapSkin_Report_{scan.id}.pdf", "application/pdf", fp.read())
454
- mail.send(msg)
455
- os.remove(pdf_path)
456
- report["email_status"] = "Report sent to your email."
457
- except Exception as e:
458
- logger.error(f"Failed to send email: {e}")
459
- report["email_status"] = "Failed to send report to email."
460
-
461
- return redirect(url_for("result"))
462
- except Exception as e:
463
- return render_template("form.html", history_plot="/training_plot.png", result={
464
- "prediction": "Error",
465
- "confidence": "N/A",
466
- "message": str(e),
467
- "email_status": "Error occurred, no email sent."
468
- })
469
-
470
- @app.route("/result")
471
- def result():
472
- report = session.get("report", {})
473
- return render_template("result.html", **report)
474
-
475
- @app.route("/download-report")
476
- def download_report():
477
- report = session.get("report", {})
478
- if not report:
479
- return redirect(url_for("form"))
480
- os.makedirs("/tmp/reports", exist_ok=True)
481
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
482
- filepath = f"/tmp/reports/report_{timestamp}.pdf"
483
- generate_pdf(report, filepath)
484
- return send_file(filepath, as_attachment=True)
485
-
486
- @app.route("/uploads/<filename>")
487
- def uploaded_file(filename):
488
- return send_file(os.path.join("static/uploads", filename))
489
-
490
- if __name__ == "__main__":
491
- with app.app_context():
492
- db.create_all()
493
- app.run(host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ os.environ['MPLCONFIGDIR'] = '/tmp/matplotlib'
3
+ from flask import Flask, render_template, request, redirect, url_for, session, send_file
4
+ from flask_sqlalchemy import SQLAlchemy
5
+ from flask_migrate import Migrate
6
+ import tensorflow as tf
7
+ import numpy as np
8
+ from PIL import Image
9
+ import pickle
10
+ import io
11
+ import os
12
+ import matplotlib.pyplot as plt
13
+ from reportlab.lib.pagesizes import A4
14
+ from reportlab.lib import colors
15
+ from reportlab.pdfgen import canvas
16
+ from reportlab.lib.units import inch
17
+ from datetime import datetime
18
+ import logging
19
+ from flask_mail import Mail, Message
20
+ from flask import jsonify, url_for
21
+
22
+ app = Flask(__name__)
23
+ app.secret_key = "e3f6f40bb8b2471b9f07c4025d845be9"
24
+
25
+ # Database configuration
26
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/snapsin.db'
27
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
28
+ db = SQLAlchemy(app)
29
+ migrate = Migrate(app, db)
30
+
31
+ # Mail configuration
32
+ app.config['MAIL_SERVER'] = 'smtp.gmail.com'
33
+ app.config['MAIL_PORT'] = 465
34
+ app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
35
+ app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
36
+ app.config['MAIL_USE_TLS'] = False
37
+ app.config['MAIL_USE_SSL'] = True
38
+ mail = Mail(app)
39
+
40
+ MODEL_PATH = "skin_lesion_model.h5"
41
+ HISTORY_PATH = "training_history.pkl"
42
+ PLOT_PATH = "/tmp/static/training_plot.png"
43
+ LOGO_PATH = "static/logo.jpg"
44
+ IMG_SIZE = (224, 224)
45
+ CONFIDENCE_THRESHOLD = 0.30
46
+
47
+ label_map = {
48
+ 0: "Melanoma",
49
+ 1: "Melanocytic nevus",
50
+ 2: "Basal cell carcinoma",
51
+ 3: "Actinic keratosis",
52
+ 4: "Benign keratosis",
53
+ 5: "Dermatofibroma",
54
+ 6: "Vascular lesion",
55
+ 7: "Squamous cell carcinoma"
56
+ }
57
+
58
+ recommendations = {
59
+ "Melanoma": {
60
+ "solutions": [
61
+ "Consult a dermatologist immediately.",
62
+ "Surgical removal is typically required.",
63
+ "Regular follow-up and screening for metastasis."
64
+ ],
65
+ "medications": ["Interferon alfa-2b", "Vemurafenib", "Dacarbazine"]
66
+ },
67
+ "Melanocytic nevus": {
68
+ "solutions": [
69
+ "Usually benign and requires no treatment.",
70
+ "Monitor for any change in shape or color."
71
+ ],
72
+ "medications": ["No medication necessary unless changes occur."]
73
+ },
74
+ "Basal cell carcinoma": {
75
+ "solutions": [
76
+ "Surgical excision or Mohs surgery.",
77
+ "Topical treatments if superficial.",
78
+ "Radiation in select cases."
79
+ ],
80
+ "medications": ["Imiquimod cream", "Fluorouracil cream", "Vismodegib"]
81
+ },
82
+ "Actinic keratosis": {
83
+ "solutions": [
84
+ "Cryotherapy or topical treatments.",
85
+ "Avoid prolonged sun exposure.",
86
+ "Use of sunscreen regularly."
87
+ ],
88
+ "medications": ["Fluorouracil", "Imiquimod", "Diclofenac gel"]
89
+ },
90
+ "Benign keratosis": {
91
+ "solutions": [
92
+ "Generally harmless and often left untreated.",
93
+ "Can be removed for cosmetic reasons."
94
+ ],
95
+ "medications": ["No medication required unless infected."]
96
+ },
97
+ "Dermatofibroma": {
98
+ "solutions": [
99
+ "Benign skin growth, no treatment needed.",
100
+ "Surgical removal if painful or for cosmetic reasons."
101
+ ],
102
+ "medications": ["No medication needed."]
103
+ },
104
+ "Vascular lesion": {
105
+ "solutions": [
106
+ "Treatment depends on type (e.g., hemangioma).",
107
+ "Laser therapy is commonly used.",
108
+ "Observation if no complications."
109
+ ],
110
+ "medications": ["Beta-blockers (e.g., propranolol for hemangioma)"]
111
+ },
112
+ "Squamous cell carcinoma": {
113
+ "solutions": [
114
+ "Surgical removal is standard.",
115
+ "Follow-up for recurrence or metastasis.",
116
+ "Avoid sun exposure and use sunscreen."
117
+ ],
118
+ "medications": ["Fluorouracil", "Cisplatin", "Imiquimod"]
119
+ },
120
+ "Low confidence": {
121
+ "solutions": [
122
+ "The image is not confidently classified.",
123
+ "Please upload a clearer image or consult a doctor."
124
+ ],
125
+ "medications": ["Not available due to low confidence."]
126
+ },
127
+ "Unknown": {
128
+ "solutions": ["No specific guidance available."],
129
+ "medications": ["N/A"]
130
+ }
131
+ }
132
+
133
+ # Logger
134
+ logging.basicConfig(level=logging.INFO)
135
+ logger = logging.getLogger(__name__)
136
+
137
+ # Database Models
138
+ class User(db.Model):
139
+ id = db.Column(db.Integer, primary_key=True)
140
+ name = db.Column(db.String(100), nullable=False)
141
+ email = db.Column(db.String(120), unique=True, nullable=False)
142
+ scans = db.relationship('Scan', backref='user', lazy=True)
143
+
144
+ class Scan(db.Model):
145
+ id = db.Column(db.Integer, primary_key=True)
146
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
147
+ patient_name = db.Column(db.String(100), nullable=False)
148
+ patient_gender = db.Column(db.String(20), nullable=False)
149
+ patient_age = db.Column(db.Integer, nullable=False)
150
+ prediction = db.Column(db.String(100), nullable=False)
151
+ confidence = db.Column(db.String(20), nullable=False)
152
+ timestamp = db.Column(db.DateTime, default=datetime.utcnow)
153
+ image_filename = db.Column(db.String(100), nullable=False)
154
+
155
+ # Load Model
156
+ model = None
157
+ try:
158
+ if os.path.exists(MODEL_PATH):
159
+ logger.info("Loading model from %s", MODEL_PATH)
160
+ model = tf.keras.models.load_model(MODEL_PATH)
161
+ else:
162
+ logger.error("Model file %s not found", MODEL_PATH)
163
+ raise FileNotFoundError(f"Model file {MODEL_PATH} not found")
164
+ except Exception as e:
165
+ logger.error("Failed to load model: %s", str(e))
166
+ raise
167
+
168
+ # Plot training history
169
+ if os.path.exists(HISTORY_PATH):
170
+ try:
171
+ with open(HISTORY_PATH, "rb") as f:
172
+ history_dict = pickle.load(f)
173
+ if "accuracy" in history_dict and "val_accuracy" in history_dict:
174
+ os.makedirs("/tmp/static", exist_ok=True)
175
+ plt.plot(history_dict['accuracy'], label='Train Accuracy')
176
+ plt.plot(history_dict['val_accuracy'], label='Val Accuracy')
177
+ plt.xlabel('Epochs')
178
+ plt.ylabel('Accuracy')
179
+ plt.title('Training History')
180
+ plt.legend()
181
+ plt.grid(True)
182
+ plt.savefig(PLOT_PATH)
183
+ plt.close()
184
+ logger.info("Training plot saved at %s", PLOT_PATH)
185
+ else:
186
+ logger.warning("Invalid training history data in %s", HISTORY_PATH)
187
+ except Exception as e:
188
+ logger.warning("Training history load error: %s", str(e))
189
+ else:
190
+ logger.warning("Training history file %s not found", HISTORY_PATH)
191
+
192
+ def preprocess_image(image_bytes):
193
+ try:
194
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
195
+ image = image.resize(IMG_SIZE)
196
+ image_array = tf.keras.utils.img_to_array(image)
197
+ return np.expand_dims(image_array, axis=0) / 255.0
198
+ except Exception as e:
199
+ logger.error("Image preprocessing error: %s", str(e))
200
+ raise
201
+
202
+ def generate_pdf(report, filepath):
203
+ try:
204
+ c = canvas.Canvas(filepath, pagesize=A4)
205
+ width, height = A4
206
+ y = height - 60
207
+
208
+ # Background
209
+ c.setFillColor(colors.Color(0.98, 0.98, 0.99, alpha=1))
210
+ c.rect(0, 0, width, height, fill=1, stroke=0)
211
+
212
+ # Header background
213
+ c.setFillColor(colors.Color(0.94, 0.96, 0.98, alpha=1))
214
+ c.rect(0, height-120, width, 120, fill=1, stroke=0)
215
+
216
+ # Logo from root directory - square JPG format
217
+ try:
218
+ if os.path.exists(LOGO_PATH):
219
+ c.setFillColor(colors.white)
220
+ c.rect(65, y-25, 50, 50, fill=1, stroke=1)
221
+ c.setStrokeColor(colors.Color(0.7, 0.7, 0.7, alpha=1))
222
+ c.setLineWidth(1)
223
+ c.rect(65, y-25, 50, 50, fill=0, stroke=1)
224
+ c.drawImage(LOGO_PATH, 67, y-23, width=46, height=46, preserveAspectRatio=True, mask='auto')
225
+ else:
226
+ logger.warning("Logo file %s not found, skipping logo", LOGO_PATH)
227
+ except Exception as e:
228
+ logger.warning("Logo error: %s", str(e))
229
+
230
+ # Professional title
231
+ c.setFont("Helvetica-Bold", 22)
232
+ c.setFillColor(colors.Color(0.2, 0.2, 0.2, alpha=1))
233
+ c.drawCentredString(width / 2, y + 5, "Medical Diagnosis Report")
234
+
235
+ # Subtitle
236
+ c.setFont("Helvetica", 11)
237
+ c.setFillColor(colors.Color(0.5, 0.5, 0.5, alpha=1))
238
+ c.drawCentredString(width / 2, y - 15, "Dermatological Analysis")
239
+
240
+ # Professional line
241
+ c.setStrokeColor(colors.Color(0.8, 0.8, 0.8, alpha=1))
242
+ c.setLineWidth(1)
243
+ c.line(80, y - 35, width - 80, y - 35)
244
+
245
+ y -= 80
246
+
247
+ def professional_section_box(title, fields, extra_gap=20):
248
+ nonlocal y
249
+
250
+ box_height = len(fields) * 20 + 40
251
+ c.setFillColor(colors.Color(0.96, 0.96, 0.96, alpha=0.3))
252
+ c.rect(42, y - box_height - 2, width - 84, box_height, fill=1, stroke=0)
253
+
254
+ c.setFillColor(colors.white)
255
+ c.rect(40, y - box_height, width - 80, box_height, fill=1, stroke=1)
256
+ c.setStrokeColor(colors.Color(0.9, 0.9, 0.9, alpha=1))
257
+
258
+ c.setFillColor(colors.Color(0.95, 0.95, 0.95, alpha=1))
259
+ c.rect(40, y - 30, width - 80, 30, fill=1, stroke=0)
260
+
261
+ c.setFont("Helvetica-Bold", 12)
262
+ c.setFillColor(colors.Color(0.3, 0.3, 0.3, alpha=1))
263
+ c.drawString(55, y - 20, title)
264
+
265
+ y -= 45
266
+ c.setFont("Helvetica", 10)
267
+ c.setFillColor(colors.Color(0.2, 0.2, 0.2, alpha=1))
268
+
269
+ for label, val in fields.items():
270
+ c.setFont("Helvetica-Bold", 9)
271
+ c.setFillColor(colors.Color(0.4, 0.4, 0.4, alpha=1))
272
+ c.drawString(55, y, f"{label}:")
273
+ c.setFont("Helvetica", 9)
274
+ c.setFillColor(colors.Color(0.2, 0.2, 0.2, alpha=1))
275
+ c.drawString(150, y, str(val))
276
+ y -= 20
277
+
278
+ y -= extra_gap
279
+
280
+ professional_section_box("Patient Information", {
281
+ "Name": report["name"],
282
+ "Email": report["email"],
283
+ "Gender": report["gender"],
284
+ "Age": f"{report['age']} years"
285
+ })
286
+
287
+ confidence_val = float(report["confidence"].replace('%', ''))
288
+ confidence_text = f"{report['confidence']} ({'High' if confidence_val > 85 else 'Moderate' if confidence_val > 70 else 'Low'} Confidence)"
289
+
290
+ professional_section_box("Diagnostic Results", {
291
+ "Condition": report["prediction"],
292
+ "Confidence": confidence_text,
293
+ "Notes": report["message"] if report["message"] else "No additional notes"
294
+ })
295
+
296
+ disease = report["prediction"]
297
+ treatment = recommendations.get(disease, recommendations["Unknown"])
298
+
299
+ professional_section_box("Treatment Recommendations", {
300
+ f"{i+1}. {line}": "" for i, line in enumerate(treatment["solutions"])
301
+ })
302
+
303
+ professional_section_box("Medication Guidelines", {
304
+ f"{i+1}. {line}": "" for i, line in enumerate(treatment["medications"])
305
+ })
306
+
307
+ c.setFillColor(colors.Color(0.98, 0.98, 0.98, alpha=1))
308
+ c.rect(40, 40, width - 80, 70, fill=1, stroke=1)
309
+ c.setStrokeColor(colors.Color(0.9, 0.9, 0.9, alpha=1))
310
+
311
+ c.setFont("Helvetica-Bold", 10)
312
+ c.setFillColor(colors.Color(0.4, 0.4, 0.4, alpha=1))
313
+ c.drawString(50, 95, "Medical Disclaimer")
314
+
315
+ c.setFont("Helvetica", 8)
316
+ c.setFillColor(colors.Color(0.3, 0.3, 0.3, alpha=1))
317
+ disclaimer_lines = [
318
+ "This report is generated using AI technology for preliminary assessment purposes only.",
319
+ "Results should not replace professional medical consultation and diagnosis.",
320
+ "Please consult a qualified healthcare provider for comprehensive medical evaluation."
321
+ ]
322
+
323
+ for i, line in enumerate(disclaimer_lines):
324
+ c.drawString(50, 80 - (i * 10), line)
325
+
326
+ c.save()
327
+ except Exception as e:
328
+ logger.error("PDF generation error: %s", str(e))
329
+ raise
330
+
331
+ @app.route("/")
332
+ def home():
333
+ return redirect(url_for("form"))
334
+
335
+ @app.route("/form")
336
+ def form():
337
+ try:
338
+ return render_template("form.html", history_plot="/training_plot.png")
339
+ except Exception as e:
340
+ logger.error("Error rendering form: %s", str(e))
341
+ return render_template("form.html", history_plot=None, result={
342
+ "prediction": "Error",
343
+ "confidence": "N/A",
344
+ "message": "Failed to load form. Please try again.",
345
+ "email_status": "N/A"
346
+ })
347
+
348
+ @app.route("/training_plot.png")
349
+ def training_plot():
350
+ try:
351
+ if os.path.exists(PLOT_PATH):
352
+ return send_file(PLOT_PATH, mimetype="image/png")
353
+ else:
354
+ logger.warning("Training plot %s not found", PLOT_PATH)
355
+ return "", 404
356
+ except Exception as e:
357
+ logger.error("Error serving training plot: %s", str(e))
358
+ return "", 500
359
+
360
+ @app.route("/api/history")
361
+ def api_history():
362
+ try:
363
+ user_email = request.args.get('email')
364
+ if not user_email:
365
+ return jsonify({"error": "Email parameter is required"}), 400
366
+
367
+ user = User.query.filter_by(email=user_email).first()
368
+ if not user:
369
+ return jsonify([])
370
+
371
+ scans = Scan.query.filter_by(user_id=user.id).order_by(Scan.timestamp.desc()).all()
372
+
373
+ history_data = []
374
+ for scan in scans:
375
+ image_url = url_for('uploaded_file', filename=scan.image_filename, _external=True)
376
+ history_data.append({
377
+ "id": scan.id,
378
+ "prediction": scan.prediction,
379
+ "confidence": scan.confidence,
380
+ "timestamp": scan.timestamp.strftime("%B %d, %Y at %I:%M %p"),
381
+ "patient_name": scan.patient_name,
382
+ "image_url": image_url
383
+ })
384
+
385
+ return jsonify(history_data)
386
+ except Exception as e:
387
+ logger.error("Error in history API: %s", str(e))
388
+ return jsonify({"error": "Internal server error"}), 500
389
+
390
+ @app.route("/api/email-report/<int:scan_id>")
391
+ def email_report(scan_id):
392
+ try:
393
+ scan = Scan.query.get(scan_id)
394
+ if not scan:
395
+ return jsonify({"error": "Report not found"}), 404
396
+
397
+ report_data = {
398
+ "name": scan.user.name,
399
+ "email": scan.user.email,
400
+ "gender": scan.patient_gender,
401
+ "age": scan.patient_age,
402
+ "prediction": scan.prediction,
403
+ "confidence": scan.confidence,
404
+ "message": ""
405
+ }
406
+ pdf_path = f"/tmp/report_{scan_id}.pdf"
407
+ generate_pdf(report_data, pdf_path)
408
+
409
+ msg = Message(
410
+ 'Your SnapSkin Diagnostic Report',
411
+ sender=app.config['MAIL_USERNAME'],
412
+ recipients=[scan.user.email]
413
+ )
414
+ msg.body = f"Dear {scan.user.name},\n\nPlease find your requested diagnostic report attached.\n\nThank you for using SnapSkin."
415
+ with app.open_resource(pdf_path) as fp:
416
+ msg.attach(f"SnapSkin_Report_{scan_id}.pdf", "application/pdf", fp.read())
417
+
418
+ mail.send(msg)
419
+ os.remove(pdf_path)
420
+
421
+ return jsonify({"success": True, "message": f"Report sent to {scan.user.email}"})
422
+ except Exception as e:
423
+ logger.error(f"Failed to send email for scan {scan_id}: {e}")
424
+ return jsonify({"success": False, "message": "Failed to send email."}), 500
425
+
426
+ @app.route("/predict", methods=["POST"])
427
+ def predict():
428
+ try:
429
+ if "image" not in request.files:
430
+ raise ValueError("No image uploaded.")
431
+ image = request.files["image"]
432
+ image_bytes = image.read()
433
+ img_array = preprocess_image(image_bytes)
434
+ prediction = model.predict(img_array)[0]
435
+ predicted_index = int(np.argmax(prediction))
436
+ confidence = float(prediction[predicted_index])
437
+ label = label_map.get(predicted_index, "Unknown") if confidence >= CONFIDENCE_THRESHOLD else "Low confidence"
438
+ msg = "⚠ This image is not confidently recognized. Please upload a clearer image." if confidence < CONFIDENCE_THRESHOLD else ""
439
+
440
+ # Save user and scan data
441
+ email = request.form.get("email")
442
+ user = User.query.filter_by(email=email).first()
443
+ if not user:
444
+ user = User(name=request.form.get("name"), email=email)
445
+ db.session.add(user)
446
+ db.session.commit()
447
+
448
+ # Save image
449
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
450
+ image_filename = f"scan_{timestamp}.jpg"
451
+ image_path = os.path.join("static/uploads", image_filename)
452
+ os.makedirs("static/uploads", exist_ok=True)
453
+ image.seek(0)
454
+ image.save(image_path)
455
+
456
+ scan = Scan(
457
+ user_id=user.id,
458
+ patient_name=request.form.get("name"),
459
+ patient_gender=request.form.get("gender"),
460
+ patient_age=int(request.form.get("age")),
461
+ prediction=label,
462
+ confidence=f"{confidence * 100:.2f}%",
463
+ image_filename=image_filename
464
+ )
465
+ db.session.add(scan)
466
+ db.session.commit()
467
+
468
+ report = {
469
+ "name": request.form.get("name"),
470
+ "email": email,
471
+ "gender": request.form.get("gender"),
472
+ "age": request.form.get("age"),
473
+ "prediction": label,
474
+ "confidence": f"{confidence * 100:.2f}%",
475
+ "message": msg,
476
+ "scan_id": scan.id
477
+ }
478
+ session["report"] = report
479
+
480
+ # Send email automatically
481
+ try:
482
+ pdf_path = f"/tmp/report_{scan.id}.pdf"
483
+ generate_pdf(report, pdf_path)
484
+ msg = Message(
485
+ 'Your SnapSkin Diagnostic Report',
486
+ sender=app.config['MAIL_USERNAME'],
487
+ recipients=[email]
488
+ )
489
+ msg.body = f"Dear {report['name']},\n\nPlease find your diagnostic report attached.\n\nThank you for using SnapSkin."
490
+ with app.open_resource(pdf_path) as fp:
491
+ msg.attach(f"SnapSkin_Report_{scan.id}.pdf", "application/pdf", fp.read())
492
+ mail.send(msg)
493
+ os.remove(pdf_path)
494
+ report["email_status"] = "Report sent to your email."
495
+ except Exception as e:
496
+ logger.error(f"Failed to send email: {e}")
497
+ report["email_status"] = "Failed to send report to email."
498
+
499
+ return redirect(url_for("result"))
500
+ except Exception as e:
501
+ logger.error("Prediction error: %s", str(e))
502
+ return render_template("form.html", history_plot="/training_plot.png", result={
503
+ "prediction": "Error",
504
+ "confidence": "N/A",
505
+ "message": str(e),
506
+ "email_status": "Error occurred, no email sent."
507
+ })
508
+
509
+ @app.route("/result")
510
+ def result():
511
+ try:
512
+ report = session.get("report", {})
513
+ return render_template("result.html", **report)
514
+ except Exception as e:
515
+ logger.error("Error rendering result: %s", str(e))
516
+ return render_template("form.html", history_plot="/training_plot.png", result={
517
+ "prediction": "Error",
518
+ "confidence": "N/A",
519
+ "message": "Failed to load result. Please try again.",
520
+ "email_status": "N/A"
521
+ })
522
+
523
+ @app.route("/download-report")
524
+ def download_report():
525
+ try:
526
+ report = session.get("report", {})
527
+ if not report:
528
+ return redirect(url_for("form"))
529
+ os.makedirs("/tmp/reports", exist_ok=True)
530
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
531
+ filepath = f"/tmp/reports/report_{timestamp}.pdf"
532
+ generate_pdf(report, filepath)
533
+ return send_file(filepath, as_attachment=True)
534
+ except Exception as e:
535
+ logger.error("Download report error: %s", str(e))
536
+ return redirect(url_for("form"))
537
+
538
+ @app.route("/uploads/<filename>")
539
+ def uploaded_file(filename):
540
+ try:
541
+ file_path = os.path.join("static/uploads", filename)
542
+ if os.path.exists(file_path):
543
+ return send_file(file_path)
544
+ else:
545
+ logger.warning("Image file %s not found", file_path)
546
+ return "", 404
547
+ except Exception as e:
548
+ logger.error("Error serving uploaded file: %s", str(e))
549
+ return "", 500
550
+
551
+ if __name__ == "__main__":
552
+ try:
553
+ with app.app_context():
554
+ db.create_all()
555
+ app.run(host="0.0.0.0", port=7860)
556
+ except Exception as e:
557
+ logger.error("Application startup error: %s", str(e))
558
+ raise