Yash goyal commited on
Commit
5de2fbf
·
verified ·
1 Parent(s): 8c5304a

Update app.py

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