{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "69cff53e",
   "metadata": {},
   "source": [
    "# Ανάλυση Ιατρικών Εικόνων DICOM στην Python\n",
    "\n",
    "\n",
    "---\n",
    "\n",
    "**Δομή του μαθήματος:**\n",
    "\n",
    "| Μέρος | Ενότητες | Περιεχόμενο |\n",
    "|-------|----------|--------------|\n",
    "| **A** | 1–25 | Βασικές έννοιες DICOM, ανάγνωση, tags, οπτικοποίηση, Hounsfield Units, windowing, σειρές, εργαλεία |\n",
    "| **B** | 26–36 | Στατιστική ανάλυση, ιστογράμματα, ποιότητα εικόνας, θόρυβος, αναφορές |\n",
    "\n",
    "**Τι θα μάθετε σε αυτό το notebook:**\n",
    "- Τι είναι ένα αρχείο DICOM και γιατί υπάρχει.\n",
    "- Πώς διαβάζουμε DICOM με την Python και τη βιβλιοθήκη `pydicom`.\n",
    "- Πώς εξερευνούμε metadata (DICOM tags) — δημογραφικά ασθενούς, παράμετροι εξέτασης, χωρικές πληροφορίες.\n",
    "- Πώς οπτικοποιούμε ιατρικές εικόνες σωστά (grayscale, windowing, Hounsfield).\n",
    "- Πώς πλοηγούμαστε σε σειρές εικόνων και χτίζουμε 3D όγκους.\n",
    "- Πώς αναλύουμε στατιστικά την εικόνα και αξιολογούμε ποιότητα.\n",
    "\n",
    "**Τι ΧΡΕΙΑΖΕΤΑΙ να ξέρετε από πριν:**\n",
    "- Βασικά Python (μεταβλητές, συναρτήσεις, κλάσεις).\n",
    "- Στοιχειώδης χρήση Jupyter notebooks.\n",
    "\n",
    "> 💡 **Συμβουλή του διδάσκοντα:** Μην απλώς τρέχετε τα κελιά. Σταματήστε σε καθένα, διαβάστε την ελληνική επεξήγηση, και προσπαθήστε να αναγνωρίσετε τι κάνει ο κώδικας **πριν** δείτε το αποτέλεσμα. Έτσι θα μάθετε πραγματικά.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dad1c902",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "This notebook introduces students to DICOM (Digital Imaging and Communications in Medicine) \n",
    "images - the standard format for medical imaging data.\n",
    "\n",
    "Learning Objectives:\n",
    "1. Understand what DICOM files are\n",
    "2. Load and read DICOM images\n",
    "3. Explore DICOM tags and metadata\n",
    "4. Visualize medical images\n",
    "5. Navigate through image series/slices\n",
    "6. Perform basic image processing\n",
    "\"\"\"\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd88b53f",
   "metadata": {},
   "source": [
    "## Ενότητα 1 — Εγκατάσταση Πακέτων & Εισαγωγή Βιβλιοθηκών\n",
    "\n",
    "### Γιατί χρειαζόμαστε εξωτερικές βιβλιοθήκες;\n",
    "\n",
    "Η Python μόνη της δεν ξέρει τι είναι DICOM, ούτε πώς να φτιάξει διαγράμματα, ούτε πώς να κάνει αποδοτικές πράξεις σε εκατομμύρια αριθμούς. Αυτές τις δουλειές τις αναλαμβάνουν εξειδικευμένες **βιβλιοθήκες** που τις «εισάγουμε» (import) στο πρόγραμμά μας.\n",
    "\n",
    "### Τι κάνει η κάθε βιβλιοθήκη\n",
    "\n",
    "| Βιβλιοθήκη | Ρόλος | Τυπική χρήση |\n",
    "|------------|-------|--------------|\n",
    "| **`pydicom`** | Ανάγνωση/εγγραφή DICOM | `pydicom.dcmread()` |\n",
    "| **`numpy`** (ως `np`) | Αριθμητικοί πίνακες (arrays) | Πράξεις σε pixel |\n",
    "| **`matplotlib.pyplot`** (ως `plt`) | Γραφικά | `plt.imshow()`, `plt.hist()` |\n",
    "| **`pandas`** (ως `pd`) | Πίνακες δεδομένων (DataFrames) | Λίστες tags, αναφορές |\n",
    "| **`pathlib`** / **`os`** | Διαχείριση αρχείων και διαδρομών | Φόρτωση φακέλων |\n",
    "| **`IPython.display`** | Ωραίες εμφανίσεις στο Jupyter | `display(df)` για πίνακες |\n",
    "\n",
    "### Πώς εγκαθίστανται;\n",
    "\n",
    "Αν δεν έχετε ήδη τα πακέτα, ξεκομμένη το σχόλιο στη γραμμή `# !pip install ...` και τρέξτε το κελί. Το `!` λέει στο Jupyter να εκτελέσει εντολή **shell**, όχι Python.\n",
    "\n",
    "### Τι κάνουν οι ρυθμίσεις `plt.rcParams`\n",
    "\n",
    "Είναι **καθολικές προεπιλογές** για όλα τα διαγράμματα του notebook:\n",
    "\n",
    "- `figure.figsize = (12, 8)` — μεγάλα γραφήματα από προεπιλογή.\n",
    "- `image.cmap = 'gray'` — όλες οι εικόνες σε **κλίμακα του γκρι**, που είναι το πρότυπο στις ιατρικές εικόνες.\n",
    "\n",
    "> 📌 **Σημείωση:** Στο ιατρικό περιβάλλον, μην χρησιμοποιείτε χρωματικές κλίμακες (rainbow, jet) για τις ίδιες τις εικόνες — δημιουργούν ψευδαισθήσεις δομών που δεν υπάρχουν. Η γκρι κλίμακα είναι ο χρυσός κανόνας.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef186311",
   "metadata": {},
   "outputs": [],
   "source": [
    "# First, let's install required packages (run this cell first!)\n",
    "# Uncomment the line below if packages are not installed\n",
    "# !pip install pydicom numpy matplotlib pillow pandas\n",
    "\n",
    "import pydicom\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from pathlib import Path\n",
    "import pandas as pd\n",
    "from IPython.display import display, HTML\n",
    "import os\n",
    "import json\n",
    "\n",
    "# Set up matplotlib for better visualization\n",
    "plt.rcParams['figure.figsize'] = (12, 8)\n",
    "plt.rcParams['image.cmap'] = 'gray'\n",
    "\n",
    "print(\"✓ All packages imported successfully!\")\n",
    "print(f\"PyDICOM version: {pydicom.__version__}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "28424233",
   "metadata": {},
   "source": [
    "## Ενότητα 2 — Τι είναι το DICOM;\n",
    "\n",
    "### Από το χάος στο πρότυπο\n",
    "\n",
    "Πριν το DICOM, κάθε κατασκευαστής ιατρικού μηχανήματος (Siemens, GE, Philips...) είχε **δικό του format** για εικόνες. Νοσοκομεία με μηχανήματα από διαφορετικές εταιρείες δεν μπορούσαν να ανταλλάξουν δεδομένα. Το **DICOM** (Digital Imaging and Communications in Medicine) δημιουργήθηκε από τους ACR και NEMA τη δεκαετία του '80 ακριβώς για να λύσει αυτό το πρόβλημα. Σήμερα είναι το **παγκόσμιο πρότυπο**.\n",
    "\n",
    "### Γιατί το DICOM δεν είναι «απλά μια εικόνα»;\n",
    "\n",
    "Ένα αρχείο JPG είναι μόνο pixels. Ένα αρχείο DICOM περιέχει **τρεις συνιστώσες**:\n",
    "\n",
    "1. **Δεδομένα εικόνας** — οι τιμές των pixel (ή voxel σε 3D).\n",
    "2. **Metadata (DICOM tags)** — εκατοντάδες πεδία που περιγράφουν τα πάντα: ποιος ο ασθενής, ποιο μηχάνημα, ποια ημερομηνία, ποιες παράμετροι, ποια θέση στο σώμα...\n",
    "3. **Header** — τεχνικές πληροφορίες για την κωδικοποίηση του αρχείου.\n",
    "\n",
    "> 🎯 **Κρίσιμη σκέψη:** Σε ένα πραγματικό κλινικό σύστημα, αν χάσετε τα metadata δεν μπορείτε να εμπιστευθείτε την εικόνα. Δεν ξέρετε σε ποιον ανήκει, πώς τραβήχτηκε, σε ποια θέση. Χωρίς context, μια εικόνα είναι ιατρικά **άχρηστη**.\n",
    "\n",
    "### Τα modalities που υποστηρίζει\n",
    "\n",
    "Όλα τα ιατρικά απεικονιστικά μηχανήματα παράγουν DICOM:\n",
    "\n",
    "- **CT** (Υπολογιστική Τομογραφία)\n",
    "- **MRI / MR** (Μαγνητική Τομογραφία)\n",
    "- **CR / DX / RF** (Ψηφιακή Ακτινογραφία / Φθοριοσκόπηση)\n",
    "- **US** (Υπερηχογράφημα)\n",
    "- **PET / NM** (Τομογραφία Εκπομπής Ποζιτρονίων / Πυρηνική Ιατρική)\n",
    "- **MG** (Μαστογραφία)\n",
    "- **OCT, Endoscopy, RT...**\n",
    "\n",
    "### Παρατηρήστε στον κώδικα\n",
    "\n",
    "Το κελί απλώς **τυπώνει** μια περιγραφή. Δεν εκτελεί τίποτα τεχνικά. Είναι κελί «θεωρίας» — να το διαβάσετε, όχι να ψάχνετε αλγόριθμο μέσα του.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a8f19988",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                    WHAT IS DICOM?                              ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "DICOM (Digital Imaging and Communications in Medicine) is:\n",
    "- The international standard for medical images and related information\n",
    "- More than just an image format - it contains metadata about the patient,\n",
    "  study, and acquisition parameters\n",
    "- Used by virtually all medical imaging modalities:\n",
    "  * CT (Computed Tomography)\n",
    "  * MRI (Magnetic Resonance Imaging)\n",
    "  * X-Ray\n",
    "  * Ultrasound\n",
    "  * PET (Positron Emission Tomography)\n",
    "  * And many more...\n",
    "\n",
    "Key Components:\n",
    "1. Image Data: The actual pixel/voxel values\n",
    "2. DICOM Tags: Metadata describing the image\n",
    "3. Header: Information about patient, study, series, equipment\n",
    "\"\"\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7d38060a",
   "metadata": {},
   "source": [
    "## Ενότητα 3 — Από πού παίρνουμε δείγματα DICOM;\n",
    "\n",
    "### Τρεις πηγές δεδομένων\n",
    "\n",
    "Για να εξασκηθείτε χρειάζεστε αρχεία DICOM. Υπάρχουν τρεις κύριες πηγές:\n",
    "\n",
    "#### 1. Test data της `pydicom`\n",
    "Η βιβλιοθήκη φορτώνεται μαζί με μερικά **μικρά test αρχεία** (CT, MRI, RT plan κ.ά.) — ιδανικά για διδακτικούς σκοπούς. Τα παίρνουμε με:\n",
    "\n",
    "```python\n",
    "from pydicom.data import get_testdata_file\n",
    "path = get_testdata_file(\"CT_small.dcm\")\n",
    "```\n",
    "\n",
    "#### 2. Δημόσια ερευνητικά datasets\n",
    "- **TCIA (The Cancer Imaging Archive)** — εκατοντάδες χιλιάδες ανωνυμοποιημένες σαρώσεις από κλινικές μελέτες. Ιδανικό για ML projects.\n",
    "- **Medical Segmentation Decathlon** — datasets με segmentation labels.\n",
    "- **OpenNeuro, ADNI, OASIS** — εξειδικευμένα για νευροαπεικόνιση.\n",
    "- **Grand Challenge** — datasets από διαγωνισμούς ML.\n",
    "\n",
    "#### 3. Τα δικά σας DICOM\n",
    "Αν έχετε **πραγματικά κλινικά δεδομένα** από νοσοκομείο, χρειάζεστε:\n",
    "- Έγκριση από **Επιτροπή Δεοντολογίας / IRB**.\n",
    "- **Ανωνυμοποίηση** (την κάνουμε στην Ενότητα 21).\n",
    "- Συμμόρφωση με **GDPR** (στην Ευρώπη) και τοπικούς κανονισμούς.\n",
    "\n",
    "> ⚠️ **ΣΗΜΑΝΤΙΚΟ — Ποτέ μην:**\n",
    "> - Ανεβάζετε κλινικά DICOM σε δημόσια GitHub repositories.\n",
    "> - Στέλνετε δεδομένα ασθενών σε cloud υπηρεσίες χωρίς συμβατικό πλαίσιο.\n",
    "> - Δημοσιεύετε εικόνες χωρίς να αφαιρέσετε **ΟΛΑ** τα PHI (Protected Health Information).\n",
    "\n",
    "### Τι κάνει ο κώδικας\n",
    "\n",
    "Φορτώνει το «CT_small.dcm», ένα μικρό demo αρχείο που έρχεται με την pydicom. Η μεταβλητή `sample_dcm_path` είναι απλά η **διαδρομή** στο αρχείο — δεν έχουμε ακόμη ανοίξει το περιεχόμενό του. Αυτό γίνεται στην επόμενη ενότητα.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "258d924c",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              GETTING SAMPLE DICOM FILES                        ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "For this tutorial, you can:\n",
    "1. Use pydicom's built-in sample files\n",
    "2. Download from public datasets (e.g., The Cancer Imaging Archive)\n",
    "3. Use your own DICOM files\n",
    "\n",
    "We'll use pydicom's built-in samples for demonstration.\n",
    "\"\"\")\n",
    "\n",
    "# Load a sample DICOM file from pydicom's test data\n",
    "from pydicom.data import get_testdata_file\n",
    "\n",
    "# Get sample DICOM file\n",
    "sample_dcm_path = get_testdata_file(\"CT_small.dcm\")\n",
    "print(f\"Sample DICOM file loaded from: {sample_dcm_path}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f470372",
   "metadata": {},
   "source": [
    "## Ενότητα 4 — Ανάγνωση Αρχείων DICOM\n",
    "\n",
    "### Η συνάρτηση «κλειδί»: `pydicom.dcmread`\n",
    "\n",
    "Όλη η μαγεία ξεκινά με μία γραμμή:\n",
    "\n",
    "```python\n",
    "dicom_data = pydicom.dcmread(sample_dcm_path)\n",
    "```\n",
    "\n",
    "Η `dcmread` διαβάζει το αρχείο DICOM και επιστρέφει ένα **αντικείμενο τύπου `FileDataset`**. Αυτό το αντικείμενο συμπεριφέρεται με δύο τρόπους ταυτόχρονα:\n",
    "\n",
    "- **Σαν «αντικείμενο» με ιδιότητες:** `dicom_data.PatientName`\n",
    "- **Σαν «λεξικό» με κλειδιά:** `dicom_data[0x0008, 0x0060]`\n",
    "\n",
    "Θα δούμε και τους δύο τρόπους στις επόμενες ενότητες.\n",
    "\n",
    "### Lazy loading — γιατί είναι γρήγορο\n",
    "\n",
    "Παρατηρήστε ότι τα **pixel** δεν φορτώνονται αμέσως στη μνήμη. Η `dcmread` φορτώνει μόνο τα **metadata** και «δείχνει» πού είναι τα pixel στο αρχείο. Τα pixel διαβάζονται **όταν τα ζητήσετε** (μέσω `dicom_data.pixel_array`). Αυτό λέγεται **lazy evaluation** και είναι πολύ χρήσιμο όταν έχετε χιλιάδες αρχεία και θέλετε να φιλτράρετε με βάση τα tags χωρίς να φορτώνετε τα pixel.\n",
    "\n",
    "> 💡 **Tip για ταχύτητα:** Αν θέλετε να διαβάσετε **μόνο** τα metadata (όχι τα pixel), χρησιμοποιήστε:\n",
    "> ```python\n",
    "> dcm = pydicom.dcmread(path, stop_before_pixels=True)\n",
    "> ```\n",
    "> Αυτό είναι 10× πιο γρήγορο για μεγάλα αρχεία.\n",
    "\n",
    "### Τι θα δείτε στην έξοδο\n",
    "\n",
    "- Επιβεβαίωση ότι το αρχείο φορτώθηκε (✓).\n",
    "- Ο τύπος του αντικειμένου: `<class 'pydicom.dataset.FileDataset'>`.\n",
    "\n",
    "> 🧠 **Συνηθισμένα σφάλματα ανάγνωσης:**\n",
    "> - **`InvalidDicomError`** — το αρχείο δεν είναι έγκυρο DICOM. Δοκιμάστε `force=True` αν λείπει το preamble.\n",
    "> - **`PermissionError`** — δεν έχετε δικαιώματα ανάγνωσης. Ελέγξτε τα permissions του αρχείου.\n",
    "> - **`FileNotFoundError`** — λάθος διαδρομή.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "db6b353b",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                  READING DICOM FILES                           ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "# Read the DICOM file\n",
    "dicom_data = pydicom.dcmread(sample_dcm_path)\n",
    "\n",
    "print(\"✓ DICOM file successfully loaded!\")\n",
    "print(f\"Type of object: {type(dicom_data)}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "716b862d",
   "metadata": {},
   "source": [
    "## Ενότητα 5 — Κατανόηση των DICOM Tags\n",
    "\n",
    "### Τι είναι ένα tag;\n",
    "\n",
    "Ένα **DICOM tag** είναι ένα στοιχείο metadata. Σκεφτείτε ένα τεράστιο τυποποιημένο ερωτηματολόγιο που γεμίζει αυτόματα το μηχάνημα κάθε φορά που τραβάει εικόνα. Κάθε ερώτηση («όνομα ασθενούς», «kV εκπομπής», «πάχος τομής»...) είναι ένα tag.\n",
    "\n",
    "### Δομή ενός tag\n",
    "\n",
    "Κάθε tag έχει **τρία συστατικά**:\n",
    "\n",
    "| Συστατικό | Παράδειγμα | Σημασία |\n",
    "|-----------|------------|---------|\n",
    "| **Tag number** | `(0008, 0060)` | Δύο 16-bit ακέραιοι σε hex: (group, element) |\n",
    "| **VR (Value Representation)** | `CS`, `PN`, `DA`, `IS`... | Ο τύπος δεδομένων |\n",
    "| **Value** | `\"CT\"` | Η ίδια η τιμή |\n",
    "\n",
    "### Οι τύποι (VRs) που θα συναντήσετε\n",
    "\n",
    "| VR | Σημασία | Παράδειγμα |\n",
    "|----|---------|------------|\n",
    "| **PN** | Person Name | `Doe^John` |\n",
    "| **CS** | Code String | `CT`, `MR`, `MONOCHROME2` |\n",
    "| **DA** | Date | `20240315` |\n",
    "| **TM** | Time | `143025.000` |\n",
    "| **IS** | Integer String | `\"512\"` |\n",
    "| **DS** | Decimal String | `\"0.7\"` |\n",
    "| **UI** | Unique Identifier | `1.2.840.10008...` |\n",
    "| **OB / OW** | Other Byte/Word (binary) | Pixel data, ιδιωτικά |\n",
    "\n",
    "### Πώς οργανώνονται τα tags — τα modules\n",
    "\n",
    "Το πρότυπο DICOM οργανώνει τα tags σε **modules**:\n",
    "\n",
    "- **Patient Module** — όνομα, ID, ηλικία, φύλο.\n",
    "- **Study Module** — ημερομηνία/περιγραφή/UID της εξέτασης.\n",
    "- **Series Module** — ομάδα τομών μιας ακολουθίας.\n",
    "- **Image Module** — διαστάσεις, encoding, position.\n",
    "- **Equipment Module** — κατασκευαστής, μοντέλο, software.\n",
    "- **Modality-specific** (CT Image, MR Image, ...) — παράμετροι για το συγκεκριμένο τύπο.\n",
    "\n",
    "### Τι κάνει ο κώδικας\n",
    "\n",
    "Με `print(dicom_data)` τυπώνεται **όλη η DICOM header** — δηλαδή κάθε tag του αρχείου, με όλα τα συστατικά του. Είναι μεγάλη έξοδος. **Ξοδέψτε χρόνο** να την κοιτάξετε — είναι ο καλύτερος τρόπος να καταλάβετε τι περιέχει ένα DICOM.\n",
    "\n",
    "> 📚 **Reference:** Όλα τα tags που έχει ορίσει το πρότυπο βρίσκονται στο επίσημο **DICOM Data Dictionary** στη διεύθυνση `https://dicom.innolitics.com/ciods` — βάλτε το στα bookmarks σας.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "31195b00",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                  UNDERSTANDING DICOM TAGS                      ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "DICOM tags are metadata elements that describe the image.\n",
    "Each tag has:\n",
    "- Tag number: (XXXX, XXXX) in hexadecimal\n",
    "- VR (Value Representation): data type\n",
    "- Value: the actual data\n",
    "\"\"\")\n",
    "\n",
    "# Display the entire DICOM header\n",
    "print(\"\\n\" + \"=\"*70)\n",
    "print(\"COMPLETE DICOM HEADER:\")\n",
    "print(\"=\"*70)\n",
    "print(dicom_data)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62d8ac94",
   "metadata": {},
   "source": [
    "## Ενότητα 6 — Πρόσβαση σε Συγκεκριμένα Tags\n",
    "\n",
    "### Τα τρία βασικά «modules» που μας ενδιαφέρουν συνήθως\n",
    "\n",
    "Σε αυτή την ενότητα διαβάζουμε επιλεκτικά **τρεις ομάδες tags** που χρησιμοποιούνται σχεδόν σε κάθε ανάλυση:\n",
    "\n",
    "#### 1. Patient Module — δημογραφικά\n",
    "- `PatientName` — όνομα (μορφή `LastName^FirstName^Middle`).\n",
    "- `PatientID` — μοναδικό ID στο νοσοκομείο.\n",
    "- `PatientBirthDate` — ημερομηνία γέννησης.\n",
    "- `PatientSex` — `M`, `F`, `O`.\n",
    "\n",
    "#### 2. Study Module — η εξέταση\n",
    "- `StudyDate` — πότε έγινε.\n",
    "- `StudyDescription` — περιγραφή («Brain MRI without contrast»).\n",
    "- `StudyInstanceUID` — μοναδικό αναγνωριστικό. Δύο εικόνες με το ίδιο UID ανήκουν στην **ίδια εξέταση**.\n",
    "\n",
    "#### 3. Image Module — η εικόνα\n",
    "- `Modality` — `CT`, `MR`, `US`, ...\n",
    "- `Rows`, `Columns` — διαστάσεις.\n",
    "- `SliceThickness` — πάχος τομής σε mm.\n",
    "- `PixelSpacing` — απόσταση μεταξύ pixel σε mm.\n",
    "\n",
    "### Γιατί χρειαζόμαστε `try / except`;\n",
    "\n",
    "Παρατηρήστε ότι ο κώδικας τυλίγει την προσπάθεια ανάγνωσης σε `try`/`except AttributeError`. Γιατί;\n",
    "\n",
    "> ⚠️ **Όχι όλα τα DICOM έχουν όλα τα tags!** Ορισμένα tags είναι «type 1» (υποχρεωτικά πάντα), «type 2» (υποχρεωτικά αλλά μπορεί να είναι κενά), και «type 3» (προαιρετικά). Επιπλέον, σε ανωνυμοποιημένα δεδομένα, τα δημογραφικά μπορεί να έχουν αφαιρεθεί.\n",
    "\n",
    "Αν προσπαθήσετε `dicom_data.PatientAge` και το tag δεν υπάρχει, η Python ρίχνει `AttributeError`. Με το `try/except` το πρόγραμμά μας **δεν κρασάρει** — απλώς εμφανίζει «Some patient information not available».\n",
    "\n",
    "### Η εναλλακτική: `hasattr()`\n",
    "\n",
    "Στις τελευταίες γραμμές βλέπετε:\n",
    "```python\n",
    "if hasattr(dicom_data, 'SliceThickness'):\n",
    "    print(f\"Slice Thickness: {dicom_data.SliceThickness} mm\")\n",
    "```\n",
    "\n",
    "Αυτό είναι **πιο καθαρό** από `try/except` όταν θέλετε απλώς να ελέγξετε αν υπάρχει το tag πριν το χρησιμοποιήσετε. Στην επόμενη ενότητα θα δούμε και την `get()` που είναι ακόμα πιο κομψή.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "75c55f5b",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              ACCESSING SPECIFIC DICOM TAGS                     ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "# Common DICOM tags and how to access them\n",
    "print(\"\\n📋 PATIENT INFORMATION:\")\n",
    "print(\"-\" * 50)\n",
    "try:\n",
    "    print(f\"Patient Name: {dicom_data.PatientName}\")\n",
    "    print(f\"Patient ID: {dicom_data.PatientID}\")\n",
    "    print(f\"Patient Birth Date: {dicom_data.PatientBirthDate}\")\n",
    "    print(f\"Patient Sex: {dicom_data.PatientSex}\")\n",
    "except AttributeError as e:\n",
    "    print(f\"Some patient information not available: {e}\")\n",
    "\n",
    "print(\"\\n🏥 STUDY INFORMATION:\")\n",
    "print(\"-\" * 50)\n",
    "try:\n",
    "    print(f\"Study Date: {dicom_data.StudyDate}\")\n",
    "    print(f\"Study Description: {dicom_data.StudyDescription}\")\n",
    "    print(f\"Study Instance UID: {dicom_data.StudyInstanceUID}\")\n",
    "except AttributeError as e:\n",
    "    print(f\"Some study information not available: {e}\")\n",
    "\n",
    "print(\"\\n📸 IMAGE ACQUISITION PARAMETERS:\")\n",
    "print(\"-\" * 50)\n",
    "print(f\"Modality: {dicom_data.Modality}\")\n",
    "print(f\"Rows (Height): {dicom_data.Rows}\")\n",
    "print(f\"Columns (Width): {dicom_data.Columns}\")\n",
    "\n",
    "# Check if these attributes exist before accessing\n",
    "if hasattr(dicom_data, 'SliceThickness'):\n",
    "    print(f\"Slice Thickness: {dicom_data.SliceThickness} mm\")\n",
    "if hasattr(dicom_data, 'PixelSpacing'):\n",
    "    print(f\"Pixel Spacing: {dicom_data.PixelSpacing} mm\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8cf115aa",
   "metadata": {},
   "source": [
    "## Ενότητα 7 — Τέσσερις Τρόποι Πρόσβασης σε ένα Tag\n",
    "\n",
    "### Το ίδιο αποτέλεσμα, διαφορετικός κώδικας\n",
    "\n",
    "Στη Python (όπως και στη ζωή), συχνά υπάρχουν πολλοί τρόποι να πετύχετε το ίδιο πράγμα. Σε αυτή την ενότητα βλέπετε **τέσσερις τρόπους** να πάρετε την τιμή του tag `Modality`:\n",
    "\n",
    "| # | Μέθοδος | Πότε προτιμάται |\n",
    "|---|---------|------------------|\n",
    "| 1 | `dicom_data.Modality` | Γρήγορη, καθαρή — όταν είστε σίγουροι ότι το tag υπάρχει |\n",
    "| 2 | `dicom_data[0x0008, 0x0060].value` | Όταν δεν θυμάστε το όνομα αλλά ξέρετε τον αριθμό |\n",
    "| 3 | `dicom_data.get(\"Modality\", \"Not found\")` | **Συνιστάται** — δεν κρασάρει αν λείπει το tag |\n",
    "| 4 | `dicom_data.data_element(\"Modality\").value` | Όταν θέλετε ολόκληρο το element (όχι μόνο την τιμή) |\n",
    "\n",
    "### Σύγκριση\n",
    "\n",
    "#### Μέθοδος 1: Direct attribute\n",
    "```python\n",
    "modality = dicom_data.Modality\n",
    "```\n",
    "**Πλεονεκτήματα:** Σύντομο, ευανάγνωστο.\n",
    "**Μειονεκτήματα:** Κρασάρει με `AttributeError` αν το tag δεν υπάρχει.\n",
    "\n",
    "#### Μέθοδος 2: Tag number (hex)\n",
    "```python\n",
    "modality = dicom_data[0x0008, 0x0060].value\n",
    "```\n",
    "**Πλεονεκτήματα:** Λειτουργεί ακόμα και για **ιδιωτικά tags** (private tags) που δεν έχουν κανονικό όνομα.\n",
    "**Μειονεκτήματα:** Δυσανάγνωστο. Πρέπει να ξέρετε τον αριθμό.\n",
    "\n",
    "#### Μέθοδος 3: `get()` με default\n",
    "```python\n",
    "modality = dicom_data.get(\"Modality\", \"Not found\")\n",
    "```\n",
    "**Πλεονεκτήματα:** **Ποτέ** δεν κρασάρει. Επιστρέφει το default αν λείπει.\n",
    "**Μειονεκτήματα:** Λίγο πιο φλύαρο.\n",
    "\n",
    "#### Μέθοδος 4: `data_element()`\n",
    "```python\n",
    "elem = dicom_data.data_element(\"Modality\")\n",
    "modality = elem.value\n",
    "print(elem.VR)        # επίσης ο τύπος\n",
    "print(elem.tag)       # επίσης ο αριθμός\n",
    "```\n",
    "**Πλεονεκτήματα:** Επιστρέφει ολόκληρο το element με όλα τα metadata του (VR, tag number).\n",
    "**Μειονεκτήματα:** Δύο γραμμές αντί για μία.\n",
    "\n",
    "### Συμβουλή του διδάσκοντα\n",
    "\n",
    "> 🎯 **Στη ρουτίνα σας, χρησιμοποιείτε τη Μέθοδο 3 (`.get()`) για παραγωγικό κώδικα.** Είναι ο μόνος τρόπος που εγγυάται ότι το πρόγραμμά σας δεν θα σταματήσει σε κάποιο «παράξενο» αρχείο μέσα σε 1000+ DICOMs.\n",
    "\n",
    "> 💡 **Παράδειγμα defensive programming:**\n",
    "> ```python\n",
    "> patient_age = dicom_data.get(\"PatientAge\", \"Unknown\")\n",
    "> slice_thick = float(dicom_data.get(\"SliceThickness\", 0))\n",
    "> ```\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0ad9fbd2",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║           DIFFERENT METHODS TO ACCESS TAGS                     ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "# Method 1: Direct attribute access\n",
    "method1 = dicom_data.Modality\n",
    "print(f\"Method 1 - Attribute access: {method1}\")\n",
    "\n",
    "# Method 2: Using tag numbers\n",
    "method2 = dicom_data[0x0008, 0x0060].value\n",
    "print(f\"Method 2 - Tag number: {method2}\")\n",
    "\n",
    "# Method 3: Using get() method (safer - won't crash if tag doesn't exist)\n",
    "method3 = dicom_data.get(\"Modality\", \"Not found\")\n",
    "print(f\"Method 3 - get() method: {method3}\")\n",
    "\n",
    "# Method 4: Using tag name as string\n",
    "method4 = dicom_data.data_element(\"Modality\").value\n",
    "print(f\"Method 4 - data_element(): {method4}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bddd3e20",
   "metadata": {},
   "source": [
    "## Ενότητα 8 — Πίνακας Σύνοψης Tags με `pandas`\n",
    "\n",
    "### Γιατί DataFrame;\n",
    "\n",
    "Όταν δουλεύετε με **πολλά** tags, δεν θέλετε `print(...)` παντού. Θέλετε **έναν πίνακα** που να μπορείτε να:\n",
    "- Φιλτράρετε,\n",
    "- Ταξινομήσετε,\n",
    "- Εξάγετε σε CSV/Excel,\n",
    "- Συγκρίνετε με άλλο DICOM.\n",
    "\n",
    "Το `pandas.DataFrame` είναι το εργαλείο της Python για ακριβώς αυτή τη δουλειά — μια **2D δομή με ονόματα στηλών**, σαν Excel αλλά προγραμματιζόμενο.\n",
    "\n",
    "### Τι κάνει η συνάρτηση `create_dicom_summary`\n",
    "\n",
    "Δέχεται:\n",
    "- `dicom_obj` — το DICOM που θέλουμε να εξετάσουμε.\n",
    "- `tags_of_interest` — λίστα ονομάτων tag (προαιρετικό· έχει default).\n",
    "\n",
    "Επιστρέφει DataFrame με τρεις στήλες:\n",
    "\n",
    "| Tag Name | Tag Number | Value |\n",
    "|----------|------------|-------|\n",
    "| PatientName | (0010, 0010) | CompressedSamples^CT1 |\n",
    "| Modality | (0008, 0060) | CT |\n",
    "| Rows | (0028, 0010) | 128 |\n",
    "| ... | ... | ... |\n",
    "\n",
    "### Παιδαγωγικά σημεία στον κώδικα\n",
    "\n",
    "#### 1. Default arguments\n",
    "```python\n",
    "def create_dicom_summary(dicom_obj, tags_of_interest=None):\n",
    "    if tags_of_interest is None:\n",
    "        tags_of_interest = [...]\n",
    "```\n",
    "Το `None` ως default μας επιτρέπει να φτιάξουμε **διαφορετική προεπιλεγμένη λίστα κάθε φορά**. (Αν είχαμε `tags_of_interest=[]` ως default, θα μοιραζόμασταν την ίδια λίστα μεταξύ κλήσεων — γνωστό «mutable default argument» bug της Python!)\n",
    "\n",
    "#### 2. Defensive coding με `try/except`\n",
    "Μέσα στο loop, αν κάποιο tag δεν υπάρχει, καταγράφουμε «Not available» αντί να κρασάρουμε. Σε **πραγματικό** σενάριο που σαρώνετε χιλιάδες DICOM, αυτό είναι αναγκαίο.\n",
    "\n",
    "#### 3. Επαναχρησιμοποιήσιμη συνάρτηση\n",
    "Δεν είναι hardcoded για ένα συγκεκριμένο αρχείο. Μπορείτε να την καλέσετε σε **οποιοδήποτε** DICOM, με **οποιαδήποτε** λίστα tags. Αυτό λέγεται **abstraction** — βασική αρχή του καλού κώδικα.\n",
    "\n",
    "> 💡 **Επέκταση για άσκηση:** Φτιάξτε μια συνάρτηση `compare_dicoms(dicom1, dicom2)` που επιστρέφει DataFrame με τις τιμές δίπλα-δίπλα και χρωματίζει τις διαφορές. Ιδανικό για QC σε πειραματικά πρωτόκολλα.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cbd3f653",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              CREATING A TAGS SUMMARY TABLE                     ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "def create_dicom_summary(dicom_obj, tags_of_interest=None):\n",
    "    \"\"\"\n",
    "    Create a pandas DataFrame summarizing important DICOM tags\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        The DICOM object to summarize\n",
    "    tags_of_interest : list\n",
    "        List of tag names to include. If None, uses default important tags.\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    pd.DataFrame : Summary of DICOM tags\n",
    "    \"\"\"\n",
    "    \n",
    "    if tags_of_interest is None:\n",
    "        tags_of_interest = [\n",
    "            'PatientName', 'PatientID', 'PatientSex', 'PatientBirthDate',\n",
    "            'StudyDate', 'StudyDescription', 'Modality',\n",
    "            'Manufacturer', 'ManufacturerModelName',\n",
    "            'Rows', 'Columns', 'PixelSpacing', 'SliceThickness',\n",
    "            'ImagePositionPatient', 'ImageOrientationPatient'\n",
    "        ]\n",
    "    \n",
    "    data = []\n",
    "    for tag_name in tags_of_interest:\n",
    "        try:\n",
    "            value = getattr(dicom_obj, tag_name)\n",
    "            # Get the tag number\n",
    "            tag = dicom_obj.data_element(tag_name).tag\n",
    "            data.append({\n",
    "                'Tag Name': tag_name,\n",
    "                'Tag Number': str(tag),\n",
    "                'Value': str(value)\n",
    "            })\n",
    "        except AttributeError:\n",
    "            data.append({\n",
    "                'Tag Name': tag_name,\n",
    "                'Tag Number': 'N/A',\n",
    "                'Value': 'Not available'\n",
    "            })\n",
    "    \n",
    "    return pd.DataFrame(data)\n",
    "\n",
    "# Create and display the summary\n",
    "summary_df = create_dicom_summary(dicom_data)\n",
    "display(summary_df)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ec1a252",
   "metadata": {},
   "source": [
    "## Ενότητα 9 — Πρόσβαση στα Δεδομένα της Εικόνας\n",
    "\n",
    "### Από metadata σε pixels\n",
    "\n",
    "Μέχρι τώρα είδαμε μόνο **κείμενο και αριθμούς** — metadata. Σε αυτή την ενότητα ζητάμε για πρώτη φορά τα **ίδια τα pixel** της εικόνας με την ιδιότητα:\n",
    "\n",
    "```python\n",
    "pixel_array = dicom_data.pixel_array\n",
    "```\n",
    "\n",
    "### Τι είναι το `pixel_array`;\n",
    "\n",
    "Είναι ένας **NumPy ndarray** — δηλαδή ένας πολυδιάστατος πίνακας αριθμών. Σε μια απλή 2D DICOM εικόνα η μορφή του είναι `(Rows, Columns)`. Σε μια έγχρωμη ή πολυτομική εικόνα μπορεί να έχει 3 ή 4 διαστάσεις.\n",
    "\n",
    "### Οι πέντε ερωτήσεις πρώτης γραμμής\n",
    "\n",
    "Πάντα, σε **κάθε** εικόνα που σας δίνει κάποιος, ξεκινήστε με αυτές τις πέντε ερωτήσεις:\n",
    "\n",
    "| Ερώτηση | Κώδικας | Γιατί έχει σημασία |\n",
    "|---------|---------|----------------------|\n",
    "| Πόσο μεγάλη είναι; | `.shape` | Επιβεβαίωση δομής, υπολογισμός μνήμης |\n",
    "| Τι τύπος δεδομένων; | `.dtype` | int16, uint16, float32 — επηρεάζει εύρος |\n",
    "| Ελάχιστη τιμή; | `.min()` | Έλεγχος εγκυρότητας |\n",
    "| Μέγιστη τιμή; | `.max()` | Δυναμικό εύρος |\n",
    "| Μέση τιμή; | `.mean()` | «Φωτεινότητα» |\n",
    "\n",
    "> 🧠 **Παράδειγμα ερμηνείας:** Αν δείτε CT εικόνα με `min=0, max=4095`, **δεν** είναι ακόμα σε Hounsfield Units. Αν δείτε `min=-1024, max=3071`, είναι ήδη σε HU. Αυτή η μικρή λεπτομέρεια είναι κρίσιμη — θα τη συζητήσουμε αναλυτικά στην Ενότητα 11.\n",
    "\n",
    "### Συνηθισμένοι τύποι δεδομένων στις ιατρικές εικόνες\n",
    "\n",
    "| dtype | Εύρος | Πού συναντάται |\n",
    "|-------|-------|------------------|\n",
    "| `uint8` | 0–255 | Πιο σπάνιο, μερικά ultrasound |\n",
    "| `uint16` | 0–65535 | CT, MRI πριν το rescale |\n",
    "| `int16` | −32768–32767 | CT μετά το rescale (Hounsfield) |\n",
    "| `float32` | μεγάλο εύρος | Quantitative MRI, parametric maps |\n",
    "\n",
    "> ⚠️ **Προσοχή με τους ακέραιους:** Αν κάνετε πράξεις σε `uint16` και υπάρξει υπερχείλιση (overflow) ή αρνητικό αποτέλεσμα, παίρνετε εντελώς λάθος νούμερα **σιωπηλά**. Πάντα μετατρέπετε σε `float` πριν από αριθμητικές πράξεις: `img = pixel_array.astype(np.float32)`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6cdcd409",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                 ACCESSING IMAGE DATA                           ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "# Get the pixel array\n",
    "pixel_array = dicom_data.pixel_array\n",
    "\n",
    "print(f\"Image shape: {pixel_array.shape}\")\n",
    "print(f\"Data type: {pixel_array.dtype}\")\n",
    "print(f\"Min pixel value: {pixel_array.min()}\")\n",
    "print(f\"Max pixel value: {pixel_array.max()}\")\n",
    "print(f\"Mean pixel value: {pixel_array.mean():.2f}\")\n",
    "print(f\"Standard deviation: {pixel_array.std():.2f}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa834929",
   "metadata": {},
   "source": [
    "## Ενότητα 10 — Οπτικοποίηση Εικόνων DICOM\n",
    "\n",
    "### Από αριθμούς σε εικόνα\n",
    "\n",
    "Μέχρι τώρα οι «εικόνες» μας ήταν πίνακες αριθμών. Τώρα τους **βλέπουμε** ως πραγματικές εικόνες με τη `matplotlib`.\n",
    "\n",
    "### Η `imshow` — η συνάρτηση-κλειδί\n",
    "\n",
    "```python\n",
    "plt.imshow(pixel_array, cmap='gray')\n",
    "```\n",
    "\n",
    "Δύο σημαντικές παράμετροι:\n",
    "\n",
    "#### 1. `cmap='gray'` — η χρωματική κλίμακα\n",
    "Η matplotlib από προεπιλογή χρησιμοποιεί `viridis` (μπλε-κίτρινο), που είναι ωραίο για επιστημονικά γραφήματα **αλλά ΟΧΙ για ιατρικές εικόνες**. Οι ακτινολόγοι αναγνωρίζουν παθολογία σε γκρι κλίμακα — βλέποντας μια CT σε ψυχρά-θερμά χρώματα μπορεί να μη δουν αυτό που ψάχνουν.\n",
    "\n",
    "#### 2. Auto-scaling vs manual range\n",
    "Η `imshow` από προεπιλογή κάνει **αυτόματη κανονικοποίηση**: αντιστοιχίζει τη min τιμή στο μαύρο και τη max στο λευκό. Αυτό είναι:\n",
    "- **Καλό** για γρήγορη εξερεύνηση.\n",
    "- **Κακό** όταν θέλετε σωστή σύγκριση μεταξύ εικόνων ή σωστή HU απεικόνιση. Σε αυτές τις περιπτώσεις προτιμούμε **windowing** (Ενότητα 12).\n",
    "\n",
    "### Συμπληρωματικά χαρακτηριστικά\n",
    "\n",
    "Η συνάρτηση `display_dicom_image` προσθέτει:\n",
    "\n",
    "- **`colorbar`** — μπάρα δεξιά που δείχνει τι τιμή είναι το λευκό/μαύρο. Σημαντικό!\n",
    "- **`title`** με modality και διαστάσεις.\n",
    "- **`xlabel`/`ylabel`** — άξονες με νόημα (αν και σε ιατρικές εικόνες συχνά τους κρύβουμε με `axis('off')`).\n",
    "- **`text`** σε γωνία — info box με min/max/mean.\n",
    "\n",
    "### Παιδαγωγικά σημεία\n",
    "\n",
    "> 💡 **Πάντα να βάζετε colorbar.** Χωρίς αυτή, ο θεατής δεν ξέρει αν το «λευκό» αντιπροσωπεύει 100 ή 10000. Στην ιατρική απεικόνιση, αυτό κάνει τη διαφορά.\n",
    "\n",
    "> 🎯 **`figsize=(10, 10)`** — τετράγωνο μέγεθος. Οι ιατρικές εικόνες είναι συνήθως τετράγωνες (π.χ. 512×512). Αν βάλετε ορθογώνιο figsize, παραμορφώνεται οπτικά.\n",
    "\n",
    "> ⚠️ **Coordinate systems:** Στην matplotlib το pixel `(0,0)` είναι **πάνω-αριστερά** της εικόνας. Στις ιατρικές εικόνες, οι συντεταγμένες ασθενούς (LPS/RAS) είναι διαφορετικές. Όταν κάνετε segmentation και θέλετε να αναφερθείτε σε «δεξί πνεύμονα», βεβαιωθείτε ποια κατεύθυνση είναι ποια!\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "012e08e6",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              VISUALIZING DICOM IMAGES                          ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "def display_dicom_image(dicom_obj, title=\"DICOM Image\", figsize=(10, 10)):\n",
    "    \"\"\"\n",
    "    Display a DICOM image with proper window/level settings\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        The DICOM object to display\n",
    "    title : str\n",
    "        Title for the plot\n",
    "    figsize : tuple\n",
    "        Figure size\n",
    "    \"\"\"\n",
    "    \n",
    "    plt.figure(figsize=figsize)\n",
    "    \n",
    "    # Get pixel array\n",
    "    pixel_array = dicom_obj.pixel_array\n",
    "    \n",
    "    # Display the image\n",
    "    plt.imshow(pixel_array, cmap='gray')\n",
    "    plt.colorbar(label='Pixel Intensity')\n",
    "    plt.title(f\"{title}\\n{dicom_obj.Modality} - {dicom_obj.Rows}x{dicom_obj.Columns}\")\n",
    "    plt.xlabel('Column (X)')\n",
    "    plt.ylabel('Row (Y)')\n",
    "    \n",
    "    # Add image information\n",
    "    info_text = f\"Min: {pixel_array.min()}, Max: {pixel_array.max()}, Mean: {pixel_array.mean():.1f}\"\n",
    "    plt.text(0.02, 0.98, info_text, transform=plt.gca().transAxes,\n",
    "             bbox=dict(boxstyle='round', facecolor='white', alpha=0.8),\n",
    "             verticalalignment='top', fontsize=9)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "# Display the image\n",
    "display_dicom_image(dicom_data, \"Sample DICOM CT Image\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8566f5e4",
   "metadata": {},
   "source": [
    "## Ενότητα 11 — Hounsfield Units (αποκλειστικά για CT)\n",
    "\n",
    "### Τι είναι οι Hounsfield Units;\n",
    "\n",
    "Στην **Υπολογιστική Τομογραφία (CT)**, οι τιμές των pixel **δεν** είναι αυθαίρετοι αριθμοί — αντιπροσωπεύουν την **εξασθένιση των ακτίνων Χ** σε σχέση με το νερό. Αυτή η κανονικοποιημένη κλίμακα λέγεται **Hounsfield Units** (HU), από τον Sir Godfrey Hounsfield (Νόμπελ Φυσιολογίας 1979).\n",
    "\n",
    "Ο ορισμός:\n",
    "$$\n",
    "\\text{HU} = 1000 \\times \\dfrac{\\mu_{ιστού} - \\mu_{νερό}}{\\mu_{νερό} - \\mu_{αέρα}}\n",
    "$$\n",
    "\n",
    "όπου **μ** ο γραμμικός συντελεστής εξασθένισης.\n",
    "\n",
    "### Η αξία της κλίμακας: συγκρισιμότητα\n",
    "\n",
    "Επειδή η κλίμακα HU είναι **απόλυτη και τυποποιημένη**, ένας ιστός με συγκεκριμένη σύσταση δίνει την **ίδια** τιμή σε οποιοδήποτε CT μηχάνημα στον κόσμο. Αυτό είναι κρίσιμο.\n",
    "\n",
    "### Πίνακας τιμών HU για βασικούς ιστούς\n",
    "\n",
    "| Ιστός / υλικό | HU |\n",
    "|----------------|-----|\n",
    "| Αέρας | −1000 |\n",
    "| Πνεύμονας | −500 έως −400 |\n",
    "| Λίπος | −100 έως −50 |\n",
    "| Νερό | 0 |\n",
    "| Μυς | 30 έως 50 |\n",
    "| Αίμα (φρέσκο) | 50 έως 80 |\n",
    "| Συκώτι | 50 έως 70 |\n",
    "| Εγκέφαλος (φαιά ουσία) | 35 έως 45 |\n",
    "| Σπογγώδες οστό | 200 έως 700 |\n",
    "| Συμπαγές οστό | 1000+ |\n",
    "| Μέταλλο (εμφυτεύματα) | 2000+ (συχνά «γεμίζει» με streak artifacts) |\n",
    "\n",
    "### Η μετατροπή — γιατί χρειάζεται;\n",
    "\n",
    "Το DICOM **δεν αποθηκεύει** πάντα τις τιμές HU απευθείας. Για λόγους αποδοτικότητας, αποθηκεύει «raw» τιμές pixel και δίνει δύο tags για τη μετατροπή:\n",
    "\n",
    "- **`RescaleSlope`** (συνήθως 1)\n",
    "- **`RescaleIntercept`** (συνήθως −1024 για CT)\n",
    "\n",
    "Η μετατροπή είναι γραμμική:\n",
    "\n",
    "$$\n",
    "\\text{HU} = (\\text{pixel\\_value} \\times \\text{RescaleSlope}) + \\text{RescaleIntercept}\n",
    "$$\n",
    "\n",
    "### Τι κάνει η συνάρτηση `convert_to_hounsfield`\n",
    "\n",
    "1. Παίρνει τα pixel.\n",
    "2. Τα μετατρέπει σε `float64` για ακρίβεια.\n",
    "3. Διαβάζει `RescaleSlope` και `RescaleIntercept` από τα tags (με **defaults** 1 και 0 αν λείπουν).\n",
    "4. Εφαρμόζει τη γραμμική μετατροπή.\n",
    "5. Επιστρέφει τη **σωστή** εικόνα HU.\n",
    "\n",
    "> ⚠️ **ΠΡΟΣΟΧΗ — μην εφαρμόσετε σε non-CT:** Η μετατροπή HU **έχει νόημα μόνο σε CT**. Σε MRI, οι τιμές είναι **σχετικές** (δεν υπάρχει «φυσική κλίμακα»). Σε υπερηχογράφημα, ομοίως. Γι' αυτό ο κώδικας ελέγχει `if dicom_data.Modality == 'CT'` πριν τρέξει τη μετατροπή.\n",
    "\n",
    "> 🎯 **Πρακτική σύσταση:** Στα CT projects σας, **πάντα** μετατρέπετε σε HU πριν κάνετε οτιδήποτε άλλο. Είναι το πρώτο preprocessing βήμα για segmentation, radiomics, ML κ.λπ.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb8cdcab",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║           HOUNSFIELD UNITS (for CT images)                     ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "For CT images, pixel values are in Hounsfield Units (HU):\n",
    "- Air: -1000 HU\n",
    "- Water: 0 HU\n",
    "- Soft tissue: 40-80 HU\n",
    "- Bone: 400-1000 HU\n",
    "\n",
    "DICOM stores pixel values that need to be converted using:\n",
    "HU = pixel_value * RescaleSlope + RescaleIntercept\n",
    "\"\"\")\n",
    "\n",
    "def convert_to_hounsfield(dicom_obj):\n",
    "    \"\"\"\n",
    "    Convert pixel values to Hounsfield Units\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        DICOM object\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    np.ndarray : Image in Hounsfield Units\n",
    "    \"\"\"\n",
    "    \n",
    "    pixel_array = dicom_obj.pixel_array.astype(np.float64)\n",
    "    \n",
    "    # Get rescale parameters\n",
    "    intercept = dicom_obj.RescaleIntercept if hasattr(dicom_obj, 'RescaleIntercept') else 0\n",
    "    slope = dicom_obj.RescaleSlope if hasattr(dicom_obj, 'RescaleSlope') else 1\n",
    "    \n",
    "    # Convert to HU\n",
    "    hu_image = pixel_array * slope + intercept\n",
    "    \n",
    "    print(f\"Rescale Slope: {slope}\")\n",
    "    print(f\"Rescale Intercept: {intercept}\")\n",
    "    print(f\"HU range: [{hu_image.min():.1f}, {hu_image.max():.1f}]\")\n",
    "    \n",
    "    return hu_image\n",
    "\n",
    "# Convert to Hounsfield Units if it's a CT image\n",
    "if dicom_data.Modality == 'CT':\n",
    "    hu_image = convert_to_hounsfield(dicom_data)\n",
    "else:\n",
    "    print(\"This is not a CT image, Hounsfield conversion not applicable.\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6fe45b24",
   "metadata": {},
   "source": [
    "## Ενότητα 12 — Windowing (Window/Level)\n",
    "\n",
    "### Το πρόβλημα του δυναμικού εύρους\n",
    "\n",
    "Η ανθρώπινη όραση **ξεχωρίζει περίπου 100 αποχρώσεις του γκρι**. Όμως μια CT εικόνα μπορεί να περιέχει τιμές από −1000 (αέρας) έως +3000 HU (οστό) — **4000 διαφορετικές τιμές**. Αν τις απεικονίσουμε όλες ταυτόχρονα στις 256 αποχρώσεις του γκρι μιας οθόνης, **χάνουμε τη λεπτομέρεια** σε κάθε ιστό.\n",
    "\n",
    "**Windowing:** μια τεχνική που λέει «δείξε μου με αντίθεση **μόνο** ένα συγκεκριμένο εύρος HU, και κόψε τα υπόλοιπα στο μαύρο/λευκό».\n",
    "\n",
    "### Δύο παράμετροι\n",
    "\n",
    "| Παράμετρος | Σύμβολο | Σημασία |\n",
    "|------------|---------|---------|\n",
    "| **Window Center** (Level) | C ή WL | Η μέση τιμή HU που μας ενδιαφέρει |\n",
    "| **Window Width** | W ή WW | Πόσο πλατύ εύρος γύρω από το center |\n",
    "\n",
    "Το εύρος εμφάνισης είναι:\n",
    "$$\n",
    "[C - W/2, \\quad C + W/2]\n",
    "$$\n",
    "\n",
    "- Pixel με τιμή **κάτω** από `C - W/2` → μαύρο.\n",
    "- Pixel με τιμή **πάνω** από `C + W/2` → λευκό.\n",
    "- Ενδιάμεσες τιμές → γραμμική κλιμάκωση στο γκρι.\n",
    "\n",
    "### Συνηθισμένα presets για CT\n",
    "\n",
    "| Όνομα | Center (HU) | Width (HU) | Τι αναδεικνύει |\n",
    "|-------|-------------|------------|------------------|\n",
    "| **Lung** | −600 | 1500 | Παρέγχυμα πνεύμονα, οζίδια |\n",
    "| **Mediastinum / Soft tissue** | 50 | 350–400 | Ήπαρ, νεφροί, καρδιά |\n",
    "| **Bone** | 400 | 1800 | Σπονδυλική στήλη, κατάγματα |\n",
    "| **Brain** | 40 | 80 | Λεπτή αντίθεση φαιάς-λευκής ουσίας |\n",
    "| **Stroke / acute brain** | 35 | 30 | Πρώιμα σημεία ισχαιμίας |\n",
    "| **Liver** | 60 | 160 | Εστιακές βλάβες ήπατος |\n",
    "\n",
    "> 💡 **Στην κλινική πράξη:** Ο ακτινολόγος αλλάζει συνεχώς windows κατά την ερμηνεία. Δεν βλέπει το CT με ένα μόνο preset — αλλάζει για να ελέγξει διαφορετικές δομές.\n",
    "\n",
    "### Τι κάνει η συνάρτηση `apply_window`\n",
    "\n",
    "```python\n",
    "def apply_window(image, window_center, window_width):\n",
    "    img_min = window_center - window_width // 2\n",
    "    img_max = window_center + window_width // 2\n",
    "    windowed = np.clip(image, img_min, img_max)         # κόβει στα όρια\n",
    "    windowed = ((windowed - img_min) / (img_max - img_min) * 255.0).astype(np.uint8)\n",
    "    return windowed\n",
    "```\n",
    "\n",
    "Τρία βήματα:\n",
    "1. **`np.clip`** — όλες οι τιμές κάτω από `img_min` γίνονται `img_min`, πάνω από `img_max` γίνονται `img_max`.\n",
    "2. **Κανονικοποίηση** στο εύρος [0, 1].\n",
    "3. **Κλιμάκωση** σε [0, 255] και μετατροπή σε `uint8` για εμφάνιση.\n",
    "\n",
    "### Σημαντική διάκριση\n",
    "\n",
    "> 🎯 **Windowing = ρύθμιση εμφάνισης ΜΟΝΟ.** ΔΕΝ αλλάζει τα δεδομένα της εικόνας. Αν θέλετε να κάνετε ποσοτική ανάλυση (π.χ. μετρήσετε τη μέση HU ενός όγκου), χρησιμοποιείτε την **αρχική** εικόνα HU, όχι τη windowed. Το windowed είναι μόνο για να **βλέπετε**.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3e72ab7e",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                  WINDOWING (Window/Level)                      ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "Windowing is used to enhance contrast in specific tissue ranges.\n",
    "- Window Center (Level): middle of the range of HU values to display\n",
    "- Window Width: range of HU values to display\n",
    "\n",
    "Common CT windows:\n",
    "- Lung: Center=-600, Width=1500\n",
    "- Mediastinum: Center=50, Width=350\n",
    "- Bone: Center=400, Width=1800\n",
    "- Brain: Center=40, Width=80\n",
    "\"\"\")\n",
    "\n",
    "def apply_window(image, window_center, window_width):\n",
    "    \"\"\"\n",
    "    Apply windowing to an image\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    image : np.ndarray\n",
    "        Input image (typically in HU for CT)\n",
    "    window_center : float\n",
    "        Window center (level)\n",
    "    window_width : float\n",
    "        Window width\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    np.ndarray : Windowed image scaled to 0-255\n",
    "    \"\"\"\n",
    "    \n",
    "    img_min = window_center - window_width // 2\n",
    "    img_max = window_center + window_width // 2\n",
    "    \n",
    "    windowed = np.clip(image, img_min, img_max)\n",
    "    windowed = ((windowed - img_min) / (img_max - img_min) * 255.0).astype(np.uint8)\n",
    "    \n",
    "    return windowed\n",
    "\n",
    "def display_multiple_windows(dicom_obj):\n",
    "    \"\"\"\n",
    "    Display the same CT image with different window settings\n",
    "    \"\"\"\n",
    "    \n",
    "    if dicom_obj.Modality != 'CT':\n",
    "        print(\"Windowing demonstration is most relevant for CT images.\")\n",
    "        return\n",
    "    \n",
    "    hu_image = convert_to_hounsfield(dicom_obj)\n",
    "    \n",
    "    # Define different window settings\n",
    "    windows = {\n",
    "        'Soft Tissue': (40, 400),\n",
    "        'Lung': (-600, 1500),\n",
    "        'Bone': (400, 1800),\n",
    "        'Brain': (40, 80)\n",
    "    }\n",
    "    \n",
    "    fig, axes = plt.subplots(2, 2, figsize=(15, 15))\n",
    "    axes = axes.ravel()\n",
    "    \n",
    "    for idx, (name, (center, width)) in enumerate(windows.items()):\n",
    "        windowed = apply_window(hu_image, center, width)\n",
    "        axes[idx].imshow(windowed, cmap='gray')\n",
    "        axes[idx].set_title(f'{name} Window\\nCenter: {center}, Width: {width}')\n",
    "        axes[idx].axis('off')\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "# Display with different windows if CT\n",
    "if dicom_data.Modality == 'CT':\n",
    "    display_multiple_windows(dicom_data)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86308228",
   "metadata": {},
   "source": [
    "## Ενότητα 13 — Φόρτωση Πολλών Αρχείων (Series)\n",
    "\n",
    "### Τι είναι μια «σειρά» (series);\n",
    "\n",
    "Σπάνια μια ιατρική εξέταση είναι μία μόνο εικόνα. Πιο συχνά είναι μια **σειρά τομών** (slices) που μαζί συνθέτουν τρισδιάστατη πληροφορία:\n",
    "\n",
    "- **CT θώρακος**: 200–500 αξονικές τομές, μία ανά ~1mm.\n",
    "- **MRI εγκεφάλου**: 30–200 τομές ανά ακολουθία, και πολλές ακολουθίες (T1, T2, FLAIR, DWI...).\n",
    "- **DCE-MRI**: τομές × χρονικές στιγμές → 4D δεδομένα.\n",
    "\n",
    "Στο DICOM, **κάθε τομή είναι ξεχωριστό αρχείο**. Μια σειρά είναι ένας **φάκελος** με δεκάδες ή εκατοντάδες αρχεία.\n",
    "\n",
    "### Η ιεραρχία DICOM\n",
    "\n",
    "```\n",
    "Patient\n",
    "└── Study  (μια εξέταση, π.χ. «MRI εγκεφάλου 15/03/2024»)\n",
    "    ├── Series 1: T1\n",
    "    │   ├── Image 1 (slice 1)\n",
    "    │   ├── Image 2 (slice 2)\n",
    "    │   └── ...\n",
    "    ├── Series 2: T2\n",
    "    └── Series 3: FLAIR\n",
    "```\n",
    "\n",
    "Κάθε επίπεδο έχει το δικό του **UID**: `PatientID`, `StudyInstanceUID`, `SeriesInstanceUID`, `SOPInstanceUID`.\n",
    "\n",
    "### Το κρίσιμο πρόβλημα: η σειρά των τομών\n",
    "\n",
    "Όταν διαβάζετε αρχεία από έναν φάκελο, τα παίρνετε **σε αλφαβητική σειρά** του filename. Αυτή **σπανίως** είναι η σωστή ανατομική σειρά! Παράδειγμα: `IM_0001.dcm, IM_0010.dcm, IM_0011.dcm, ..., IM_0002.dcm` — αλφαβητικά αλλά όχι σωστά.\n",
    "\n",
    "### Δύο τρόποι σωστής ταξινόμησης\n",
    "\n",
    "#### 1. `InstanceNumber`\n",
    "Ένας ακέραιος που δίνει το μηχάνημα. Συνήθως αξιόπιστο.\n",
    "\n",
    "```python\n",
    "dicom_files.sort(key=lambda x: int(x.InstanceNumber))\n",
    "```\n",
    "\n",
    "#### 2. `ImagePositionPatient[2]` (z-coordinate)\n",
    "Η **πραγματική** ανατομική θέση στον άξονα z (κρανιοκαυδικός). Πιο αξιόπιστο όταν το `InstanceNumber` είναι αναξιόπιστο.\n",
    "\n",
    "```python\n",
    "dicom_files.sort(key=lambda x: float(x.ImagePositionPatient[2]))\n",
    "```\n",
    "\n",
    "### Τι κάνει η `load_dicom_series`\n",
    "\n",
    "1. Διασχίζει τον φάκελο με `os.listdir`.\n",
    "2. Προσπαθεί να φορτώσει κάθε αρχείο. Αν αποτύχει (π.χ. δεν είναι DICOM), το προσπερνάει.\n",
    "3. Ταξινομεί κατά `InstanceNumber`.\n",
    "4. Επιστρέφει τη λίστα.\n",
    "\n",
    "> 💡 **Πιο εύρωστο:** Χρησιμοποιήστε `pydicom.dcmread(filepath, stop_before_pixels=True)` αρχικά για ταχύτητα, ταξινομήστε, και μόνο μετά διαβάστε τα pixel των αρχείων που σας ενδιαφέρουν.\n",
    "\n",
    "### Τι κάνει το demo\n",
    "\n",
    "Αντί για πραγματικό φάκελο, ο κώδικας φορτώνει **τρία διαφορετικά** built-in test files (`CT_small.dcm`, `MR_small.dcm`, `rtplan.dcm`) για να έχετε υλικό για επόμενες ενότητες (montage, viewer). Αυτά **δεν** ανήκουν στην ίδια σειρά — απλά είναι demos.\n",
    "\n",
    "> 🧠 **Σημείωση:** Σε πραγματική σειρά, όλα τα αρχεία θα είχαν τον **ίδιο** `SeriesInstanceUID`. Αυτό είναι ο σίγουρος τρόπος να επιβεβαιώσετε ότι ανήκουν μαζί.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7a9355c2",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║          LOADING MULTIPLE DICOM FILES (SERIES)                 ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "Medical imaging studies often consist of multiple slices (series).\n",
    "Let's learn how to load and navigate through them.\n",
    "\"\"\")\n",
    "\n",
    "def load_dicom_series(directory_path):\n",
    "    \"\"\"\n",
    "    Load all DICOM files from a directory and sort them by instance number\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    directory_path : str\n",
    "        Path to directory containing DICOM files\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    list : List of sorted DICOM objects\n",
    "    \"\"\"\n",
    "    \n",
    "    dicom_files = []\n",
    "    \n",
    "    # Get all files in directory\n",
    "    for filename in os.listdir(directory_path):\n",
    "        filepath = os.path.join(directory_path, filename)\n",
    "        try:\n",
    "            dcm = pydicom.dcmread(filepath)\n",
    "            dicom_files.append(dcm)\n",
    "        except:\n",
    "            continue  # Skip non-DICOM files\n",
    "    \n",
    "    # Sort by Instance Number if available\n",
    "    try:\n",
    "        dicom_files.sort(key=lambda x: int(x.InstanceNumber))\n",
    "    except:\n",
    "        print(\"Warning: Could not sort by InstanceNumber\")\n",
    "    \n",
    "    print(f\"Loaded {len(dicom_files)} DICOM files\")\n",
    "    return dicom_files\n",
    "\n",
    "# For demonstration, we'll load multiple sample files\n",
    "print(\"Loading sample DICOM series...\")\n",
    "\n",
    "# Get multiple sample files for demonstration\n",
    "sample_files = []\n",
    "try:\n",
    "    # Try to load multiple sample files\n",
    "    sample_paths = ['CT_small.dcm', 'MR_small.dcm', 'rtplan.dcm']\n",
    "    for path_name in sample_paths:\n",
    "        try:\n",
    "            dcm = pydicom.dcmread(get_testdata_file(path_name))\n",
    "            # Only keep files that actually have image pixel data.\n",
    "            # Files like rtplan.dcm carry treatment-plan info, not pixels.\n",
    "            if hasattr(dcm, 'PixelData'):\n",
    "                sample_files.append(dcm)\n",
    "        except Exception:\n",
    "            pass\n",
    "    print(f\"Loaded {len(sample_files)} sample files for demonstration\")\n",
    "except Exception as e:\n",
    "    print(f\"Note: {e}\")\n",
    "    sample_files = [dicom_data]  # Use our original file\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7fe68c57",
   "metadata": {},
   "source": [
    "## Ενότητα 14 — Διαδραστικός Slice Viewer (κλάση `DICOMSliceViewer`)\n",
    "\n",
    "### Γιατί κλάση και όχι απλές συναρτήσεις;\n",
    "\n",
    "Μέχρι τώρα γράφαμε **συναρτήσεις**: εργαλεία που παίρνουν δεδομένα και επιστρέφουν αποτελέσματα. Όταν όμως έχετε:\n",
    "- Δεδομένα που θέλετε να **κρατήσετε** (η σειρά τομών),\n",
    "- Πολλές διαφορετικές λειτουργίες πάνω στα ίδια δεδομένα (show, montage, 3D, orthogonal views),\n",
    "- Κατάσταση που αλλάζει (ποια τομή βλέπω τώρα),\n",
    "\n",
    "τότε η **κλάση** είναι η σωστή δομή. Μια κλάση «δένει» δεδομένα και λειτουργίες σε ένα αντικείμενο.\n",
    "\n",
    "### Η ανατομία της κλάσης\n",
    "\n",
    "```python\n",
    "class DICOMSliceViewer:\n",
    "    def __init__(self, dicom_series):  # constructor\n",
    "        self.dicom_series = dicom_series\n",
    "        self.current_slice = 0\n",
    "        self.num_slices = len(dicom_series)\n",
    "    \n",
    "    def show_slice(self, slice_idx): ...\n",
    "    def show_montage(self, num_cols=4): ...\n",
    "    def create_3d_volume(self): ...\n",
    "    def show_orthogonal_views(self): ...\n",
    "```\n",
    "\n",
    "- **`__init__`**: «κατασκευαστής». Καλείται όταν φτιάχνετε αντικείμενο: `viewer = DICOMSliceViewer(files)`. Αποθηκεύει τα δεδομένα ως **attributes** (`self.dicom_series`).\n",
    "- **`self`**: αναφέρεται στο τρέχον αντικείμενο. Είναι το πρώτο όρισμα κάθε method.\n",
    "- **Methods**: συναρτήσεις-μέλη της κλάσης. Έχουν πρόσβαση στα attributes.\n",
    "\n",
    "### Οι τέσσερις λειτουργίες\n",
    "\n",
    "#### 1. `show_slice(slice_idx)` — μία τομή τη φορά\n",
    "Ζωγραφίζει συγκεκριμένη τομή με matplotlib και δείχνει info (instance number, position).\n",
    "\n",
    "#### 2. `show_montage(num_cols=4)` — όλες οι τομές σε grid\n",
    "Όλες μαζί σε πλέγμα. Χρήσιμο για **γρήγορη επισκόπηση** ολόκληρης της σειράς πριν εμβαθύνετε.\n",
    "\n",
    "#### 3. `create_3d_volume()` — από 2D σε 3D\n",
    "Στοιβάζει τις τομές σε **3D NumPy array** σχήματος `(slices, rows, columns)`.\n",
    "```python\n",
    "volume = np.stack([dcm.pixel_array for dcm in self.dicom_series], axis=0)\n",
    "```\n",
    "Αυτή είναι η βάση για **ογκομετρική ανάλυση**: segmentation, registration, MIP κ.λπ.\n",
    "\n",
    "#### 4. `show_orthogonal_views()` — οι τρεις ανατομικές προβολές\n",
    "\n",
    "Από τον 3D όγκο εξάγουμε τρεις προβολές μέσα από τη μέση:\n",
    "\n",
    "| Προβολή | Slicing | Τι δείχνει |\n",
    "|---------|---------|------------|\n",
    "| **Axial** (εγκάρσια) | `volume[mid_z, :, :]` | Όπως κοιτάζουμε από τα πόδια προς το κεφάλι |\n",
    "| **Coronal** (στεφανιαία) | `volume[:, mid_y, :]` | Από εμπρός προς πίσω |\n",
    "| **Sagittal** (οβελιαία) | `volume[:, :, mid_x]` | Από δεξιά προς αριστερά |\n",
    "\n",
    "> 🎯 **Σημαντικό:** Στις coronal και sagittal προβολές, χρειάζεστε `aspect='auto'` ή ρητή κλιμάκωση γιατί συχνά το **slice spacing** διαφέρει από το **pixel spacing** — αν δεν διορθώσετε, η εικόνα θα φαίνεται «πατικωμένη» ή «τραβηγμένη».\n",
    "\n",
    "### Παιδαγωγικά σημεία\n",
    "\n",
    "> 💡 **OOP για αρχάριους:** Η κλάση είναι σαν «καλούπι». Φτιάχνετε αντικείμενα από αυτή. Κάθε αντικείμενο έχει τα δικά του δεδομένα αλλά τις ίδιες ικανότητες.\n",
    "\n",
    "> 🧠 **Επόμενο βήμα:** Δοκιμάστε να προσθέσετε δικό σας method `apply_window_to_all(center, width)` που εφαρμόζει windowing σε όλες τις τομές της σειράς. Έτσι θα νιώσετε τη δύναμη του OOP.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f475f537",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              INTERACTIVE SLICE VIEWER                          ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "class DICOMSliceViewer:\n",
    "    \"\"\"\n",
    "    Interactive viewer for navigating through DICOM slices\n",
    "    \"\"\"\n",
    "    \n",
    "    def __init__(self, dicom_series):\n",
    "        \"\"\"\n",
    "        Initialize viewer with a series of DICOM files\n",
    "        \n",
    "        Parameters:\n",
    "        -----------\n",
    "        dicom_series : list\n",
    "            List of DICOM objects\n",
    "        \"\"\"\n",
    "        self.dicom_series = dicom_series\n",
    "        self.current_slice = 0\n",
    "        self.num_slices = len(dicom_series)\n",
    "        \n",
    "    def show_slice(self, slice_idx):\n",
    "        \"\"\"Display a specific slice\"\"\"\n",
    "        \n",
    "        if slice_idx < 0 or slice_idx >= self.num_slices:\n",
    "            print(f\"Slice index out of range. Valid range: 0-{self.num_slices-1}\")\n",
    "            return\n",
    "        \n",
    "        self.current_slice = slice_idx\n",
    "        dcm = self.dicom_series[slice_idx]\n",
    "        \n",
    "        plt.figure(figsize=(10, 10))\n",
    "        plt.imshow(dcm.pixel_array, cmap='gray')\n",
    "        plt.title(f'Slice {slice_idx + 1}/{self.num_slices}\\n'\n",
    "                  f'Modality: {dcm.Modality}')\n",
    "        plt.colorbar(label='Pixel Intensity')\n",
    "        plt.axis('on')\n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "        \n",
    "        # Display slice information\n",
    "        print(f\"\\n📊 Slice {slice_idx + 1} Information:\")\n",
    "        print(f\"   Instance Number: {dcm.get('InstanceNumber', 'N/A')}\")\n",
    "        print(f\"   Image Position: {dcm.get('ImagePositionPatient', 'N/A')}\")\n",
    "        print(f\"   Shape: {dcm.pixel_array.shape}\")\n",
    "    \n",
    "    def show_montage(self, num_cols=4):\n",
    "        \"\"\"Display all slices in a montage\"\"\"\n",
    "        \n",
    "        num_slices = len(self.dicom_series)\n",
    "        num_rows = (num_slices + num_cols - 1) // num_cols\n",
    "        \n",
    "        fig, axes = plt.subplots(num_rows, num_cols, figsize=(15, 4*num_rows))\n",
    "        \n",
    "        if num_slices == 1:\n",
    "            axes = np.array([axes])\n",
    "        axes = axes.ravel()\n",
    "        \n",
    "        for idx in range(num_cols * num_rows):\n",
    "            if idx < num_slices:\n",
    "                axes[idx].imshow(self.dicom_series[idx].pixel_array, cmap='gray')\n",
    "                axes[idx].set_title(f'Slice {idx + 1}', fontsize=10)\n",
    "            axes[idx].axis('off')\n",
    "        \n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "    \n",
    "    def create_3d_volume(self):\n",
    "        \"\"\"Stack slices to create a 3D volume\"\"\"\n",
    "        \n",
    "        if self.num_slices < 2:\n",
    "            print(\"Need at least 2 slices to create a volume\")\n",
    "            return None\n",
    "        \n",
    "        # Stack all slices\n",
    "        volume = np.stack([dcm.pixel_array for dcm in self.dicom_series], axis=0)\n",
    "        \n",
    "        print(f\"3D Volume shape: {volume.shape}\")\n",
    "        print(f\"(slices, rows, columns)\")\n",
    "        \n",
    "        return volume\n",
    "    \n",
    "    def show_orthogonal_views(self):\n",
    "        \"\"\"Show axial, coronal, and sagittal views\"\"\"\n",
    "        \n",
    "        volume = self.create_3d_volume()\n",
    "        \n",
    "        if volume is None:\n",
    "            return\n",
    "        \n",
    "        # Get middle slices\n",
    "        mid_slice = volume.shape[0] // 2\n",
    "        mid_row = volume.shape[1] // 2\n",
    "        mid_col = volume.shape[2] // 2\n",
    "        \n",
    "        fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n",
    "        \n",
    "        # Axial view\n",
    "        axes[0].imshow(volume[mid_slice, :, :], cmap='gray')\n",
    "        axes[0].set_title('Axial View')\n",
    "        axes[0].axis('off')\n",
    "        \n",
    "        # Coronal view\n",
    "        axes[1].imshow(volume[:, mid_row, :], cmap='gray', aspect='auto')\n",
    "        axes[1].set_title('Coronal View')\n",
    "        axes[1].axis('off')\n",
    "        \n",
    "        # Sagittal view\n",
    "        axes[2].imshow(volume[:, :, mid_col], cmap='gray', aspect='auto')\n",
    "        axes[2].set_title('Sagittal View')\n",
    "        axes[2].axis('off')\n",
    "        \n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "\n",
    "# Create viewer instance\n",
    "viewer = DICOMSliceViewer(sample_files)\n",
    "\n",
    "print(f\"\\n✓ Viewer created with {viewer.num_slices} slice(s)\")\n",
    "\n",
    "# Show first slice\n",
    "viewer.show_slice(0)\n",
    "\n",
    "# Show montage if multiple slices\n",
    "if len(sample_files) > 1:\n",
    "    viewer.show_montage()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6364f4f",
   "metadata": {},
   "source": [
    "## Ενότητα 15 — Εξαγωγή Πίνακα Metadata από Σειρά\n",
    "\n",
    "### Από τομές σε πίνακα-σύνοψη\n",
    "\n",
    "Όταν δουλεύετε με σειρά **πολλών** τομών, χρειάζεστε γρήγορη επισκόπηση: «τι περιέχει αυτό το dataset;». Η συνάρτηση `extract_series_metadata` φτιάχνει ένα DataFrame με μία γραμμή ανά τομή, ώστε να μπορείτε να ελέγξετε σε ένα ξεκάθαρο πίνακα:\n",
    "\n",
    "| Slice | Instance Number | Modality | Rows | Columns | Slice Thickness | Image Position |\n",
    "|-------|-----------------|----------|------|---------|------------------|------------------|\n",
    "\n",
    "### Πότε χρησιμεύει στην πράξη\n",
    "\n",
    "- **Quality control:** Όλες οι τομές έχουν την ίδια χωρική ανάλυση; Έχουν ίδιο πάχος; Είναι ίδιες διαστάσεις;\n",
    "- **Πλοήγηση:** Σε ποιες τομές βλέπω τη συγκεκριμένη ανατομική περιοχή;\n",
    "- **Διαλογή:** Αν η σειρά έχει «παραπανίσιες» τομές από scout/localizer, τις εντοπίζω και τις αφαιρώ.\n",
    "\n",
    "### Το «είδος» κώδικα: list comprehension με dict\n",
    "\n",
    "Η συνάρτηση συσσωρεύει μία εγγραφή ανά τομή:\n",
    "\n",
    "```python\n",
    "metadata.append({\n",
    "    'Slice': idx + 1,\n",
    "    'Instance Number': dcm.get('InstanceNumber', 'N/A'),\n",
    "    ...\n",
    "})\n",
    "return pd.DataFrame(metadata)\n",
    "```\n",
    "\n",
    "Αυτό το μοτίβο **«λίστα από dictionaries → DataFrame»** είναι ο πιο καθαρός τρόπος να φτιάξετε DataFrame όταν τα δεδομένα έρχονται γραμμή-γραμμή.\n",
    "\n",
    "### Παρατηρήστε την defensive πρόσβαση\n",
    "\n",
    "Παντού `dcm.get('TagName', 'N/A')` αντί για `dcm.TagName`. Σε πραγματικές σειρές, θα συναντήσετε τομές που λείπουν tags (ειδικά σε ανωνυμοποιημένα ή παλιά δεδομένα). Με την `get()`, η συνάρτηση **δουλεύει** ακόμη και τότε.\n",
    "\n",
    "> 💡 **Επέκταση για άσκηση:** Προσθέστε στήλες για `KVP`, `XRayTubeCurrent` (CT) ή `RepetitionTime`, `EchoTime` (MRI). Αυτά τα **acquisition parameters** είναι κρίσιμα για να ξέρετε τι ακριβώς σαρώθηκε.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4fce9270",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║           EXTRACTING METADATA FROM SERIES                      ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "def extract_series_metadata(dicom_series):\n",
    "    \"\"\"\n",
    "    Extract key metadata from a series of DICOM files\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_series : list\n",
    "        List of DICOM objects\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    pd.DataFrame : Metadata table\n",
    "    \"\"\"\n",
    "    \n",
    "    metadata = []\n",
    "    \n",
    "    for idx, dcm in enumerate(dicom_series):\n",
    "        metadata.append({\n",
    "            'Slice': idx + 1,\n",
    "            'Instance Number': dcm.get('InstanceNumber', 'N/A'),\n",
    "            'Modality': dcm.get('Modality', 'N/A'),\n",
    "            'Rows': dcm.get('Rows', 'N/A'),\n",
    "            'Columns': dcm.get('Columns', 'N/A'),\n",
    "            'Slice Thickness': dcm.get('SliceThickness', 'N/A'),\n",
    "            'Image Position': str(dcm.get('ImagePositionPatient', 'N/A'))[:30] + '...'\n",
    "        })\n",
    "    \n",
    "    return pd.DataFrame(metadata)\n",
    "\n",
    "# Extract and display metadata\n",
    "metadata_df = extract_series_metadata(sample_files)\n",
    "display(metadata_df)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bbe13b82",
   "metadata": {},
   "source": [
    "## Ενότητα 16 — Pixel Spacing & Πραγματικές Μετρήσεις\n",
    "\n",
    "### Το θεμελιώδες ερώτημα\n",
    "\n",
    "«Πόσα cm είναι ο όγκος;»\n",
    "Δεν μπορείτε να απαντήσετε αν δεν ξέρετε **πόσα mm είναι κάθε pixel**.\n",
    "\n",
    "### Τα κρίσιμα tags\n",
    "\n",
    "| Tag | Σημασία | Μονάδες |\n",
    "|-----|---------|---------|\n",
    "| **`PixelSpacing`** | Απόσταση μεταξύ κέντρων pixel σε X και Y | mm |\n",
    "| **`SliceThickness`** | Πάχος της κάθε τομής | mm |\n",
    "| **`SpacingBetweenSlices`** | Απόσταση κέντρου-κέντρου μεταξύ διαδοχικών τομών | mm |\n",
    "| **`ImagePositionPatient`** | Συντεταγμένες (x,y,z) του πρώτου pixel σε **patient coordinate system** | mm |\n",
    "| **`ImageOrientationPatient`** | Διανύσματα row/column σε patient coords | unit vectors |\n",
    "\n",
    "### Από pixel σε mm\n",
    "\n",
    "Αν `PixelSpacing = [0.7, 0.7]`, τότε:\n",
    "\n",
    "- Διάσταση εικόνας 512×512 pixels = **358.4 × 358.4 mm**.\n",
    "- Ένας όγκος που εκτείνεται σε 50 pixels = **35 mm**.\n",
    "\n",
    "### Voxel vs Pixel\n",
    "\n",
    "- **Pixel** (picture element): 2D, στοιχείο εικόνας.\n",
    "- **Voxel** (volume element): 3D, στοιχείο όγκου.\n",
    "\n",
    "Σε μια 3D σειρά, ο όγκος ενός voxel είναι:\n",
    "$$\n",
    "V_{voxel} = \\text{PixelSpacing}_x \\times \\text{PixelSpacing}_y \\times \\text{SliceThickness}\n",
    "$$\n",
    "\n",
    "Για να μετρήσετε π.χ. τον **όγκο όγκου**: μετράτε τα voxels στη segmentation και πολλαπλασιάζετε επί $V_{voxel}$.\n",
    "\n",
    "### Anisotropic vs Isotropic δεδομένα\n",
    "\n",
    "- **Isotropic**: ίδια ανάλυση και στις τρεις διαστάσεις (π.χ. 1×1×1 mm). Ιδανικά για 3D ανάλυση και deep learning.\n",
    "- **Anisotropic**: διαφορετική ανάλυση (π.χ. 0.5×0.5×3 mm — συνηθισμένο σε MRI). Σε αυτή την περίπτωση, για 3D αλγορίθμους συχνά χρειάζεται **resampling** σε isotropic.\n",
    "\n",
    "### Τι κάνει η `get_pixel_spacing_info`\n",
    "\n",
    "1. Διαβάζει το `PixelSpacing`.\n",
    "2. Υπολογίζει το **φυσικό μέγεθος** της εικόνας σε mm.\n",
    "3. Διαβάζει το `SliceThickness`.\n",
    "4. Τα τυπώνει.\n",
    "\n",
    "### Συνηθισμένα λάθη στη μέτρηση\n",
    "\n",
    "> ⚠️ **Λάθος #1:** Να μετρήσετε διάμετρο όγκου σε **pixels** και να την αναφέρετε σε αναφορά. Πάντα μετατρέπετε σε mm.\n",
    "\n",
    "> ⚠️ **Λάθος #2:** Να ξεχάσετε ότι το spacing μπορεί να **διαφέρει** μεταξύ σειρών (T1 vs T2). Σε multi-modal ανάλυση, αυτό προκαλεί χάος αν δεν προσέξετε.\n",
    "\n",
    "> ⚠️ **Λάθος #3:** Να αγνοήσετε το `ImageOrientationPatient`. Αν δύο σειρές έχουν διαφορετικό orientation και τις «στοιβάζετε» χωρίς registration, τα voxels δεν αντιστοιχούν στα ίδια ανατομικά σημεία!\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dfc02e20",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║        PIXEL SPACING & REAL-WORLD MEASUREMENTS                 ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "Pixel spacing tells us the physical distance between pixels.\n",
    "This is crucial for accurate measurements!\n",
    "\"\"\")\n",
    "\n",
    "def get_pixel_spacing_info(dicom_obj):\n",
    "    \"\"\"\n",
    "    Extract pixel spacing information\n",
    "    \"\"\"\n",
    "    \n",
    "    print(f\"\\n📏 Spatial Information:\")\n",
    "    print(\"-\" * 50)\n",
    "    \n",
    "    if hasattr(dicom_obj, 'PixelSpacing'):\n",
    "        row_spacing, col_spacing = dicom_obj.PixelSpacing\n",
    "        print(f\"Pixel Spacing (Row, Column): {row_spacing} mm, {col_spacing} mm\")\n",
    "        \n",
    "        # Calculate physical size of the image\n",
    "        physical_width = dicom_obj.Columns * col_spacing\n",
    "        physical_height = dicom_obj.Rows * row_spacing\n",
    "        \n",
    "        print(f\"Image size in pixels: {dicom_obj.Columns} x {dicom_obj.Rows}\")\n",
    "        print(f\"Image size in mm: {physical_width:.2f} x {physical_height:.2f}\")\n",
    "    else:\n",
    "        print(\"Pixel spacing information not available\")\n",
    "    \n",
    "    if hasattr(dicom_obj, 'SliceThickness'):\n",
    "        print(f\"Slice Thickness: {dicom_obj.SliceThickness} mm\")\n",
    "\n",
    "get_pixel_spacing_info(dicom_data)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d223179f",
   "metadata": {},
   "source": [
    "## Ενότητα 17 — Στοιχειώδης Επεξεργασία Εικόνας\n",
    "\n",
    "### Έξι βασικές πράξεις σε μία ματιά\n",
    "\n",
    "Σε αυτή την ενότητα παρουσιάζονται έξι **θεμελιώδεις** μεταμορφώσεις που θα συναντάτε **παντού** στην ανάλυση εικόνας:\n",
    "\n",
    "| Operation | Τι κάνει | Τυπική χρήση |\n",
    "|-----------|----------|---------------|\n",
    "| **Original** | Αναφορά (καμία αλλαγή) | Σύγκριση |\n",
    "| **Histogram** | Κατανομή τιμών | Ποσοτική σύνοψη |\n",
    "| **Normalization** | Κλιμάκωση σε [0, 1] | Pre-processing για ML |\n",
    "| **Inversion** | `max − pixel` | Αλλαγή «πολικότητας» (π.χ. αρνητικό X-ray) |\n",
    "| **Thresholding** | Δυαδικό: pixel > T → 1, αλλιώς 0 | Πρώτο βήμα segmentation |\n",
    "| **Edge detection** | Ανίχνευση κλίσης | Ορισμός ορίων |\n",
    "\n",
    "### Παιδαγωγικά σημεία ανά πράξη\n",
    "\n",
    "#### Normalization\n",
    "```python\n",
    "img_norm = (img - img.min()) / (img.max() - img.min())\n",
    "```\n",
    "Λεγόμενη **min-max normalization**. Εξαιρετικά συνηθισμένη στο preprocessing για deep learning. **Προσοχή:** ευαίσθητη σε outliers — αν ένα pixel είναι ακραίο, «συμπιέζει» όλα τα άλλα. Εναλλακτική: **z-score** (`(img - mean) / std`) ή **percentile clipping** (κλιπ στο 1ο και 99ο percentile πριν τη normalization).\n",
    "\n",
    "#### Inversion\n",
    "```python\n",
    "img_inv = img.max() - img\n",
    "```\n",
    "Σε **CT/X-ray**, οι ακτινολόγοι μερικές φορές προτιμούν inverted προβολή («negative») για να βλέπουν καλύτερα μαλακούς ιστούς.\n",
    "\n",
    "#### Thresholding\n",
    "```python\n",
    "threshold = img.mean()\n",
    "binary = img > threshold\n",
    "```\n",
    "Είναι η **απλούστερη μορφή segmentation**. Χρήσιμη όταν η εικόνα έχει σαφή διαχωρισμό ιστών (π.χ. οστά σε CT). Πιο sophisticated εκδοχές:\n",
    "- **Otsu**: αυτόματη επιλογή κατωφλίου από το ιστόγραμμα.\n",
    "- **Adaptive thresholding**: τοπικό κατώφλι που προσαρμόζεται σε διαφορετικές περιοχές.\n",
    "\n",
    "#### Edge detection με gradient\n",
    "```python\n",
    "edges = |∂I/∂x| + |∂I/∂y|\n",
    "```\n",
    "Πολύ απλή προσέγγιση. Πιο εξελιγμένα:\n",
    "- **Sobel filter**: σταθμισμένη πρώτη παράγωγος.\n",
    "- **Canny edge detector**: full pipeline με smoothing, non-max suppression, double threshold.\n",
    "- **Laplacian of Gaussian (LoG)**: ανίχνευση blob-like δομών.\n",
    "\n",
    "> 💡 **Σύνδεση με προχωρημένες έννοιες:** Όλα τα παραπάνω είναι «κλασικές» μέθοδοι (pre-deep-learning). Σήμερα συχνά αντικαθίστανται από CNN-based μοντέλα (U-Net, nnU-Net) που μαθαίνουν την κατάλληλη μετατροπή από δεδομένα. Αλλά **οι βασικές αρχές παραμένουν** — και το να καταλαβαίνετε τις κλασικές μεθόδους κάνει καλύτερο deep learning practitioner.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ff720629",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              BASIC IMAGE PROCESSING                            ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "def demonstrate_image_processing(dicom_obj):\n",
    "    \"\"\"\n",
    "    Demonstrate basic image processing operations\n",
    "    \"\"\"\n",
    "    \n",
    "    img = dicom_obj.pixel_array.astype(float)\n",
    "    \n",
    "    fig, axes = plt.subplots(2, 3, figsize=(15, 10))\n",
    "    \n",
    "    # Original\n",
    "    axes[0, 0].imshow(img, cmap='gray')\n",
    "    axes[0, 0].set_title('Original')\n",
    "    axes[0, 0].axis('off')\n",
    "    \n",
    "    # Histogram\n",
    "    axes[0, 1].hist(img.ravel(), bins=50, color='blue', alpha=0.7)\n",
    "    axes[0, 1].set_title('Histogram')\n",
    "    axes[0, 1].set_xlabel('Pixel Intensity')\n",
    "    axes[0, 1].set_ylabel('Frequency')\n",
    "    \n",
    "    # Normalized\n",
    "    img_norm = (img - img.min()) / (img.max() - img.min())\n",
    "    axes[0, 2].imshow(img_norm, cmap='gray')\n",
    "    axes[0, 2].set_title('Normalized [0, 1]')\n",
    "    axes[0, 2].axis('off')\n",
    "    \n",
    "    # Inverted\n",
    "    img_inv = img.max() - img\n",
    "    axes[1, 0].imshow(img_inv, cmap='gray')\n",
    "    axes[1, 0].set_title('Inverted')\n",
    "    axes[1, 0].axis('off')\n",
    "    \n",
    "    # Thresholded\n",
    "    threshold = img.mean()\n",
    "    img_thresh = img > threshold\n",
    "    axes[1, 1].imshow(img_thresh, cmap='gray')\n",
    "    axes[1, 1].set_title(f'Thresholded (>{threshold:.0f})')\n",
    "    axes[1, 1].axis('off')\n",
    "    \n",
    "    # Edge detection (simple gradient)\n",
    "    img_edges = np.abs(np.gradient(img)[0]) + np.abs(np.gradient(img)[1])\n",
    "    axes[1, 2].imshow(img_edges, cmap='hot')\n",
    "    axes[1, 2].set_title('Edge Detection')\n",
    "    axes[1, 2].axis('off')\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "demonstrate_image_processing(dicom_data)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0fac348d",
   "metadata": {},
   "source": [
    "## Ενότητα 18 — Αποθήκευση Εικόνων\n",
    "\n",
    "### Γιατί να αποθηκεύσουμε;\n",
    "\n",
    "Αφού επεξεργαστούμε ή αναλύσουμε μια εικόνα, συχνά θέλουμε:\n",
    "\n",
    "- **Να τη μοιραστούμε** με συναδέλφους που δεν έχουν Python.\n",
    "- **Να την εντάξουμε σε αναφορά** (PDF, παρουσίαση).\n",
    "- **Να χτίσουμε dataset** για machine learning.\n",
    "- **Να την επιστρέψουμε** στο PACS του νοσοκομείου ως νέο DICOM.\n",
    "\n",
    "### Τρεις μορφές, τρεις σκοποί\n",
    "\n",
    "| Format | Πότε το χρησιμοποιούμε | Διατηρεί metadata; |\n",
    "|--------|--------------------------|---------------------|\n",
    "| **PNG** | Για αναφορές, παρουσιάσεις, web | ❌ Όχι |\n",
    "| **NumPy (`.npy`)** | Για επόμενη επεξεργασία σε Python | ❌ Όχι |\n",
    "| **DICOM (`.dcm`)** | Για επιστροφή σε PACS, μοιραστείτε με ιατρικό λογισμικό | ✅ Ναι |\n",
    "\n",
    "### PNG για παρουσιάσεις\n",
    "\n",
    "```python\n",
    "img_normalized = ((pixel_array - pixel_array.min()) /\n",
    "                  (pixel_array.max() - pixel_array.min()) * 255).astype(np.uint8)\n",
    "Image.fromarray(img_normalized).save(\"output.png\")\n",
    "```\n",
    "\n",
    "> ⚠️ **Σημαντικό:** Το PNG είναι 8-bit (ή 16-bit). Αν αποθηκεύσετε CT απευθείας, θα **χάσετε ακρίβεια**. Αυτός είναι ο λόγος που πρώτα κανονικοποιούμε στο [0, 255]. Αυτό είναι αποδεκτό για **οπτικοποίηση** αλλά **όχι** για ποσοτική ανάλυση.\n",
    "\n",
    "### NumPy για επόμενα στάδια\n",
    "\n",
    "```python\n",
    "np.save(\"output.npy\", pixel_array)\n",
    "# ... αργότερα ...\n",
    "loaded = np.load(\"output.npy\")\n",
    "```\n",
    "\n",
    "Διατηρείται το `dtype` και η ακρίβεια. Είναι το **πιο γρήγορο** format για επαναφόρτωση σε Python.\n",
    "\n",
    "### DICOM για κλινική επιστροφή\n",
    "\n",
    "```python\n",
    "dicom_obj_copy = dicom_obj.copy()\n",
    "dicom_obj_copy.SeriesDescription = \"Processed by Student\"\n",
    "dicom_obj_copy.save_as(\"output.dcm\")\n",
    "```\n",
    "\n",
    "Διατηρούνται όλα τα tags. **Σημαντικό:** σε σοβαρή χρήση, πρέπει να αλλάξετε:\n",
    "- **`SOPInstanceUID`** (νέο μοναδικό UID για το νέο αρχείο).\n",
    "- **`SeriesInstanceUID`** (νέα σειρά).\n",
    "- Κάποιο **derivation flag** για να δηλώσετε ότι το αρχείο είναι παράγωγο.\n",
    "\n",
    "Αλλιώς το PACS μπορεί να μπερδέψει τα νέα αρχεία με τα αρχικά.\n",
    "\n",
    "### Κλειδί ασφαλείας\n",
    "\n",
    "> 🚨 **ΠΑΝΤΑ** δουλεύετε σε **αντίγραφο** (`.copy()`). Ποτέ μην τροποποιείτε το original DICOM στη μνήμη και μετά το αποθηκεύετε με το ίδιο όνομα. Διατήρηση των πρωτότυπων δεδομένων είναι θεμελιώδης κανόνας.\n",
    "\n",
    "### Κλειδί ιδιωτικότητας\n",
    "\n",
    "> 🔐 **ΠΑΝΤΑ** ανωνυμοποιήστε πριν μοιραστείτε. Δείτε την Ενότητα 21 για το πώς. Ένα μόνο PNG που γλιστρά με όνομα ασθενούς στο burnin (κειμενική επικάλυψη) μπορεί να είναι παραβίαση GDPR.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "54d547ae",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              SAVING DICOM AND OTHER FORMATS                    ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "def save_dicom_as_formats(dicom_obj, output_prefix=\"output\"):\n",
    "    \"\"\"\n",
    "    Save DICOM image in different formats\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        DICOM object to save\n",
    "    output_prefix : str\n",
    "        Prefix for output files\n",
    "    \"\"\"\n",
    "    \n",
    "    pixel_array = dicom_obj.pixel_array\n",
    "    \n",
    "    # Normalize to 0-255 for standard image formats\n",
    "    img_normalized = ((pixel_array - pixel_array.min()) / \n",
    "                      (pixel_array.max() - pixel_array.min()) * 255).astype(np.uint8)\n",
    "    \n",
    "    # Save as PNG\n",
    "    from PIL import Image\n",
    "    img_pil = Image.fromarray(img_normalized)\n",
    "    png_path = f\"{output_prefix}.png\"\n",
    "    img_pil.save(png_path)\n",
    "    print(f\"✓ Saved as PNG: {png_path}\")\n",
    "    \n",
    "    # Save as NumPy array\n",
    "    npy_path = f\"{output_prefix}.npy\"\n",
    "    np.save(npy_path, pixel_array)\n",
    "    print(f\"✓ Saved as NumPy: {npy_path}\")\n",
    "    \n",
    "    # Save modified DICOM (example: add a tag)\n",
    "    dicom_obj_copy = dicom_obj.copy()\n",
    "    dicom_obj_copy.SeriesDescription = \"Processed by Student\"\n",
    "    dcm_path = f\"{output_prefix}.dcm\"\n",
    "    dicom_obj_copy.save_as(dcm_path)\n",
    "    print(f\"✓ Saved as DICOM: {dcm_path}\")\n",
    "\n",
    "# Uncomment to save files\n",
    "# save_dicom_as_formats(dicom_data, \"my_processed_image\")\n",
    "print(\"(Saving functions defined - uncomment to use)\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9a1f2e7",
   "metadata": {},
   "source": [
    "## Ενότητα 19 — Αναζήτηση και Φιλτράρισμα Tags\n",
    "\n",
    "### Το πρόβλημα: τα tags είναι ΠΟΛΛΑ\n",
    "\n",
    "Ένα τυπικό DICOM περιέχει **50-300+ tags**. Πώς βρίσκετε αυτό που ψάχνετε;\n",
    "\n",
    "### Η συνάρτηση `search_dicom_tags`\n",
    "\n",
    "Δέχεται ένα DICOM και έναν **όρο αναζήτησης** (string), και επιστρέφει DataFrame με όλα τα tags των οποίων το όνομα **περιέχει** τον όρο:\n",
    "\n",
    "```python\n",
    "patient_tags = search_dicom_tags(dicom_data, 'Patient')\n",
    "# → όλα τα tags με \"Patient\" στο όνομα: PatientName, PatientID, PatientBirthDate, ...\n",
    "```\n",
    "\n",
    "### Πώς λειτουργεί η iteration σε DICOM\n",
    "\n",
    "Το `for elem in dicom_obj` διασχίζει **κάθε στοιχείο** του dataset. Κάθε `elem` έχει:\n",
    "\n",
    "- `.name` — ανθρώπινα διαβάσιμο όνομα (π.χ. \"Patient's Name\")\n",
    "- `.tag` — το (group, element) σε hex\n",
    "- `.VR` — ο τύπος δεδομένων\n",
    "- `.value` — η ίδια η τιμή\n",
    "\n",
    "### Πρακτικές χρήσεις\n",
    "\n",
    "| Όρος | Τι βρίσκετε |\n",
    "|------|--------------|\n",
    "| `'Patient'` | Δημογραφικά |\n",
    "| `'Image'` | Παράμετροι εικόνας |\n",
    "| `'Pixel'` | Pixel-σχετικά (`PixelSpacing`, `PixelData`, `BitsStored`...) |\n",
    "| `'Date'` | Όλες οι ημερομηνίες (study, series, acquisition...) |\n",
    "| `'Time'` | Ώρες |\n",
    "| `'UID'` | Όλα τα μοναδικά αναγνωριστικά |\n",
    "| `'Window'` | Default window settings |\n",
    "\n",
    "### Παρατήρηση: τα `display(...)` calls\n",
    "\n",
    "Στο Jupyter notebook, η `display()` είναι σαν `print()` αλλά «έξυπνη» για αντικείμενα όπως DataFrame — τα δείχνει σε **όμορφο HTML πίνακα** αντί για ASCII. Αν είστε σε plain Python script, χρησιμοποιήστε `print(df)` ή `df.to_string()`.\n",
    "\n",
    "> 💡 **Tip:** Αν θέλετε να δείτε **όλα** τα tags ενός DICOM χωρίς φίλτρο, απλά κάντε `print(dicom_data)`. Παίρνετε ολόκληρη τη header.\n",
    "\n",
    "> 🎯 **Άσκηση μνήμης:** Όταν συναντάτε ένα νέο tag, ψάξτε το στο **innolitics DICOM browser** (https://dicom.innolitics.com) — εκεί θα δείτε τη μορφή, τα όρια, το VR, και πού ανήκει στο πρότυπο.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6c8da297",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              SEARCHING AND FILTERING TAGS                      ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "def search_dicom_tags(dicom_obj, search_term):\n",
    "    \"\"\"\n",
    "    Search for DICOM tags containing a specific term\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        DICOM object\n",
    "    search_term : str\n",
    "        Term to search for in tag names\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    pd.DataFrame : Matching tags\n",
    "    \"\"\"\n",
    "    \n",
    "    matches = []\n",
    "    \n",
    "    for elem in dicom_obj:\n",
    "        tag_name = elem.name\n",
    "        if search_term.lower() in tag_name.lower():\n",
    "            matches.append({\n",
    "                'Tag Name': tag_name,\n",
    "                'Tag': str(elem.tag),\n",
    "                'VR': elem.VR,\n",
    "                'Value': str(elem.value)[:50]  # Truncate long values\n",
    "            })\n",
    "    \n",
    "    return pd.DataFrame(matches)\n",
    "\n",
    "# Search for patient-related tags\n",
    "print(\"\\n🔍 Searching for 'Patient' tags:\")\n",
    "patient_tags = search_dicom_tags(dicom_data, 'Patient')\n",
    "display(patient_tags)\n",
    "\n",
    "# Search for image-related tags\n",
    "print(\"\\n🔍 Searching for 'Image' tags:\")\n",
    "image_tags = search_dicom_tags(dicom_data, 'Image')\n",
    "display(image_tags)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6ba889ae",
   "metadata": {},
   "source": [
    "## Ενότητα 20 — Ασκήσεις Πρακτικής (Μέρος Α)\n",
    "\n",
    "### Πέντε ασκήσεις πάνω στις Ενότητες 1-19\n",
    "\n",
    "Έχετε δει αρκετά. Τώρα είναι ώρα να **δουλέψετε μόνοι σας**. Αυτές οι ασκήσεις δεν λύνονται με αντιγραφή — απαιτούν να συνδέσετε **διαφορετικά κομμάτια** του υλικού.\n",
    "\n",
    "### Σύντομες οδηγίες\n",
    "\n",
    "| Άσκηση | Τι εξασκεί | Σχετικές ενότητες |\n",
    "|--------|------------|-------------------|\n",
    "| **1. Tag Exploration** | Ανάγνωση + φυσικές μετρήσεις | 4, 6, 16 |\n",
    "| **2. Image Analysis** | Στατιστική + ιστόγραμμα + threshold | 9, 17 |\n",
    "| **3. Series Navigation** | Φόρτωση φακέλου + montage + όγκος κάλυψης | 13, 14, 16 |\n",
    "| **4. Window/Level** | Hounsfield + windows | 11, 12 |\n",
    "| **5. Metadata Comparison** | Σύγκριση modalities | 5, 6, 8 |\n",
    "\n",
    "### Πώς να δουλέψετε\n",
    "\n",
    "> 1️⃣ **Διαβάστε όλη την άσκηση πριν αρχίσετε.** Εντοπίστε τι ακριβώς ζητείται.\n",
    "> 2️⃣ **Σχεδιάστε το pipeline στα ελληνικά** σε χαρτί ή σε σχόλια. Ποια συνάρτηση έρχεται πρώτη;\n",
    "> 3️⃣ **Γράψτε τον κώδικα βήμα-βήμα** και τρέξτε συχνά για να βλέπετε ενδιάμεσα αποτελέσματα.\n",
    "> 4️⃣ **Επικυρώστε** με αναμενόμενες τιμές. Π.χ. αν μετράτε pixel σε mm, η τιμή πρέπει να είναι **λογική** (όγκος εγκεφάλου ≈ 1.4 L).\n",
    "> 5️⃣ **Εξηγήστε γραπτώς** τι είδατε. Ο κώδικας **ΧΩΡΙΣ ερμηνεία** δεν είναι παράδοση — είναι μισή δουλειά.\n",
    "\n",
    "### Παγίδες που να αποφύγετε\n",
    "\n",
    "> ⚠️ **Παγίδα 1:** Να ξεχάσετε `pixel_array.astype(float)` πριν τις πράξεις. Σε `uint16`, ένα `pixel - 100` μπορεί να σας δώσει κενό λόγω overflow.\n",
    "\n",
    "> ⚠️ **Παγίδα 2:** Να χρησιμοποιήσετε **raw pixel values** σε CT αντί για HU. Όλα σας τα νούμερα θα είναι λάθος.\n",
    "\n",
    "> ⚠️ **Παγίδα 3:** Να μην ταξινομήσετε τις τομές πριν φτιάξετε τον 3D όγκο. Θα έχετε «παγωτό» από ασύνδετες τομές.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "316f4abf",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                    PRACTICE EXERCISES                          ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "📝 EXERCISE 1: Tag Exploration\n",
    "   - Load a DICOM file\n",
    "   - Extract and print: Patient Name, Study Date, Modality\n",
    "   - Calculate the physical size of the image in mm\n",
    "\n",
    "📝 EXERCISE 2: Image Analysis\n",
    "   - Load a DICOM image\n",
    "   - Calculate: mean, median, standard deviation of pixel values\n",
    "   - Create a histogram of pixel intensities\n",
    "   - Apply a threshold to segment the image\n",
    "\n",
    "📝 EXERCISE 3: Series Navigation\n",
    "   - Load a series of DICOM files\n",
    "   - Sort them by slice location\n",
    "   - Create a montage view\n",
    "   - Calculate the total volume coverage (in mm)\n",
    "\n",
    "📝 EXERCISE 4: Window/Level\n",
    "   - Load a CT image\n",
    "   - Apply different window settings\n",
    "   - Compare: soft tissue, lung, and bone windows\n",
    "   - Explain what each window emphasizes\n",
    "\n",
    "📝 EXERCISE 5: Metadata Comparison\n",
    "   - Load multiple DICOM files from different modalities\n",
    "   - Create a comparison table of key tags\n",
    "   - Identify which tags are modality-specific\n",
    "\"\"\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "399686f3",
   "metadata": {},
   "source": [
    "## Ενότητα 21 — Βιβλιοθήκη Βοηθητικών Συναρτήσεων (κλάση `DICOMUtilities`)\n",
    "\n",
    "### Γιατί τα ομαδοποιούμε σε κλάση;\n",
    "\n",
    "Έχουμε τέσσερις διαφορετικές βοηθητικές λειτουργίες που **δεν χρειάζονται κατάσταση** (state). Είναι «καθαρά εργαλεία». Τα μαζεύουμε σε μια κλάση **για οργάνωση**, χρησιμοποιώντας `@staticmethod`:\n",
    "\n",
    "- **`@staticmethod`** σημαίνει «αυτή η συνάρτηση δεν χρειάζεται `self`». Είναι σαν συνηθισμένη συνάρτηση, απλά «ζει» μέσα στην κλάση για namespace.\n",
    "\n",
    "Έτσι μπορείτε να κάνετε `DICOMUtilities.anonymize_dicom(dcm)` ή `DICOMUtilities.calculate_slice_spacing(series)` χωρίς να χρειάζεται να φτιάξετε αντικείμενο.\n",
    "\n",
    "### Οι τέσσερις λειτουργίες\n",
    "\n",
    "#### 1. `anonymize_dicom` — αφαίρεση προσωπικών στοιχείων\n",
    "**Νομικά κρίσιμη.** Αντικαθιστά PHI tags με 'ANONYMIZED':\n",
    "- PatientName, PatientID, PatientBirthDate, PatientSex, PatientAge, PatientAddress\n",
    "\n",
    "> 🚨 **ΠΡΟΣΟΧΗ — η συγκεκριμένη υλοποίηση είναι ΕΛΑΧΙΣΤΗ.** Πραγματική ανωνυμοποίηση πρέπει να καλύπτει:\n",
    "> - **All PHI tags** (`InstitutionName`, `ReferringPhysicianName`, `OperatorsName`, `StudyID`, ...).\n",
    "> - **Burnt-in pixel data** (κείμενο μέσα στην ίδια την εικόνα — ονόματα, ημερομηνίες). Σε ορισμένες ακολουθίες, εμφανίζονται **πάνω** στην εικόνα.\n",
    "> - **Private tags** που πιθανόν περιέχουν proprietary metadata.\n",
    "> - **UIDs** (πρέπει να αντικατασταθούν για να σπάσει η σύνδεση με την αρχική εξέταση).\n",
    ">\n",
    "> Για παραγωγική χρήση, δείτε εργαλεία όπως **dicognito**, **CTP** (DICOM Anonymizer), **DicomAnonymizerTool** — που εφαρμόζουν τις προδιαγραφές του DICOM standard για de-identification (PS3.15, Annex E).\n",
    "\n",
    "#### 2. `get_image_orientation` — αξιακή/στεφανιαία/οβελιαία\n",
    "Διαβάζει το `ImageOrientationPatient` (6 αριθμοί: row vector + column vector), υπολογίζει το **κάθετο διάνυσμα** (cross product), και επιστρέφει τον προσανατολισμό:\n",
    "\n",
    "| Dominant axis | Προβολή |\n",
    "|---------------|---------|\n",
    "| X | Sagittal |\n",
    "| Y | Coronal |\n",
    "| Z | Axial |\n",
    "\n",
    "> 📌 Αυτή είναι **απλοποίηση**. Σε λοξές (oblique) ακολουθίες, η εικόνα δεν είναι τέλεια ευθυγραμμισμένη με τους ανατομικούς άξονες — απλά παίρνουμε τον «κυρίαρχο» άξονα. Για ακριβή υπολογισμό χωρικού μετασχηματισμού, χρησιμοποιήστε SimpleITK.\n",
    "\n",
    "#### 3. `calculate_slice_spacing` — απόσταση μεταξύ τομών\n",
    "Υπολογίζει mean/std/min/max του spacing ταξινομώντας τις τομές κατά **z-coordinate**.\n",
    "\n",
    "Γιατί std/min/max; Για να εντοπίσουμε **uneven spacing** — δηλαδή σειρές με «κενά» τομές που λείπουν.\n",
    "\n",
    "#### 4. `quick_view` — γρήγορη οπτικοποίηση\n",
    "Για να μην ξαναγράφετε `plt.imshow(...)` δέκα φορές την ώρα.\n",
    "\n",
    "> 💡 **Παιδαγωγικό σημείο:** Ένα **utility module** σαν αυτό είναι από τα πρώτα πράγματα που χτίζει ένας έμπειρος προγραμματιστής όταν αρχίζει project. Δικές σας παραλλαγές που μπορείτε να γράψετε:\n",
    "> - `dicom_diff(dcm1, dcm2)` — εμφανίζει διαφορές tags.\n",
    "> - `validate_dicom(dcm)` — ελέγχει αν περιέχει όλα τα απαραίτητα tags για ML.\n",
    "> - `bulk_summary(folder)` — γρήγορη σύνοψη όλων των DICOMs σε φάκελο.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "279b076e",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                 UTILITY FUNCTIONS LIBRARY                      ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "class DICOMUtilities:\n",
    "    \"\"\"\n",
    "    Collection of utility functions for DICOM processing\n",
    "    \"\"\"\n",
    "    \n",
    "    @staticmethod\n",
    "    def anonymize_dicom(dicom_obj):\n",
    "        \"\"\"Remove patient identifiable information\"\"\"\n",
    "        tags_to_anonymize = [\n",
    "            'PatientName', 'PatientID', 'PatientBirthDate',\n",
    "            'PatientSex', 'PatientAge', 'PatientAddress'\n",
    "        ]\n",
    "        \n",
    "        for tag in tags_to_anonymize:\n",
    "            if hasattr(dicom_obj, tag):\n",
    "                setattr(dicom_obj, tag, 'ANONYMIZED')\n",
    "        \n",
    "        print(\"✓ Patient information anonymized\")\n",
    "        return dicom_obj\n",
    "    \n",
    "    @staticmethod\n",
    "    def get_image_orientation(dicom_obj):\n",
    "        \"\"\"Determine image orientation (axial, coronal, sagittal)\"\"\"\n",
    "        if not hasattr(dicom_obj, 'ImageOrientationPatient'):\n",
    "            return \"Unknown\"\n",
    "        \n",
    "        iop = dicom_obj.ImageOrientationPatient\n",
    "        \n",
    "        # This is a simplified determination\n",
    "        row_vec = np.array(iop[:3])\n",
    "        col_vec = np.array(iop[3:])\n",
    "        normal_vec = np.cross(row_vec, col_vec)\n",
    "        \n",
    "        # Find dominant direction\n",
    "        abs_normal = np.abs(normal_vec)\n",
    "        dominant = np.argmax(abs_normal)\n",
    "        \n",
    "        orientations = ['Sagittal', 'Coronal', 'Axial']\n",
    "        return orientations[dominant]\n",
    "    \n",
    "    @staticmethod\n",
    "    def calculate_slice_spacing(dicom_series):\n",
    "        \"\"\"Calculate spacing between slices\"\"\"\n",
    "        if len(dicom_series) < 2:\n",
    "            return None\n",
    "        \n",
    "        positions = []\n",
    "        for dcm in dicom_series:\n",
    "            if hasattr(dcm, 'ImagePositionPatient'):\n",
    "                positions.append(dcm.ImagePositionPatient[2])  # Z coordinate\n",
    "        \n",
    "        if len(positions) < 2:\n",
    "            return None\n",
    "        \n",
    "        positions.sort()\n",
    "        spacings = np.diff(positions)\n",
    "        \n",
    "        return {\n",
    "            'mean_spacing': np.mean(spacings),\n",
    "            'std_spacing': np.std(spacings),\n",
    "            'min_spacing': np.min(spacings),\n",
    "            'max_spacing': np.max(spacings)\n",
    "        }\n",
    "    \n",
    "    @staticmethod\n",
    "    def quick_view(dicom_obj, title=\"\"):\n",
    "        \"\"\"Quick visualization of DICOM image\"\"\"\n",
    "        plt.figure(figsize=(8, 8))\n",
    "        plt.imshow(dicom_obj.pixel_array, cmap='gray')\n",
    "        plt.title(f\"{title}\\n{dicom_obj.get('Modality', 'Unknown')} \"\n",
    "                  f\"{dicom_obj.Rows}x{dicom_obj.Columns}\")\n",
    "        plt.colorbar()\n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "\n",
    "# Demonstrate utilities\n",
    "utils = DICOMUtilities()\n",
    "\n",
    "print(\"\\n🔧 Demonstrating utility functions:\")\n",
    "print(f\"Image Orientation: {utils.get_image_orientation(dicom_data)}\")\n",
    "\n",
    "utils.quick_view(dicom_data, \"Quick View Demo\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "64c95e26",
   "metadata": {},
   "source": [
    "## Ενότητα 22 — Συνηθισμένες Παγίδες & Καλές Πρακτικές\n",
    "\n",
    "### Defensive programming — η νοοτροπία\n",
    "\n",
    "Ο κώδικας ιατρικής εικόνας **πρέπει** να είναι ανθεκτικός. Σε κλινικά δεδομένα, θα συναντήσετε:\n",
    "- Tags που λείπουν.\n",
    "- Λανθασμένες ή κενές τιμές.\n",
    "- Παλιά αρχεία με «παράξενα» VRs.\n",
    "- Ιδιαιτερότητες κατασκευαστών (Siemens vs GE vs Philips).\n",
    "\n",
    "Ο κώδικας πρέπει να **προβλέπει** όλα αυτά, όχι να κρασάρει στην πρώτη παρατυπία.\n",
    "\n",
    "### Οι πέντε κορυφαίες παγίδες\n",
    "\n",
    "#### Παγίδα 1: Πρόσβαση χωρίς έλεγχο\n",
    "```python\n",
    "# ❌ ΚΑΚΟ\n",
    "name = dicom_obj.PatientName  # Κρασάρει αν λείπει\n",
    "\n",
    "# ✅ ΚΑΛΟ\n",
    "name = dicom_obj.get('PatientName', 'Unknown')\n",
    "```\n",
    "\n",
    "#### Παγίδα 2: Pixel αντί για mm\n",
    "```python\n",
    "# ❌ ΚΑΚΟ\n",
    "print(f\"Tumor diameter: {tumor_pixels} pixels\")\n",
    "\n",
    "# ✅ ΚΑΛΟ\n",
    "spacing_mm = dicom_obj.PixelSpacing[0]\n",
    "print(f\"Tumor diameter: {tumor_pixels * spacing_mm:.1f} mm\")\n",
    "```\n",
    "\n",
    "#### Παγίδα 3: CT χωρίς HU conversion\n",
    "```python\n",
    "# ❌ ΚΑΚΟ — αυτό το νούμερο δεν έχει νόημα\n",
    "mean_intensity = dicom_obj.pixel_array.mean()\n",
    "\n",
    "# ✅ ΚΑΛΟ\n",
    "hu = dicom_obj.pixel_array * dicom_obj.RescaleSlope + dicom_obj.RescaleIntercept\n",
    "mean_hu = hu.mean()  # τώρα έχει φυσικό νόημα\n",
    "```\n",
    "\n",
    "#### Παγίδα 4: Υπόθεση ότι όλα τα tags υπάρχουν\n",
    "```python\n",
    "# ❌ ΚΑΚΟ\n",
    "thickness = dicom_obj.SliceThickness\n",
    "\n",
    "# ✅ ΚΑΛΟ\n",
    "if hasattr(dicom_obj, 'SliceThickness'):\n",
    "    thickness = dicom_obj.SliceThickness\n",
    "else:\n",
    "    thickness = None  # ή υπολογίστε από ImagePositionPatient\n",
    "```\n",
    "\n",
    "#### Παγίδα 5: Μη ταξινομημένη σειρά\n",
    "```python\n",
    "# ❌ ΚΑΚΟ — τυχαία σειρά\n",
    "volume = np.stack([pydicom.dcmread(f).pixel_array for f in os.listdir(folder)])\n",
    "\n",
    "# ✅ ΚΑΛΟ\n",
    "files = [pydicom.dcmread(f) for f in folder]\n",
    "files.sort(key=lambda d: float(d.ImagePositionPatient[2]))\n",
    "volume = np.stack([f.pixel_array for f in files])\n",
    "```\n",
    "\n",
    "### Επτά καλές πρακτικές\n",
    "\n",
    "1. **Επικυρώνετε τα DICOM πριν την επεξεργασία** — έλεγχος `Modality`, διαστάσεων, απαραίτητων tags.\n",
    "2. **Χρησιμοποιείτε try-except** για robustness — ειδικά σε batch processing.\n",
    "3. **Παρακολουθείτε coordinate systems** — pixel coords vs patient coords vs world coords.\n",
    "4. **Καταγράφετε τα window/level settings** στις παρουσιάσεις.\n",
    "5. **Ποτέ μην τροποποιείτε τα original** — δουλέψτε σε copies.\n",
    "6. **Ανωνυμοποιείτε πριν τη διανομή** — και θεωρήστε ότι κάθε file leak είναι πρόβλημα.\n",
    "7. **Ελέγχετε modality** πριν εφαρμόσετε modality-specific operations (HU σε CT, T1/T2 σε MRI...).\n",
    "\n",
    "> 🎯 **Η μάντρα του developer ιατρικής εικόνας:** «Αν μπορεί να πάει στραβά, **θα** πάει στραβά σε κάποιον ασθενή κάποια στιγμή. Γράψε τον κώδικα προβλέποντάς το.»\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3d92408c",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              COMMON PITFALLS AND TIPS                          ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "⚠️ COMMON PITFALLS:\n",
    "\n",
    "1. Not checking if tags exist before accessing\n",
    "   ❌ value = dicom_obj.PatientName  # May crash\n",
    "   ✅ value = dicom_obj.get('PatientName', 'N/A')\n",
    "\n",
    "2. Ignoring pixel spacing for measurements\n",
    "   ❌ distance = 100 pixels\n",
    "   ✅ distance = 100 pixels * pixel_spacing mm\n",
    "\n",
    "3. Not converting to Hounsfield Units for CT\n",
    "   ❌ Using raw pixel values for CT analysis\n",
    "   ✅ Apply RescaleSlope and RescaleIntercept\n",
    "\n",
    "4. Assuming all tags are present\n",
    "   ❌ Using attributes directly\n",
    "   ✅ Check with hasattr() or use get()\n",
    "\n",
    "5. Not sorting slices in a series\n",
    "   ❌ Processing slices in random order\n",
    "   ✅ Sort by InstanceNumber or ImagePositionPatient\n",
    "\n",
    "💡 BEST PRACTICES:\n",
    "\n",
    "1. Always validate DICOM files before processing\n",
    "2. Use try-except blocks for robust code\n",
    "3. Keep track of coordinate systems\n",
    "4. Document your window/level settings\n",
    "5. Preserve original data - work on copies\n",
    "6. Anonymize data when sharing\n",
    "7. Check modality before applying modality-specific processing\n",
    "\"\"\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "432a941f",
   "metadata": {},
   "source": [
    "## Ενότητα 23 — Πρόσθετοι Πόροι\n",
    "\n",
    "### Επίσημη τεκμηρίωση\n",
    "\n",
    "- **PyDICOM**: https://pydicom.github.io/ — αναφορά API, παραδείγματα, FAQ.\n",
    "- **DICOM Standard**: https://www.dicomstandard.org/ — η «βίβλος». Μεγάλη, αλλά πλήρης.\n",
    "- **DICOM Innolitics Browser**: https://dicom.innolitics.com — ο πιο φιλικός τρόπος εξερεύνησης του προτύπου.\n",
    "\n",
    "### Δημόσια datasets\n",
    "\n",
    "- **The Cancer Imaging Archive (TCIA)** — https://www.cancerimagingarchive.net/\n",
    "- **Medical Segmentation Decathlon** — http://medicaldecathlon.com/\n",
    "- **Grand Challenge** — https://grand-challenge.org/ (datasets από διαγωνισμούς)\n",
    "- **ADNI** (Alzheimer's) — https://adni.loni.usc.edu/\n",
    "- **OASIS** (νευροαπεικόνιση) — https://www.oasis-brains.org/\n",
    "- **MIMIC-CXR** (chest X-rays + reports) — https://physionet.org/\n",
    "\n",
    "### Βιβλιοθήκες πέρα από PyDICOM\n",
    "\n",
    "| Βιβλιοθήκη | Σε τι διαφέρει | Πότε προτιμάται |\n",
    "|------------|------------------|------------------|\n",
    "| **SimpleITK** | Πιο ώριμη για 3D processing, registration, segmentation | Παραγωγικά pipelines |\n",
    "| **nibabel** | Διαβάζει NIfTI (κοινό format σε νευροαπεικόνιση) | fMRI, BIDS datasets |\n",
    "| **scikit-image** | Γενική εικόνα-processing | Filtering, segmentation, features |\n",
    "| **MONAI** | Deep learning για ιατρικές εικόνες (PyTorch-based) | Modern ML projects |\n",
    "| **TorchIO** | Augmentations για ιατρικά volumes | Training data pipelines |\n",
    "| **3D Slicer** | Λογισμικό-εργαστήριο με Python API | Διαδραστική ανάλυση |\n",
    "\n",
    "### Μάθηση & βίντεο\n",
    "\n",
    "- **PyDICOM tutorials** στο GitHub.\n",
    "- **MONAI bootcamp materials** (YouTube).\n",
    "- **Coursera: AI for Medicine Specialization** (deeplearning.ai).\n",
    "- **edX: Medical Image Analysis** (διαφορετικά πανεπιστήμια).\n",
    "\n",
    "### Κλινικά πρότυπα\n",
    "\n",
    "- **DICOM PS3.15** (Annex E): de-identification.\n",
    "- **DICOM PS3.10**: file format.\n",
    "- **IHE Profiles**: συμβατότητα ροών εργασίας.\n",
    "\n",
    "> 💡 **Συμβουλή για συνεχιζόμενη ενημέρωση:** Ακολουθήστε στα social media: **@PyDICOM**, **@MONAI_io**, και τους ερευνητές που δουλεύουν στο πεδίο σας. Το πεδίο εξελίσσεται γρήγορα, ειδικά με την έλευση των foundation models στην ιατρική εικόνα.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26cd82a2",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                  ADDITIONAL RESOURCES                          ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "📚 DOCUMENTATION:\n",
    "   - PyDICOM: https://pydicom.github.io/\n",
    "   - DICOM Standard: https://www.dicomstandard.org/\n",
    "\n",
    "📊 PUBLIC DATASETS:\n",
    "   - The Cancer Imaging Archive (TCIA): https://www.cancerimagingarchive.net/\n",
    "   - Medical Segmentation Decathlon: http://medicaldecathlon.com/\n",
    "\n",
    "🛠️ USEFUL LIBRARIES:\n",
    "   - SimpleITK: Advanced medical image processing\n",
    "   - nibabel: Working with neuroimaging formats\n",
    "   - scikit-image: General image processing\n",
    "   - 3D Slicer: Visualization and analysis platform\n",
    "\n",
    "📖 LEARNING:\n",
    "   - DICOM Tutorial: https://www.dicomstandard.org/\n",
    "   - Medical Imaging courses on Coursera, edX\n",
    "   - PyDICOM documentation and examples\n",
    "\"\"\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c18d414",
   "metadata": {},
   "source": [
    "## Ενότητα 24 — Σύνοψη Μέρους Α & Επόμενα Βήματα\n",
    "\n",
    "### Τι μάθατε σε αυτό το πρώτο μέρος\n",
    "\n",
    "Στις Ενότητες 1-23 καλύψαμε όλα τα **απαραίτητα** για να ξεκινήσετε να εργάζεστε με DICOM:\n",
    "\n",
    "**Έννοιες**\n",
    "- ✓ Τι είναι το DICOM και γιατί είναι σημαντικό\n",
    "- ✓ Η ιεραρχία Patient → Study → Series → Image\n",
    "- ✓ Τι είναι τα tags, VRs, modules\n",
    "- ✓ Hounsfield Units και η φυσική σημασία τους\n",
    "- ✓ Pixel spacing και πραγματικές μετρήσεις\n",
    "\n",
    "**Δεξιότητες**\n",
    "- ✓ Φόρτωση και ανάγνωση DICOM με `pydicom.dcmread`\n",
    "- ✓ Πρόσβαση σε tags με τέσσερις διαφορετικούς τρόπους\n",
    "- ✓ Οπτικοποίηση εικόνων με `matplotlib`\n",
    "- ✓ Windowing για βελτίωση αντίθεσης\n",
    "- ✓ Φόρτωση και πλοήγηση σειρών\n",
    "- ✓ Δημιουργία 3D όγκων και ανατομικών προβολών (axial/coronal/sagittal)\n",
    "- ✓ Στοιχειώδης image processing\n",
    "- ✓ Αποθήκευση σε διάφορες μορφές\n",
    "- ✓ Defensive programming για κλινικά δεδομένα\n",
    "\n",
    "### Τι έρχεται στο Μέρος Β (Ενότητες 26-36)\n",
    "\n",
    "Στο επόμενο μέρος προχωράμε σε **βάθος ανάλυσης**:\n",
    "\n",
    "- **Στατιστική ανάλυση** ιστογραμμάτων (mean, median, skewness, kurtosis)\n",
    "- **Συγκριτική οπτικοποίηση** (box plots, violin plots, Q-Q plots)\n",
    "- **Χωρική ανάλυση** ανά περιοχή\n",
    "- **Προφίλ έντασης** και ανίχνευση κλίσεων\n",
    "- **Histogram equalization** για βελτίωση αντίθεσης\n",
    "- **Ποσοτικοποίηση αντίθεσης/φωτεινότητας**\n",
    "- **Εκτίμηση και αξιολόγηση θορύβου** (SNR)\n",
    "- **Σύνταξη ολοκληρωμένης αναφοράς ποιότητας**\n",
    "\n",
    "### Πώς να συνεχίσετε εκτός μαθήματος\n",
    "\n",
    "> 🎯 **1ο βήμα — Εξάσκηση με πραγματικά δεδομένα.** Κατεβάστε ένα μικρό dataset από TCIA (π.χ. **LIDC-IDRI** για lung CT). Εφαρμόστε όλο το pipeline που μάθατε.\n",
    "\n",
    "> 🎯 **2ο βήμα — Επιλέξτε modality εξειδίκευσης.** CT για ογκολογία, MRI για νευρολογία/μυοσκελετικό, X-ray για cardio-pulmonary, US για μαιευτική. Κάθε ένα έχει τις δικές του ιδιαιτερότητες.\n",
    "\n",
    "> 🎯 **3ο βήμα — Πραγματικό project.** Διαλέξτε μια ερευνητική ή κλινική ερώτηση. Γράψτε ολόκληρο pipeline από φόρτωση μέχρι αναφορά. **Αυτό** είναι που μένει στη μνήμη — όχι τα notebooks.\n",
    "\n",
    "> 🎯 **4ο βήμα — Συμβολή σε open source.** Το PyDICOM, το MONAI, το SimpleITK δέχονται contributions. Είναι ο καλύτερος τρόπος να βελτιωθείτε ως developer ιατρικής εικόνας.\n",
    "\n",
    "### Πρόκληση τέλους\n",
    "\n",
    "Φτιάξτε **τον δικό σας ολοκληρωμένο DICOM viewer** που να συνδυάζει τα παρακάτω:\n",
    "\n",
    "- Πλοήγηση πολλαπλών τομών (slider)\n",
    "- Διαδραστική ρύθμιση window/level\n",
    "- Εργαλεία μέτρησης (ευθεία γραμμή σε mm)\n",
    "- Απλή annotation\n",
    "- Εξαγωγή σε PNG/PDF report\n",
    "\n",
    "Θα μάθετε **πραγματικά** πολλά από αυτή την άσκηση.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7e0ed23c",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                  SUMMARY & NEXT STEPS                          ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "🎓 WHAT YOU'VE LEARNED:\n",
    "\n",
    "✓ What DICOM files are and why they're important\n",
    "✓ How to load and read DICOM files with PyDICOM\n",
    "✓ Understanding and accessing DICOM tags\n",
    "✓ Visualizing medical images\n",
    "✓ Working with Hounsfield Units (CT)\n",
    "✓ Applying window/level adjustments\n",
    "✓ Loading and navigating image series\n",
    "✓ Creating 3D volumes from slices\n",
    "✓ Basic image processing operations\n",
    "✓ Saving and exporting DICOM data\n",
    "\n",
    "🚀 NEXT STEPS:\n",
    "\n",
    "1. Practice with different modalities (CT, MRI, X-ray)\n",
    "2. Explore advanced segmentation techniques\n",
    "3. Learn 3D visualization methods\n",
    "4. Study image registration and fusion\n",
    "5. Implement machine learning pipelines\n",
    "6. Work with real clinical datasets\n",
    "7. Build your own analysis tools\n",
    "\n",
    "💪 CHALLENGE YOURSELF:\n",
    "   Try to build a complete DICOM viewer with:\n",
    "   - Multi-slice navigation\n",
    "   - Interactive window/level adjustment\n",
    "   - Measurement tools\n",
    "   - Annotation capabilities\n",
    "   - Export functionality\n",
    "\n",
    "Good luck with your medical image analysis journey! 🏥\n",
    "\"\"\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d1e1edc6",
   "metadata": {},
   "source": [
    "## Ενότητα 25 — Τελική Επίδειξη: Dashboard Σύνοψης\n",
    "\n",
    "### Συνθέτοντας όλα όσα μάθαμε\n",
    "\n",
    "Σε αυτή την τελευταία ενότητα του Μέρους Α, η συνάρτηση `create_comprehensive_summary` δημιουργεί ένα **dashboard** που συνδυάζει σχεδόν **όλες** τις τεχνικές των προηγούμενων ενοτήτων σε ένα σχήμα.\n",
    "\n",
    "### Τι περιέχει το dashboard\n",
    "\n",
    "| Στοιχείο | Σχετική ενότητα | Τι μας λέει |\n",
    "|----------|------------------|--------------|\n",
    "| **Κύρια εικόνα** | 10 | Η ίδια η DICOM τομή με titlebar |\n",
    "| **Ιστόγραμμα έντασης** | 17 | Κατανομή τιμών pixel |\n",
    "| **Πίνακας στατιστικών** | 9, 17 | Mean, median, std, min, max |\n",
    "| **Πίνακας metadata** | 5, 6, 8 | Modality, ημερομηνία |\n",
    "| **Προφίλ έντασης (μεσαία γραμμή)** | (πρόδρομος του 30) | Πώς αλλάζει η ένταση |\n",
    "| **Downsampled view** | 17 | Compressed visualization |\n",
    "\n",
    "### Παρατηρήστε τη χρήση `gridspec`\n",
    "\n",
    "```python\n",
    "fig = plt.figure(figsize=(16, 12))\n",
    "gs = fig.add_gridspec(3, 3, hspace=0.3, wspace=0.3)\n",
    "```\n",
    "\n",
    "Αντί για `subplots(3, 3)` που δίνει **ισομεγέθη** κουτάκια, το `gridspec` σας επιτρέπει **διαφορετικά μεγέθη** για κάθε panel. Στο παράδειγμα:\n",
    "\n",
    "- `gs[0:2, 0:2]` → η κύρια εικόνα καταλαμβάνει 2×2 = 4 cells.\n",
    "- `gs[0, 2]` → το ιστόγραμμα μόνο 1 cell.\n",
    "- `gs[2, :]` → το προφίλ καταλαμβάνει όλη την κάτω γραμμή.\n",
    "\n",
    "> 💡 **Όταν η σχεδίαση μετρά:** Οι ιατρικές αναφορές πρέπει να είναι **εύληπτες με μια ματιά**. Το gridspec είναι το εργαλείο για επαγγελματικά layouts.\n",
    "\n",
    "### Από εδώ προχωράμε στο Μέρος Β\n",
    "\n",
    "Τώρα ξέρετε **πώς να φτάσετε στα pixel και στα metadata**. Στις Ενότητες 26-36 θα μάθετε **πώς να τα αναλύσετε σε βάθος** — με στατιστικές, ιστογράμματα, ποιοτικές μετρικές, και αναφορές παραγωγικού επιπέδου.\n",
    "\n",
    "> 🎓 **Στο Μέρος Β θα συνεχίσουμε να χρησιμοποιούμε τη μεταβλητή `dicom_data`** που φορτώθηκε στην Ενότητα 4. Αν τρέξατε όλα τα κελιά μέχρι εδώ, είστε έτοιμοι. Αν επανεκκινήσετε τον kernel, θα χρειαστεί να ξανατρέξετε τουλάχιστον τις Ενότητες 1, 3 και 4.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "70fc46aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              INTERACTIVE DEMO COMPLETE!                        ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "You now have a complete toolkit for working with DICOM images!\n",
    "\n",
    "Try modifying the code above to:\n",
    "- Load your own DICOM files\n",
    "- Create custom visualizations\n",
    "- Implement new analysis functions\n",
    "- Build your own viewer\n",
    "\n",
    "Remember: Practice makes perfect! 💻\n",
    "\"\"\")\n",
    "\n",
    "# Create a final comprehensive visualization\n",
    "def create_comprehensive_summary(dicom_obj):\n",
    "    \"\"\"\n",
    "    Create a comprehensive visualization summarizing all learned concepts\n",
    "    \"\"\"\n",
    "    \n",
    "    fig = plt.figure(figsize=(16, 12))\n",
    "    gs = fig.add_gridspec(3, 3, hspace=0.3, wspace=0.3)\n",
    "    \n",
    "    # Main image\n",
    "    ax1 = fig.add_subplot(gs[0:2, 0:2])\n",
    "    ax1.imshow(dicom_obj.pixel_array, cmap='gray')\n",
    "    ax1.set_title(f'DICOM Image: {dicom_obj.Modality}\\n'\n",
    "                  f'{dicom_obj.Rows}x{dicom_obj.Columns} pixels', \n",
    "                  fontsize=14, fontweight='bold')\n",
    "    ax1.axis('off')\n",
    "    \n",
    "    # Histogram\n",
    "    ax2 = fig.add_subplot(gs[0, 2])\n",
    "    ax2.hist(dicom_obj.pixel_array.ravel(), bins=50, color='steelblue', alpha=0.7)\n",
    "    ax2.set_title('Intensity Histogram', fontsize=10)\n",
    "    ax2.set_xlabel('Pixel Value')\n",
    "    ax2.set_ylabel('Frequency')\n",
    "    ax2.grid(True, alpha=0.3)\n",
    "    \n",
    "    # Statistics\n",
    "    ax3 = fig.add_subplot(gs[1, 2])\n",
    "    ax3.axis('off')\n",
    "    stats_text = f\"\"\"\n",
    "    IMAGE STATISTICS\n",
    "    ───────────────\n",
    "    Mean: {dicom_obj.pixel_array.mean():.1f}\n",
    "    Median: {np.median(dicom_obj.pixel_array):.1f}\n",
    "    Std Dev: {dicom_obj.pixel_array.std():.1f}\n",
    "    Min: {dicom_obj.pixel_array.min()}\n",
    "    Max: {dicom_obj.pixel_array.max()}\n",
    "    \n",
    "    METADATA\n",
    "    ───────────────\n",
    "    Modality: {dicom_obj.get('Modality', 'N/A')}\n",
    "    Date: {dicom_obj.get('StudyDate', 'N/A')}\n",
    "    \"\"\"\n",
    "    ax3.text(0.1, 0.5, stats_text, fontsize=9, family='monospace',\n",
    "             verticalalignment='center')\n",
    "    \n",
    "    # Profile plot\n",
    "    ax4 = fig.add_subplot(gs[2, :2])\n",
    "    mid_row = dicom_obj.Rows // 2\n",
    "    profile = dicom_obj.pixel_array[mid_row, :]\n",
    "    ax4.plot(profile, color='darkred', linewidth=2)\n",
    "    ax4.set_title(f'Intensity Profile (Row {mid_row})', fontsize=10)\n",
    "    ax4.set_xlabel('Column')\n",
    "    ax4.set_ylabel('Intensity')\n",
    "    ax4.grid(True, alpha=0.3)\n",
    "    \n",
    "    # 2D histogram\n",
    "    ax5 = fig.add_subplot(gs[2, 2])\n",
    "    # Create a simple 2D view of pixel correlations\n",
    "    small_img = dicom_obj.pixel_array[::10, ::10]  # Downsample for speed\n",
    "    ax5.imshow(small_img, cmap='viridis', aspect='auto')\n",
    "    ax5.set_title('Downsampled View', fontsize=10)\n",
    "    ax5.axis('off')\n",
    "    \n",
    "    plt.suptitle('DICOM Analysis Summary Dashboard', \n",
    "                 fontsize=16, fontweight='bold', y=0.98)\n",
    "    \n",
    "    plt.show()\n",
    "\n",
    "# Create final summary\n",
    "print(\"\\n\" + \"=\"*70)\n",
    "print(\"CREATING COMPREHENSIVE SUMMARY VISUALIZATION...\")\n",
    "print(\"=\"*70)\n",
    "create_comprehensive_summary(dicom_data)\n",
    "\n",
    "print(\"\\n🎉 Tutorial Complete! You're now ready to work with DICOM images! 🎉\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e9ba019",
   "metadata": {},
   "source": [
    "## Ενότητα 26 — Κατανόηση Ιστογραμμάτων Εικόνας\n",
    "\n",
    "### Τι είναι μια ιατρική εικόνα στον υπολογιστή;\n",
    "\n",
    "Πριν μιλήσουμε για ιστογράμματα, ας ξεκαθαρίσουμε τι «βλέπει» πραγματικά ο υπολογιστής. Μια εικόνα DICOM (CT, MRI, mammography κ.λπ.) είναι **ένας πίνακας αριθμών**. Κάθε θέση του πίνακα αντιστοιχεί σε ένα **pixel** (εικονοστοιχείο) και ο αριθμός εκεί λέγεται **ένταση** (intensity). Στην ακτινολογία, αυτή η ένταση συνδέεται με φυσικά μεγέθη — π.χ. στο CT αντιστοιχεί σε **Hounsfield Units** (πυκνότητα ιστού), στο MRI εξαρτάται από το είδος της ακολουθίας (T1, T2, DWI κ.λπ.).\n",
    "\n",
    "Άρα όταν λέμε «ανάλυση εικόνας», στην πράξη κάνουμε στατιστική σε εκατομμύρια αριθμούς.\n",
    "\n",
    "### Τι είναι το ιστόγραμμα;\n",
    "\n",
    "Το **ιστόγραμμα** είναι ένα διάγραμμα που δείχνει **πόσα pixel έχουν την κάθε τιμή έντασης**. Δηλαδή:\n",
    "\n",
    "- Στον **οριζόντιο άξονα** (x): οι τιμές έντασης (από τη μικρότερη στη μεγαλύτερη).\n",
    "- Στον **κατακόρυφο άξονα** (y): πόσες φορές εμφανίζεται η κάθε τιμή στην εικόνα.\n",
    "\n",
    "> 📌 **Διαισθητική αναλογία:** Φανταστείτε ότι ρίχνετε όλα τα pixel σε «κουτάκια» (bins) ανάλογα με την τιμή τους — π.χ. ένα κουτί για τιμές 0–10, ένα για 10–20, κ.ο.κ. Το ιστόγραμμα είναι απλά το ύψος αυτών των κουτιών.\n",
    "\n",
    "### Γιατί έχει σημασία στην ιατρική;\n",
    "\n",
    "Διαφορετικοί ιστοί έχουν διαφορετικές εντάσεις. Σε ένα CT θώρακος, για παράδειγμα, ο **αέρας** δίνει πολύ χαμηλές τιμές, το **μαλακό μόριο** μεσαίες, και το **οστό** πολύ υψηλές. Όταν δούμε ένα ιστόγραμμα με δύο ή τρεις «κορυφές» (peaks/modes), αυτό μας λέει ότι υπάρχουν στην εικόνα διακριτοί ιστοί — μια **πολυτροπική** (multimodal) κατανομή.\n",
    "\n",
    "### Τι θα δείτε στον κώδικα παρακάτω\n",
    "\n",
    "Η συνάρτηση `comprehensive_histogram_analysis` θα παρουσιάσει **έξι διαφορετικές αναπαραστάσεις** του ίδιου ιστογράμματος. Καθεμιά απαντά σε διαφορετική ερώτηση:\n",
    "\n",
    "1. **Βασικό ιστόγραμμα** — Πώς κατανέμονται γενικά οι εντάσεις;\n",
    "2. **Ιστόγραμμα με στατιστικές γραμμές** — Πού πέφτει η μέση τιμή, η διάμεσος, και το ±1 τυπική απόκλιση;\n",
    "3. **Αθροιστικό ιστόγραμμα (CDF)** — Τι ποσοστό των pixel βρίσκεται κάτω από κάθε τιμή; (Βασικό εργαλείο για windowing/thresholding.)\n",
    "4. **Ιστόγραμμα σε λογαριθμική κλίμακα** — Αν λίγα pixel έχουν ακραίες τιμές, η γραμμική κλίμακα τα «κρύβει». Ο λογάριθμος τα φέρνει στην επιφάνεια.\n",
    "5. **Κανονικοποιημένο ιστόγραμμα (PDF)** — Αντί για πλήθος, βλέπουμε **πιθανότητα**. Έτσι ιστογράμματα από εικόνες διαφορετικού μεγέθους γίνονται συγκρίσιμα.\n",
    "6. **Ιστόγραμμα με εκατοστημόρια** — Πού πέφτουν τα 1ο, 5ο, 25ο, 50ο, 75ο, 95ο, 99ο εκατοστημόριο. Αυτό μας βοηθά να εντοπίσουμε ακραίες τιμές (outliers) και να επιλέξουμε σωστό window/level για την προβολή.\n",
    "\n",
    "> ⚠️ **Σημαντικό:** Ο αριθμός των bins (εδώ 50) επηρεάζει την εμφάνιση. Λίγα bins → πολύ «λεία» εικόνα, χάνουμε λεπτομέρεια. Πολλά bins → θορυβώδες ιστόγραμμα. Στις ασκήσεις θα δείτε μόνοι σας πώς αλλάζει η ερμηνεία ανάλογα με την επιλογή.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "89c30cb0",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║            UNDERSTANDING IMAGE HISTOGRAMS                      ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "A histogram shows the distribution of pixel intensities in an image.\n",
    "- X-axis: Pixel intensity values\n",
    "- Y-axis: Number of pixels with that intensity\n",
    "\n",
    "Why histograms matter:\n",
    "✓ Understanding image contrast\n",
    "✓ Detecting image quality issues\n",
    "✓ Choosing appropriate window/level settings\n",
    "✓ Identifying different tissue types\n",
    "✓ Preprocessing decisions (normalization, etc.)\n",
    "\"\"\")\n",
    "\n",
    "def comprehensive_histogram_analysis(dicom_obj, title=\"\"):\n",
    "    \"\"\"\n",
    "    Create comprehensive histogram visualizations\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        DICOM object to analyze\n",
    "    title : str\n",
    "        Title for the analysis\n",
    "    \"\"\"\n",
    "    \n",
    "    pixel_array = dicom_obj.pixel_array.flatten()\n",
    "    \n",
    "    fig, axes = plt.subplots(2, 3, figsize=(18, 10))\n",
    "    \n",
    "    # 1. Basic Histogram\n",
    "    axes[0, 0].hist(pixel_array, bins=50, color='steelblue', alpha=0.7, edgecolor='black')\n",
    "    axes[0, 0].set_title('Basic Histogram', fontsize=12, fontweight='bold')\n",
    "    axes[0, 0].set_xlabel('Pixel Intensity')\n",
    "    axes[0, 0].set_ylabel('Frequency')\n",
    "    axes[0, 0].grid(True, alpha=0.3)\n",
    "    \n",
    "    # 2. Histogram with Statistics Lines\n",
    "    axes[0, 1].hist(pixel_array, bins=50, color='lightcoral', alpha=0.7, edgecolor='black')\n",
    "    mean_val = pixel_array.mean()\n",
    "    median_val = np.median(pixel_array)\n",
    "    std_val = pixel_array.std()\n",
    "    \n",
    "    axes[0, 1].axvline(mean_val, color='red', linestyle='--', linewidth=2, label=f'Mean: {mean_val:.1f}')\n",
    "    axes[0, 1].axvline(median_val, color='green', linestyle='--', linewidth=2, label=f'Median: {median_val:.1f}')\n",
    "    axes[0, 1].axvline(mean_val + std_val, color='orange', linestyle=':', linewidth=2, label=f'+1 SD: {mean_val+std_val:.1f}')\n",
    "    axes[0, 1].axvline(mean_val - std_val, color='orange', linestyle=':', linewidth=2, label=f'-1 SD: {mean_val-std_val:.1f}')\n",
    "    \n",
    "    axes[0, 1].set_title('Histogram with Statistics', fontsize=12, fontweight='bold')\n",
    "    axes[0, 1].set_xlabel('Pixel Intensity')\n",
    "    axes[0, 1].set_ylabel('Frequency')\n",
    "    axes[0, 1].legend(fontsize=9)\n",
    "    axes[0, 1].grid(True, alpha=0.3)\n",
    "    \n",
    "    # 3. Cumulative Histogram\n",
    "    counts, bins = np.histogram(pixel_array, bins=100)\n",
    "    cumulative = np.cumsum(counts)\n",
    "    axes[0, 2].plot(bins[:-1], cumulative, color='darkgreen', linewidth=2)\n",
    "    axes[0, 2].fill_between(bins[:-1], cumulative, alpha=0.3, color='lightgreen')\n",
    "    axes[0, 2].set_title('Cumulative Histogram', fontsize=12, fontweight='bold')\n",
    "    axes[0, 2].set_xlabel('Pixel Intensity')\n",
    "    axes[0, 2].set_ylabel('Cumulative Frequency')\n",
    "    axes[0, 2].grid(True, alpha=0.3)\n",
    "    \n",
    "    # 4. Log Scale Histogram\n",
    "    axes[1, 0].hist(pixel_array, bins=50, color='purple', alpha=0.7, edgecolor='black')\n",
    "    axes[1, 0].set_yscale('log')\n",
    "    axes[1, 0].set_title('Histogram (Log Scale)', fontsize=12, fontweight='bold')\n",
    "    axes[1, 0].set_xlabel('Pixel Intensity')\n",
    "    axes[1, 0].set_ylabel('Frequency (log scale)')\n",
    "    axes[1, 0].grid(True, alpha=0.3)\n",
    "    \n",
    "    # 5. Normalized Histogram (Probability Density)\n",
    "    axes[1, 1].hist(pixel_array, bins=50, density=True, color='teal', alpha=0.7, edgecolor='black')\n",
    "    axes[1, 1].set_title('Normalized Histogram (PDF)', fontsize=12, fontweight='bold')\n",
    "    axes[1, 1].set_xlabel('Pixel Intensity')\n",
    "    axes[1, 1].set_ylabel('Probability Density')\n",
    "    axes[1, 1].grid(True, alpha=0.3)\n",
    "    \n",
    "    # 6. Percentile Analysis\n",
    "    percentiles = [1, 5, 25, 50, 75, 95, 99]\n",
    "    percentile_values = np.percentile(pixel_array, percentiles)\n",
    "    \n",
    "    axes[1, 2].hist(pixel_array, bins=50, color='goldenrod', alpha=0.5, edgecolor='black')\n",
    "    for p, val in zip(percentiles, percentile_values):\n",
    "        axes[1, 2].axvline(val, color='red', linestyle='--', linewidth=1, alpha=0.7)\n",
    "        axes[1, 2].text(val, axes[1, 2].get_ylim()[1]*0.9, f'{p}%', \n",
    "                       rotation=90, fontsize=8, va='top')\n",
    "    \n",
    "    axes[1, 2].set_title('Histogram with Percentiles', fontsize=12, fontweight='bold')\n",
    "    axes[1, 2].set_xlabel('Pixel Intensity')\n",
    "    axes[1, 2].set_ylabel('Frequency')\n",
    "    axes[1, 2].grid(True, alpha=0.3)\n",
    "    \n",
    "    plt.suptitle(f'Comprehensive Histogram Analysis: {title}', \n",
    "                 fontsize=16, fontweight='bold', y=1.00)\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "# Demonstrate histogram analysis\n",
    "comprehensive_histogram_analysis(dicom_data, f\"{dicom_data.Modality} Image\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a5c30f0",
   "metadata": {},
   "source": [
    "## Ενότητα 27 — Αναλυτική Στατιστική Ανάλυση\n",
    "\n",
    "### Γιατί χρειαζόμαστε αριθμούς-σύνοψη;\n",
    "\n",
    "Μια εικόνα 512×512 έχει 262.144 pixel. Δεν μπορούμε να μιλάμε για κάθε ένα ξεχωριστά — χρειαζόμαστε **συνοπτικά μέτρα** που περιγράφουν την κατανομή σε λίγους αριθμούς. Αυτά τα μέτρα είναι το ABC της ανάλυσης: μια εικόνα μπορεί να συγκριθεί με μια άλλη, ένας ιστός με έναν άλλον, μια εξέταση με μια προηγούμενη.\n",
    "\n",
    "### Οι τέσσερις οικογένειες μέτρων\n",
    "\n",
    "#### 1. Μέτρα κεντρικής τάσης — *Πού «κάθεται» η κατανομή;*\n",
    "\n",
    "- **Μέση τιμή (mean, μ):** Το άθροισμα διά το πλήθος. Ευαίσθητη σε ακραίες τιμές.\n",
    "- **Διάμεσος (median):** Η μεσαία τιμή όταν τα pixel ταξινομηθούν. **Ανθεκτική** σε outliers — γι' αυτό προτιμάται όταν υπάρχει θόρυβος.\n",
    "- **Επικρατούσα τιμή (mode):** Η πιο συχνή τιμή. Σε ιατρικές εικόνες συχνά αντιστοιχεί στον κυρίαρχο ιστό ή στο φόντο.\n",
    "\n",
    "> 🧠 **Κανόνας του φοιτητή:** Αν η μέση τιμή και η διάμεσος διαφέρουν σημαντικά, η κατανομή σας **δεν είναι συμμετρική**.\n",
    "\n",
    "#### 2. Μέτρα διασποράς — *Πόσο «απλωμένη» είναι η κατανομή;*\n",
    "\n",
    "- **Τυπική απόκλιση (std, σ):** Πόσο μακριά, κατά μέσο όρο, βρίσκονται τα pixel από τη μέση τιμή. Σε ιατρικές εικόνες αντικατοπτρίζει ταυτόχρονα την **ετερογένεια ιστών** ΚΑΙ τον **θόρυβο**.\n",
    "- **Διακύμανση (variance, σ²):** Το τετράγωνο της τυπικής απόκλισης.\n",
    "- **Εύρος (range):** max − min. Πολύ ευαίσθητο — αρκεί ένα κακό pixel για να το «τινάξει στον αέρα».\n",
    "- **Ενδοτεταρτημοριακό εύρος (IQR):** Q3 − Q1. **Ανθεκτικό** μέτρο διασποράς, ισοδύναμο της διαμέσου.\n",
    "\n",
    "#### 3. Μέτρα σχήματος — *Πώς «μοιάζει» η κατανομή;*\n",
    "\n",
    "- **Ασυμμετρία (skewness):** Πόσο γέρνει η κατανομή προς τη μία πλευρά.\n",
    "  - skew ≈ 0 → συμμετρική (όπως κανονική κατανομή)\n",
    "  - skew > 0 → δεξιά ασυμμετρία (μακριά ουρά προς τα δεξιά, λιγοστά λαμπρά pixel)\n",
    "  - skew < 0 → αριστερή ασυμμετρία\n",
    "- **Κύρτωση (kurtosis):** Πόσο «μυτερή» ή «βαριά στις ουρές» είναι.\n",
    "  - kurtosis > 0 → πιο μυτερή κορυφή και βαρύτερες ουρές (περισσότερα ακραία pixel)\n",
    "  - kurtosis < 0 → πιο πλατιά κατανομή\n",
    "\n",
    "#### 4. Μέτρα ποιότητας\n",
    "\n",
    "- **Συντελεστής μεταβλητότητας (CV = σ/μ × 100%):** Σχετική διασπορά. Επιτρέπει σύγκριση μεταξύ εικόνων με διαφορετικές κλίμακες.\n",
    "- **SNR (Signal-to-Noise Ratio):** Στην απλή του μορφή, μ/σ. Όσο μεγαλύτερο, τόσο «καθαρότερη» η εικόνα.\n",
    "\n",
    "### Τι κάνει ο κώδικας\n",
    "\n",
    "Δύο συναρτήσεις:\n",
    "\n",
    "1. **`comprehensive_statistical_analysis`** — Υπολογίζει όλα τα παραπάνω σε ένα dictionary.\n",
    "2. **`display_statistics_report`** — Παρουσιάζει τα αποτελέσματα σε καλά μορφοποιημένο πίνακα και προσθέτει **αυτόματη ερμηνεία** (π.χ. «Right-skewed»).\n",
    "\n",
    "> 📌 Παρατηρήστε ότι η συνάρτηση επιστρέφει dictionary. Αυτή είναι καλή πρακτική: ο κώδικας που υπολογίζει διαχωρίζεται από τον κώδικα που εμφανίζει — μπορείτε να ξαναχρησιμοποιήσετε τα νούμερα σε άλλη ανάλυση χωρίς να ξανατρέξετε τα ίδια.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d230b296",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              DETAILED STATISTICAL ANALYSIS                     ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "Statistical measures help us understand:\n",
    "✓ Central tendency (mean, median, mode)\n",
    "✓ Spread/dispersion (standard deviation, variance, range)\n",
    "✓ Distribution shape (skewness, kurtosis)\n",
    "✓ Outliers and extreme values\n",
    "✓ Image quality and consistency\n",
    "\"\"\")\n",
    "\n",
    "def comprehensive_statistical_analysis(dicom_obj):\n",
    "    \"\"\"\n",
    "    Perform comprehensive statistical analysis on DICOM image\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        DICOM object to analyze\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    dict : Dictionary containing all statistical measures\n",
    "    \"\"\"\n",
    "    \n",
    "    pixel_array = dicom_obj.pixel_array.flatten()\n",
    "    \n",
    "    # Basic statistics\n",
    "    stats = {\n",
    "        'count': len(pixel_array),\n",
    "        'mean': np.mean(pixel_array),\n",
    "        'median': np.median(pixel_array),\n",
    "        'mode': float(np.bincount(pixel_array.astype(int)).argmax()) if pixel_array.max() < 10000 else 'N/A',\n",
    "        'std': np.std(pixel_array),\n",
    "        'variance': np.var(pixel_array),\n",
    "        'min': np.min(pixel_array),\n",
    "        'max': np.max(pixel_array),\n",
    "        'range': np.ptp(pixel_array),  # peak to peak (max - min)\n",
    "    }\n",
    "    \n",
    "    # Percentiles\n",
    "    percentiles = [1, 5, 10, 25, 50, 75, 90, 95, 99]\n",
    "    for p in percentiles:\n",
    "        stats[f'percentile_{p}'] = np.percentile(pixel_array, p)\n",
    "    \n",
    "    # Quartiles and IQR\n",
    "    q1 = np.percentile(pixel_array, 25)\n",
    "    q3 = np.percentile(pixel_array, 75)\n",
    "    stats['q1'] = q1\n",
    "    stats['q3'] = q3\n",
    "    stats['iqr'] = q3 - q1  # Interquartile range\n",
    "    \n",
    "    # Distribution shape\n",
    "    from scipy import stats as scipy_stats\n",
    "    stats['skewness'] = scipy_stats.skew(pixel_array)\n",
    "    stats['kurtosis'] = scipy_stats.kurtosis(pixel_array)\n",
    "    \n",
    "    # Coefficient of variation\n",
    "    stats['cv'] = (stats['std'] / stats['mean']) * 100 if stats['mean'] != 0 else 0\n",
    "    \n",
    "    # Signal to Noise Ratio (simplified)\n",
    "    stats['snr'] = stats['mean'] / stats['std'] if stats['std'] != 0 else 0\n",
    "    \n",
    "    return stats\n",
    "\n",
    "def display_statistics_report(stats_dict, dicom_obj):\n",
    "    \"\"\"\n",
    "    Display a formatted statistics report\n",
    "    \"\"\"\n",
    "    \n",
    "    print(\"\\n\" + \"=\"*70)\n",
    "    print(\"📊 COMPREHENSIVE STATISTICAL REPORT\")\n",
    "    print(\"=\"*70)\n",
    "    \n",
    "    print(f\"\\n{'IMAGE INFORMATION':^70}\")\n",
    "    print(\"-\"*70)\n",
    "    print(f\"{'Modality:':<30} {dicom_obj.Modality}\")\n",
    "    print(f\"{'Image Size:':<30} {dicom_obj.Rows} x {dicom_obj.Columns}\")\n",
    "    print(f\"{'Total Pixels:':<30} {stats_dict['count']:,}\")\n",
    "    \n",
    "    print(f\"\\n{'CENTRAL TENDENCY MEASURES':^70}\")\n",
    "    print(\"-\"*70)\n",
    "    print(f\"{'Mean:':<30} {stats_dict['mean']:>20.2f}\")\n",
    "    print(f\"{'Median:':<30} {stats_dict['median']:>20.2f}\")\n",
    "    print(f\"{'Mode:':<30} {stats_dict['mode']:>20}\")\n",
    "    \n",
    "    print(f\"\\n{'DISPERSION MEASURES':^70}\")\n",
    "    print(\"-\"*70)\n",
    "    print(f\"{'Standard Deviation:':<30} {stats_dict['std']:>20.2f}\")\n",
    "    print(f\"{'Variance:':<30} {stats_dict['variance']:>20.2f}\")\n",
    "    print(f\"{'Range (Max - Min):':<30} {stats_dict['range']:>20.2f}\")\n",
    "    print(f\"{'Interquartile Range (IQR):':<30} {stats_dict['iqr']:>20.2f}\")\n",
    "    print(f\"{'Coefficient of Variation:':<30} {stats_dict['cv']:>19.2f}%\")\n",
    "    \n",
    "    print(f\"\\n{'EXTREME VALUES':^70}\")\n",
    "    print(\"-\"*70)\n",
    "    print(f\"{'Minimum:':<30} {stats_dict['min']:>20.2f}\")\n",
    "    print(f\"{'Maximum:':<30} {stats_dict['max']:>20.2f}\")\n",
    "    print(f\"{'1st Percentile:':<30} {stats_dict['percentile_1']:>20.2f}\")\n",
    "    print(f\"{'99th Percentile:':<30} {stats_dict['percentile_99']:>20.2f}\")\n",
    "    \n",
    "    print(f\"\\n{'QUARTILES':^70}\")\n",
    "    print(\"-\"*70)\n",
    "    print(f\"{'Q1 (25th percentile):':<30} {stats_dict['q1']:>20.2f}\")\n",
    "    print(f\"{'Q2 (50th percentile/Median):':<30} {stats_dict['median']:>20.2f}\")\n",
    "    print(f\"{'Q3 (75th percentile):':<30} {stats_dict['q3']:>20.2f}\")\n",
    "    \n",
    "    print(f\"\\n{'DISTRIBUTION SHAPE':^70}\")\n",
    "    print(\"-\"*70)\n",
    "    print(f\"{'Skewness:':<30} {stats_dict['skewness']:>20.4f}\")\n",
    "    skew_interpretation = \"Right-skewed\" if stats_dict['skewness'] > 0 else \"Left-skewed\" if stats_dict['skewness'] < 0 else \"Symmetric\"\n",
    "    print(f\"{'  → Interpretation:':<30} {skew_interpretation:>20}\")\n",
    "    \n",
    "    print(f\"{'Kurtosis:':<30} {stats_dict['kurtosis']:>20.4f}\")\n",
    "    kurt_interpretation = \"Heavy-tailed\" if stats_dict['kurtosis'] > 0 else \"Light-tailed\" if stats_dict['kurtosis'] < 0 else \"Normal\"\n",
    "    print(f\"{'  → Interpretation:':<30} {kurt_interpretation:>20}\")\n",
    "    \n",
    "    print(f\"\\n{'QUALITY METRICS':^70}\")\n",
    "    print(\"-\"*70)\n",
    "    print(f\"{'Signal-to-Noise Ratio (SNR):':<30} {stats_dict['snr']:>20.2f}\")\n",
    "    \n",
    "    print(\"\\n\" + \"=\"*70)\n",
    "    \n",
    "    # Interpretation guide\n",
    "    print(f\"\\n{'📖 INTERPRETATION GUIDE':^70}\")\n",
    "    print(\"-\"*70)\n",
    "    print(\"\"\"\n",
    "    Skewness:\n",
    "      • = 0  : Symmetric distribution\n",
    "      • > 0  : Right-skewed (tail extends right, more low values)\n",
    "      • < 0  : Left-skewed (tail extends left, more high values)\n",
    "    \n",
    "    Kurtosis:\n",
    "      • = 0  : Normal distribution (mesokurtic)\n",
    "      • > 0  : Heavy-tailed, more outliers (leptokurtic)\n",
    "      • < 0  : Light-tailed, fewer outliers (platykurtic)\n",
    "    \n",
    "    Coefficient of Variation:\n",
    "      • Low (<15%) : Low variability relative to mean\n",
    "      • High (>30%) : High variability relative to mean\n",
    "    \"\"\")\n",
    "\n",
    "# Perform and display statistical analysis\n",
    "stats_result = comprehensive_statistical_analysis(dicom_data)\n",
    "display_statistics_report(stats_result, dicom_data)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a62ec189",
   "metadata": {},
   "source": [
    "## Ενότητα 28 — Συγκριτική Οπτικοποίηση Στατιστικών\n",
    "\n",
    "### Γιατί δεν αρκούν οι αριθμοί;\n",
    "\n",
    "Στην προηγούμενη ενότητα παραγάγαμε δεκάδες αριθμούς. Αλλά οι άνθρωποι δεν διαβάζουμε καλά πίνακες με 30+ νούμερα — βλέπουμε πολύ καλύτερα **σχήματα και σχέσεις**. Σε αυτή την ενότητα θα δείτε τέσσερα κλασικά διαγράμματα που «μεταφράζουν» τη στατιστική σε εικόνα.\n",
    "\n",
    "### Τα τέσσερα διαγράμματα του κώδικα\n",
    "\n",
    "#### 1. Box plot (διάγραμμα κουτιού)\n",
    "Πέντε αριθμοί σε ένα σχήμα: minimum, Q1, διάμεσος, Q3, maximum.\n",
    "\n",
    "- Το **κουτί** καλύπτει το IQR (το μεσαίο 50% των pixel).\n",
    "- Η **κόκκινη γραμμή** μέσα είναι η διάμεσος.\n",
    "- Τα **whiskers** (μουστάκια) εκτείνονται μέχρι περίπου τα ακραία «κανονικά» σημεία.\n",
    "- Σημεία πέρα από αυτά → **outliers**.\n",
    "\n",
    "> Χρήση: Γρήγορη αναγνώριση ακραίων τιμών και ασυμμετρίας.\n",
    "\n",
    "#### 2. Violin plot (διάγραμμα βιολιού)\n",
    "Συνδυάζει box plot + ιστόγραμμα. Το πάχος του «βιολιού» σε κάθε ύψος δείχνει **πυκνότητα pixel**. Όπου το βιολί φαρδαίνει, εκεί συγκεντρώνονται περισσότερα pixel.\n",
    "\n",
    "> Χρήση: Όταν θέλετε να φανεί η μορφή της κατανομής, όχι μόνο τα στατιστικά της.\n",
    "\n",
    "#### 3. Q-Q plot (Quantile-Quantile)\n",
    "**Το πιο σημαντικό διαγνωστικό διάγραμμα** της ενότητας. Συγκρίνει τα ποσοστημόρια των δεδομένων μας με αυτά μιας **κανονικής κατανομής**.\n",
    "\n",
    "- Αν τα σημεία πέφτουν πάνω στην ευθεία αναφοράς → η κατανομή είναι περίπου κανονική.\n",
    "- Αν τα σημεία αποκλίνουν, ειδικά στις άκρες → η κατανομή **δεν είναι κανονική** (συνηθισμένο για ιατρικές εικόνες, που έχουν συχνά πολυτροπικές κατανομές με φόντο, ιστούς, οστά).\n",
    "\n",
    "> ⚠️ Πολλές στατιστικές μέθοδοι (π.χ. t-test) προϋποθέτουν κανονικότητα. Πάντα ελέγχετε με Q-Q plot πριν εφαρμόσετε.\n",
    "\n",
    "#### 4. Bar chart κύριων στατιστικών\n",
    "Ραβδόγραμμα που δείχνει σε μία ματιά τη σχέση μέσης τιμής, διαμέσου, τυπικής απόκλισης και τεταρτημορίων. Χρήσιμο για **παρουσιάσεις** και αναφορές.\n",
    "\n",
    "> 💡 **Tip:** Όταν παρουσιάζετε αποτελέσματα σε γιατρό ή σε επιστημονική επιτροπή, το box plot και το violin plot «μιλάνε» πιο γρήγορα από τους πίνακες.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b6e72ead",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║          COMPARATIVE STATISTICS VISUALIZATION                  ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "def visualize_statistics_comparison(dicom_obj):\n",
    "    \"\"\"\n",
    "    Create visualizations comparing different statistical aspects\n",
    "    \"\"\"\n",
    "    \n",
    "    pixel_array = dicom_obj.pixel_array\n",
    "    \n",
    "    fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n",
    "    \n",
    "    # 1. Box Plot\n",
    "    axes[0, 0].boxplot(pixel_array.flatten(), vert=True, patch_artist=True,\n",
    "                       boxprops=dict(facecolor='lightblue', color='blue'),\n",
    "                       medianprops=dict(color='red', linewidth=2),\n",
    "                       whiskerprops=dict(color='blue'),\n",
    "                       capprops=dict(color='blue'))\n",
    "    \n",
    "    axes[0, 0].set_ylabel('Pixel Intensity', fontsize=12)\n",
    "    axes[0, 0].set_title('Box Plot - Distribution Summary', fontsize=12, fontweight='bold')\n",
    "    axes[0, 0].grid(True, alpha=0.3, axis='y')\n",
    "    \n",
    "    # Add annotations\n",
    "    q1, median, q3 = np.percentile(pixel_array.flatten(), [25, 50, 75])\n",
    "    axes[0, 0].text(1.15, q1, f'Q1: {q1:.1f}', fontsize=10, va='center')\n",
    "    axes[0, 0].text(1.15, median, f'Median: {median:.1f}', fontsize=10, va='center', color='red')\n",
    "    axes[0, 0].text(1.15, q3, f'Q3: {q3:.1f}', fontsize=10, va='center')\n",
    "    \n",
    "    # 2. Violin Plot (using histogram approximation)\n",
    "    axes[0, 1].violinplot([pixel_array.flatten()], vert=True, showmeans=True, showmedians=True)\n",
    "    axes[0, 1].set_ylabel('Pixel Intensity', fontsize=12)\n",
    "    axes[0, 1].set_title('Violin Plot - Distribution Density', fontsize=12, fontweight='bold')\n",
    "    axes[0, 1].grid(True, alpha=0.3, axis='y')\n",
    "    \n",
    "    # 3. Q-Q Plot (Quantile-Quantile)\n",
    "    from scipy import stats as scipy_stats\n",
    "    theoretical_quantiles = scipy_stats.norm.ppf(np.linspace(0.01, 0.99, 100))\n",
    "    sample_quantiles = np.percentile(pixel_array.flatten(), np.linspace(1, 99, 100))\n",
    "    \n",
    "    axes[1, 0].scatter(theoretical_quantiles, sample_quantiles, alpha=0.6, s=20)\n",
    "    axes[1, 0].plot(theoretical_quantiles, \n",
    "                    theoretical_quantiles * pixel_array.std() + pixel_array.mean(),\n",
    "                    'r--', linewidth=2, label='Normal distribution reference')\n",
    "    axes[1, 0].set_xlabel('Theoretical Quantiles (Normal)', fontsize=12)\n",
    "    axes[1, 0].set_ylabel('Sample Quantiles', fontsize=12)\n",
    "    axes[1, 0].set_title('Q-Q Plot - Normality Check', fontsize=12, fontweight='bold')\n",
    "    axes[1, 0].legend()\n",
    "    axes[1, 0].grid(True, alpha=0.3)\n",
    "    \n",
    "    # 4. Statistical Summary Bar Chart\n",
    "    stats_summary = {\n",
    "        'Mean': pixel_array.mean(),\n",
    "        'Median': np.median(pixel_array),\n",
    "        'Std Dev': pixel_array.std(),\n",
    "        'Q1': np.percentile(pixel_array, 25),\n",
    "        'Q3': np.percentile(pixel_array, 75)\n",
    "    }\n",
    "    \n",
    "    bars = axes[1, 1].bar(stats_summary.keys(), stats_summary.values(), \n",
    "                          color=['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8'],\n",
    "                          edgecolor='black', linewidth=1.5)\n",
    "    \n",
    "    axes[1, 1].set_ylabel('Value', fontsize=12)\n",
    "    axes[1, 1].set_title('Key Statistics Comparison', fontsize=12, fontweight='bold')\n",
    "    axes[1, 1].grid(True, alpha=0.3, axis='y')\n",
    "    axes[1, 1].tick_params(axis='x', rotation=45)\n",
    "    \n",
    "    # Add value labels on bars\n",
    "    for bar in bars:\n",
    "        height = bar.get_height()\n",
    "        axes[1, 1].text(bar.get_x() + bar.get_width()/2., height,\n",
    "                       f'{height:.1f}',\n",
    "                       ha='center', va='bottom', fontsize=10, fontweight='bold')\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "visualize_statistics_comparison(dicom_data)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "abc27be9",
   "metadata": {},
   "source": [
    "## Ενότητα 29 — Στατιστική ανά Περιοχή (Region-Based Statistics)\n",
    "\n",
    "### Το πρόβλημα του «καθολικού μέσου όρου»\n",
    "\n",
    "Μέχρι τώρα υπολογίζαμε στατιστικά για **όλη την εικόνα**. Αλλά μια ιατρική εικόνα είναι σπάνια ομοιόμορφη! Ένας όγκος καταλαμβάνει το 5% της εικόνας — αν τον αναμίξετε με 95% υγιή ιστό, τα στατιστικά του «πνίγονται». Επιπλέον, τεχνικά artifacts (π.χ. **ανομοιογένεια πεδίου** στο MRI, *bias field*) εμφανίζονται ως χωρικές διαφορές που η καθολική μέση τιμή κρύβει εντελώς.\n",
    "\n",
    "**Λύση:** χωρίζουμε την εικόνα σε υπο-περιοχές και υπολογίζουμε στατιστικά **σε κάθε μία ξεχωριστά**.\n",
    "\n",
    "### Πότε μας ενδιαφέρει η ανάλυση ανά περιοχή;\n",
    "\n",
    "- **Έλεγχος ομοιογένειας** του φόντου (background uniformity) — βασικό QC.\n",
    "- **Εντοπισμός artifacts** — αν μια περιοχή έχει πολύ διαφορετική τυπική απόκλιση από τις γειτονικές, κάτι «πειράζει».\n",
    "- **Ετερογένεια όγκου** — στην ογκολογία, η χωρική ετερογένεια είναι προγνωστικός παράγοντας.\n",
    "- **Bias field correction** — εντοπισμός βαθμωτών μεταβολών έντασης σε MRI.\n",
    "\n",
    "### Τι κάνει ο κώδικας\n",
    "\n",
    "Η `analyze_image_regions` χωρίζει την εικόνα σε **πλέγμα 3×3** (9 περιοχές) και για κάθε μία υπολογίζει mean, std, min, max, median. Το αποτέλεσμα γίνεται `pandas.DataFrame` — μια εξαιρετικά βολική μορφή για περαιτέρω ανάλυση.\n",
    "\n",
    "Στη συνέχεια η `visualize_region_statistics` παράγει:\n",
    "\n",
    "1. **Heatmap μέσης τιμής** — Δείχνει χωρικά πού είναι «πιο φωτεινή» η εικόνα. Ομοιόμορφο χρώμα → ομοιόμορφη εικόνα. Βαθμωτό χρώμα → bias.\n",
    "2. **Heatmap τυπικής απόκλισης** — Πού υπάρχει μεγαλύτερη μεταβλητότητα (συχνά εκεί όπου υπάρχουν δομές, ακμές, ιστοί).\n",
    "3. **Bar chart με error bars** — Μέση τιμή ανά περιοχή με ±1 std.\n",
    "4. **CV ανά περιοχή** — Σχετική μεταβλητότητα. Βοηθά στον εντοπισμό περιοχών με ασυνήθιστα υψηλή ή χαμηλή ετερογένεια.\n",
    "\n",
    "> 📌 **Παράμετρος `grid_size`:** Στον κώδικα είναι 3 (3×3 = 9 περιοχές). Σε εικόνες υψηλής ανάλυσης ίσως θέλετε 5×5 ή 10×10. Στις ασκήσεις θα παίξετε με αυτή την παράμετρο.\n",
    "\n",
    "> 🧠 **Σύνδεση με προχωρημένες έννοιες:** Αυτή η ιδέα — υπολογισμός χαρακτηριστικών σε τοπικά «patches» — είναι **ο πυρήνας** πολλών σύγχρονων μεθόδων: convolutional neural networks, radiomics texture features, και sliding-window analysis.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "085417b0",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              REGION-BASED STATISTICS                           ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "Analyzing statistics in different regions can reveal:\n",
    "✓ Spatial variations in image intensity\n",
    "✓ Tissue heterogeneity\n",
    "✓ Image artifacts\n",
    "✓ Quality assessment\n",
    "\"\"\")\n",
    "\n",
    "def analyze_image_regions(dicom_obj, grid_size=3):\n",
    "    \"\"\"\n",
    "    Divide image into grid and analyze each region separately\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        DICOM object\n",
    "    grid_size : int\n",
    "        Size of the grid (e.g., 3 means 3x3 = 9 regions)\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    pd.DataFrame : Statistics for each region\n",
    "    \"\"\"\n",
    "    \n",
    "    img = dicom_obj.pixel_array\n",
    "    rows, cols = img.shape\n",
    "    \n",
    "    # Calculate region sizes\n",
    "    row_step = rows // grid_size\n",
    "    col_step = cols // grid_size\n",
    "    \n",
    "    region_stats = []\n",
    "    \n",
    "    fig, axes = plt.subplots(grid_size, grid_size, figsize=(12, 12))\n",
    "    \n",
    "    for i in range(grid_size):\n",
    "        for j in range(grid_size):\n",
    "            # Extract region\n",
    "            r_start = i * row_step\n",
    "            r_end = (i + 1) * row_step if i < grid_size - 1 else rows\n",
    "            c_start = j * col_step\n",
    "            c_end = (j + 1) * col_step if j < grid_size - 1 else cols\n",
    "            \n",
    "            region = img[r_start:r_end, c_start:c_end]\n",
    "            \n",
    "            # Calculate statistics\n",
    "            region_stats.append({\n",
    "                'Region': f'R{i+1}C{j+1}',\n",
    "                'Row': i + 1,\n",
    "                'Col': j + 1,\n",
    "                'Mean': region.mean(),\n",
    "                'Std': region.std(),\n",
    "                'Min': region.min(),\n",
    "                'Max': region.max(),\n",
    "                'Median': np.median(region)\n",
    "            })\n",
    "            \n",
    "            # Visualize region\n",
    "            ax = axes[i, j] if grid_size > 1 else axes\n",
    "            ax.imshow(region, cmap='gray')\n",
    "            ax.set_title(f'R{i+1}C{j+1}\\nμ={region.mean():.0f}', fontsize=9)\n",
    "            ax.axis('off')\n",
    "            \n",
    "            # Add colored border based on mean intensity\n",
    "            for spine in ax.spines.values():\n",
    "                spine.set_edgecolor('red' if region.mean() > img.mean() else 'blue')\n",
    "                spine.set_linewidth(3)\n",
    "    \n",
    "    plt.suptitle(f'{grid_size}x{grid_size} Region Analysis\\nRed=Above Average, Blue=Below Average',\n",
    "                 fontsize=14, fontweight='bold')\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    # Create DataFrame\n",
    "    df = pd.DataFrame(region_stats)\n",
    "    \n",
    "    return df\n",
    "\n",
    "# Perform region analysis\n",
    "print(\"\\nPerforming 3x3 region analysis...\")\n",
    "region_df = analyze_image_regions(dicom_data, grid_size=3)\n",
    "print(\"\\n📊 Region Statistics Table:\")\n",
    "display(region_df)\n",
    "\n",
    "# Visualize region statistics\n",
    "def visualize_region_statistics(region_df):\n",
    "    \"\"\"\n",
    "    Visualize statistics across regions\n",
    "    \"\"\"\n",
    "    \n",
    "    fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n",
    "    \n",
    "    # Mean intensity heatmap\n",
    "    mean_grid = region_df.pivot(index='Row', columns='Col', values='Mean')\n",
    "    im1 = axes[0, 0].imshow(mean_grid, cmap='hot', aspect='auto')\n",
    "    axes[0, 0].set_title('Mean Intensity Heatmap', fontweight='bold')\n",
    "    axes[0, 0].set_xlabel('Column')\n",
    "    axes[0, 0].set_ylabel('Row')\n",
    "    plt.colorbar(im1, ax=axes[0, 0])\n",
    "    \n",
    "    # Add values on heatmap\n",
    "    for i in range(len(mean_grid)):\n",
    "        for j in range(len(mean_grid.columns)):\n",
    "            axes[0, 0].text(j, i, f'{mean_grid.iloc[i, j]:.0f}',\n",
    "                          ha='center', va='center', color='white', fontweight='bold')\n",
    "    \n",
    "    # Standard deviation heatmap\n",
    "    std_grid = region_df.pivot(index='Row', columns='Col', values='Std')\n",
    "    im2 = axes[0, 1].imshow(std_grid, cmap='viridis', aspect='auto')\n",
    "    axes[0, 1].set_title('Standard Deviation Heatmap', fontweight='bold')\n",
    "    axes[0, 1].set_xlabel('Column')\n",
    "    axes[0, 1].set_ylabel('Row')\n",
    "    plt.colorbar(im2, ax=axes[0, 1])\n",
    "    \n",
    "    # Add values on heatmap\n",
    "    for i in range(len(std_grid)):\n",
    "        for j in range(len(std_grid.columns)):\n",
    "            axes[0, 1].text(j, i, f'{std_grid.iloc[i, j]:.0f}',\n",
    "                          ha='center', va='center', color='white', fontweight='bold')\n",
    "    \n",
    "    # Bar chart comparison\n",
    "    x_pos = np.arange(len(region_df))\n",
    "    axes[1, 0].bar(x_pos, region_df['Mean'], alpha=0.7, label='Mean', color='steelblue')\n",
    "    axes[1, 0].errorbar(x_pos, region_df['Mean'], yerr=region_df['Std'], \n",
    "                       fmt='none', color='red', capsize=5, label='±1 Std')\n",
    "    axes[1, 0].set_xlabel('Region')\n",
    "    axes[1, 0].set_ylabel('Intensity')\n",
    "    axes[1, 0].set_title('Mean Intensity per Region (with Std Dev)', fontweight='bold')\n",
    "    axes[1, 0].set_xticks(x_pos)\n",
    "    axes[1, 0].set_xticklabels(region_df['Region'], rotation=45)\n",
    "    axes[1, 0].legend()\n",
    "    axes[1, 0].grid(True, alpha=0.3, axis='y')\n",
    "    \n",
    "    # Coefficient of variation\n",
    "    cv = (region_df['Std'] / region_df['Mean']) * 100\n",
    "    axes[1, 1].bar(region_df['Region'], cv, color='coral', edgecolor='black')\n",
    "    axes[1, 1].set_xlabel('Region')\n",
    "    axes[1, 1].set_ylabel('Coefficient of Variation (%)')\n",
    "    axes[1, 1].set_title('Variability per Region', fontweight='bold')\n",
    "    axes[1, 1].tick_params(axis='x', rotation=45)\n",
    "    axes[1, 1].grid(True, alpha=0.3, axis='y')\n",
    "    axes[1, 1].axhline(y=cv.mean(), color='red', linestyle='--', \n",
    "                      linewidth=2, label=f'Mean CV: {cv.mean():.1f}%')\n",
    "    axes[1, 1].legend()\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "visualize_region_statistics(region_df)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ba4a4be1",
   "metadata": {},
   "source": [
    "## Ενότητα 30 — Ανάλυση Προφίλ Έντασης (Intensity Profiles)\n",
    "\n",
    "### Τι είναι ένα προφίλ έντασης;\n",
    "\n",
    "Φανταστείτε ότι «κόβετε» την εικόνα κατά μήκος μιας ευθείας γραμμής και κοιτάτε τις τιμές των pixel πάνω σε αυτή τη γραμμή. Αυτό είναι ένα **προφίλ έντασης**: μια μονοδιάστατη ακολουθία αριθμών που δείχνει πώς αλλάζει η ένταση κατά μήκος της γραμμής.\n",
    "\n",
    "> 📌 **Αναλογία:** Όπως όταν ένα GPS δείχνει το **υψομετρικό προφίλ** μιας πεζοπορίας — από επίπεδο σε ανήφορο, σε κορυφή, σε κατήφορο. Εδώ το «υψόμετρο» είναι η ένταση των pixel.\n",
    "\n",
    "### Γιατί είναι χρήσιμα;\n",
    "\n",
    "1. **Εντοπισμός ορίων (edges):** Όπου το προφίλ αλλάζει απότομα → εκεί υπάρχει σύνορο μεταξύ ιστών.\n",
    "2. **Μέτρηση αποστάσεων:** Συνδυάζοντας με το `PixelSpacing` του DICOM, μπορούμε να μετρήσουμε διαστάσεις ανατομικών δομών σε χιλιοστά.\n",
    "3. **Quality control:** Αν το προφίλ φαίνεται «θολωμένο» αντί για απότομες μεταβάσεις, η εικόνα έχει χαμηλή χωρική ανάλυση ή κίνηση.\n",
    "4. **Ποσοτική σύγκριση:** Πριν/μετά θεραπεία — πώς άλλαξε το προφίλ στην ίδια θέση.\n",
    "\n",
    "### Τι κάνει ο κώδικας\n",
    "\n",
    "Η `analyze_intensity_profiles` εξάγει **τρία προφίλ**:\n",
    "\n",
    "- **Οριζόντιο** — η μεσαία γραμμή της εικόνας.\n",
    "- **Κάθετο** — η μεσαία στήλη.\n",
    "- **Διαγώνιο** — από επάνω-αριστερά προς κάτω-δεξιά.\n",
    "\n",
    "Και στη συνέχεια υπολογίζει την **κλίση (gradient)** του οριζόντιου προφίλ. Αυτό είναι κρίσιμο: η κλίση είναι ο **ρυθμός μεταβολής** της έντασης. Όταν η κλίση είναι κοντά στο 0, βρισκόμαστε σε μια ομοιογενή περιοχή. Όταν η κλίση εκτοξεύεται (θετικά ή αρνητικά), έχουμε **ακμή** — μετάβαση από έναν ιστό σε άλλον.\n",
    "\n",
    "```\n",
    "Προφίλ:    ─────╱╲────────╱╲──────\n",
    "Gradient:  ─0─▲──▼─0─0─▲──▼─0──\n",
    "                ↑       ↑\n",
    "              ακμές (edges)\n",
    "```\n",
    "\n",
    "> 🧠 **Σύνδεση με computer vision:** Όλοι οι κλασικοί ανιχνευτές ακμών (Sobel, Canny) βασίζονται στην ιδέα της κλίσης. Εδώ τη βλέπετε στην απλούστερη μορφή της — σε μία διάσταση.\n",
    "\n",
    "> 💡 **Στην κλινική πράξη:** Οι ακτινολόγοι σχεδιάζουν προφίλ για να μετρήσουν π.χ. το πλάτος αρτηρίας ή τη διάμετρο όγκου με ακρίβεια — όχι «με το μάτι».\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f083e4eb",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              INTENSITY PROFILE ANALYSIS                        ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "Intensity profiles show how pixel values change along a line.\n",
    "Useful for:\n",
    "✓ Measuring edges and boundaries\n",
    "✓ Detecting gradients\n",
    "✓ Quality control\n",
    "✓ Quantitative analysis\n",
    "\"\"\")\n",
    "\n",
    "def analyze_intensity_profiles(dicom_obj):\n",
    "    \"\"\"\n",
    "    Analyze intensity profiles along different directions\n",
    "    \"\"\"\n",
    "    \n",
    "    img = dicom_obj.pixel_array\n",
    "    rows, cols = img.shape\n",
    "    \n",
    "    fig, axes = plt.subplots(3, 2, figsize=(14, 12))\n",
    "    \n",
    "    # Show image with profile lines\n",
    "    axes[0, 0].imshow(img, cmap='gray')\n",
    "    axes[0, 0].set_title('Image with Profile Lines', fontweight='bold')\n",
    "    \n",
    "    # Horizontal profile (middle row)\n",
    "    mid_row = rows // 2\n",
    "    axes[0, 0].axhline(y=mid_row, color='red', linestyle='--', linewidth=2, label='Horizontal')\n",
    "    profile_h = img[mid_row, :]\n",
    "    \n",
    "    axes[0, 1].plot(profile_h, color='red', linewidth=2)\n",
    "    axes[0, 1].set_title(f'Horizontal Profile (Row {mid_row})', fontweight='bold')\n",
    "    axes[0, 1].set_xlabel('Column Position')\n",
    "    axes[0, 1].set_ylabel('Intensity')\n",
    "    axes[0, 1].grid(True, alpha=0.3)\n",
    "    axes[0, 1].fill_between(range(len(profile_h)), profile_h, alpha=0.3, color='red')\n",
    "    \n",
    "    # Vertical profile (middle column)\n",
    "    mid_col = cols // 2\n",
    "    axes[0, 0].axvline(x=mid_col, color='blue', linestyle='--', linewidth=2, label='Vertical')\n",
    "    profile_v = img[:, mid_col]\n",
    "    \n",
    "    axes[1, 0].plot(profile_v, color='blue', linewidth=2)\n",
    "    axes[1, 0].set_title(f'Vertical Profile (Column {mid_col})', fontweight='bold')\n",
    "    axes[1, 0].set_xlabel('Row Position')\n",
    "    axes[1, 0].set_ylabel('Intensity')\n",
    "    axes[1, 0].grid(True, alpha=0.3)\n",
    "    axes[1, 0].fill_between(range(len(profile_v)), profile_v, alpha=0.3, color='blue')\n",
    "    \n",
    "    # Diagonal profile (top-left to bottom-right)\n",
    "    diag_length = min(rows, cols)\n",
    "    diag_indices = np.linspace(0, diag_length-1, diag_length).astype(int)\n",
    "    profile_diag = img[diag_indices, diag_indices]\n",
    "    \n",
    "    axes[0, 0].plot([0, diag_length-1], [0, diag_length-1], \n",
    "                    color='green', linestyle='--', linewidth=2, label='Diagonal')\n",
    "    axes[0, 0].legend()\n",
    "    \n",
    "    axes[1, 1].plot(profile_diag, color='green', linewidth=2)\n",
    "    axes[1, 1].set_title('Diagonal Profile (TL to BR)', fontweight='bold')\n",
    "    axes[1, 1].set_xlabel('Position along Diagonal')\n",
    "    axes[1, 1].set_ylabel('Intensity')\n",
    "    axes[1, 1].grid(True, alpha=0.3)\n",
    "    axes[1, 1].fill_between(range(len(profile_diag)), profile_diag, alpha=0.3, color='green')\n",
    "    \n",
    "    # Profile statistics comparison\n",
    "    profiles_data = {\n",
    "        'Horizontal': profile_h,\n",
    "        'Vertical': profile_v,\n",
    "        'Diagonal': profile_diag\n",
    "    }\n",
    "    \n",
    "    profile_stats = []\n",
    "    for name, profile in profiles_data.items():\n",
    "        profile_stats.append({\n",
    "            'Profile': name,\n",
    "            'Mean': profile.mean(),\n",
    "            'Std': profile.std(),\n",
    "            'Min': profile.min(),\n",
    "            'Max': profile.max(),\n",
    "            'Range': profile.max() - profile.min()\n",
    "        })\n",
    "    \n",
    "    profile_stats_df = pd.DataFrame(profile_stats)\n",
    "    \n",
    "    # Plot statistics comparison\n",
    "    x = np.arange(len(profile_stats_df))\n",
    "    width = 0.15\n",
    "    \n",
    "    axes[2, 0].bar(x - 2*width, profile_stats_df['Mean'], width, label='Mean', color='steelblue')\n",
    "    axes[2, 0].bar(x - width, profile_stats_df['Std'], width, label='Std', color='orange')\n",
    "    axes[2, 0].bar(x, profile_stats_df['Min'], width, label='Min', color='green')\n",
    "    axes[2, 0].bar(x + width, profile_stats_df['Max'], width, label='Max', color='red')\n",
    "    axes[2, 0].bar(x + 2*width, profile_stats_df['Range'], width, label='Range', color='purple')\n",
    "    \n",
    "    axes[2, 0].set_xlabel('Profile Type')\n",
    "    axes[2, 0].set_ylabel('Value')\n",
    "    axes[2, 0].set_title('Profile Statistics Comparison', fontweight='bold')\n",
    "    axes[2, 0].set_xticks(x)\n",
    "    axes[2, 0].set_xticklabels(profile_stats_df['Profile'])\n",
    "    axes[2, 0].legend()\n",
    "    axes[2, 0].grid(True, alpha=0.3, axis='y')\n",
    "    \n",
    "    # Gradient analysis (rate of change)\n",
    "    gradient_h = np.gradient(profile_h)\n",
    "    axes[2, 1].plot(gradient_h, color='darkred', linewidth=2)\n",
    "    axes[2, 1].set_title('Horizontal Profile Gradient (Rate of Change)', fontweight='bold')\n",
    "    axes[2, 1].set_xlabel('Column Position')\n",
    "    axes[2, 1].set_ylabel('Gradient (Intensity Change)')\n",
    "    axes[2, 1].grid(True, alpha=0.3)\n",
    "    axes[2, 1].axhline(y=0, color='black', linestyle='-', linewidth=1)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    return profile_stats_df\n",
    "\n",
    "# Perform profile analysis\n",
    "profile_stats = analyze_intensity_profiles(dicom_data)\n",
    "print(\"\\n📊 Profile Statistics:\")\n",
    "display(profile_stats)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56976d90",
   "metadata": {},
   "source": [
    "## Ενότητα 31 — Εξίσωση Ιστογράμματος (Histogram Equalization)\n",
    "\n",
    "### Το πρόβλημα της χαμηλής αντίθεσης\n",
    "\n",
    "Πολλές ιατρικές εικόνες χρησιμοποιούν **μόνο ένα μικρό κομμάτι** του διαθέσιμου εύρους έντασης. Αν π.χ. μια εικόνα 8-bit (0–255) χρησιμοποιεί μόνο τιμές 80–140, το ιστόγραμμα είναι «στριμωγμένο» στη μέση και η εικόνα φαίνεται **γκρίζα και άτονη** — χάνουμε λεπτομέρειες.\n",
    "\n",
    "### Η ιδέα της εξίσωσης\n",
    "\n",
    "Η **εξίσωση ιστογράμματος** είναι μια μη-γραμμική μεταμόρφωση που **ξαπλώνει** την κατανομή των εντάσεων ώστε να καλύψει όλο το διαθέσιμο εύρος. Στόχος: ένα ιστόγραμμα όσο πιο **ομοιόμορφο** γίνεται, που μεταφράζεται σε εικόνα με αυξημένη αντίθεση.\n",
    "\n",
    "### Πώς δουλεύει — απλή μαθηματική διαίσθηση\n",
    "\n",
    "Το κλειδί είναι η **CDF** (Cumulative Distribution Function): η αθροιστική κατανομή των εντάσεων. Τα βήματα είναι:\n",
    "\n",
    "1. Υπολογίζουμε το ιστόγραμμα `h(i)`.\n",
    "2. Υπολογίζουμε την CDF: `cdf(i) = Σ h(j) για j ≤ i`.\n",
    "3. **Κανονικοποιούμε** την CDF στο εύρος [0, 255].\n",
    "4. Χρησιμοποιούμε την κανονικοποιημένη CDF ως **lookup table**: κάθε αρχικό pixel τιμής `i` αντικαθίσταται με την τιμή `cdf'(i)`.\n",
    "\n",
    "> 🧠 **Γιατί δουλεύει:** Η CDF, όταν κανονικοποιηθεί, είναι ο «τέλειος» μετασχηματισμός που μετατρέπει οποιαδήποτε κατανομή σε ομοιόμορφη.\n",
    "\n",
    "### Τι θα δείτε στον κώδικα\n",
    "\n",
    "Έξι υποδιαγράμματα που δείχνουν όλη τη διαδικασία:\n",
    "\n",
    "| # | Τι δείχνει | Τι μαθαίνετε |\n",
    "|---|------------|--------------|\n",
    "| 1 | Αρχική εικόνα | Σημείο εκκίνησης |\n",
    "| 2 | Αρχικό ιστόγραμμα | Πόσο «στριμωγμένο» ήταν |\n",
    "| 3 | CDF | Η συνάρτηση μετασχηματισμού |\n",
    "| 4 | Εικόνα μετά την εξίσωση | Αυξημένη αντίθεση |\n",
    "| 5 | Νέο ιστόγραμμα | Πιο ομοιόμορφη κατανομή |\n",
    "| 6 | Mapping function | Πώς απεικονίζονται οι παλιές → νέες τιμές |\n",
    "\n",
    "### ⚠️ Προσοχή στην ιατρική χρήση\n",
    "\n",
    "Η εξίσωση ιστογράμματος **αλλάζει** τις απόλυτες τιμές των pixel. Αυτό σημαίνει ότι:\n",
    "\n",
    "- **ΔΕΝ** μπορεί να εφαρμοστεί σε CT αν θέλετε να διατηρήσετε τις **Hounsfield Units** (που έχουν φυσικό νόημα — π.χ. −1000 = αέρας, 0 = νερό).\n",
    "- Είναι κατάλληλη για **οπτικοποίηση** ή ως pre-processing σε νευρωνικά δίκτυα φυσικών εικόνων.\n",
    "- Σε MRI, μπορεί να χρησιμοποιηθεί προσεκτικά αν δεν σας ενδιαφέρουν απόλυτες εντάσεις.\n",
    "\n",
    "> 📌 **Εναλλακτική:** Το **CLAHE** (Contrast-Limited Adaptive Histogram Equalization) εφαρμόζει εξίσωση τοπικά και είναι πιο φιλικό σε ιατρικές εικόνες. Δείτε το `cv2.createCLAHE()` αν θέλετε να πειραματιστείτε.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "392abd1f",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              HISTOGRAM EQUALIZATION                            ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "Histogram equalization is a technique to improve image contrast\n",
    "by redistributing pixel intensities to use the full dynamic range.\n",
    "\n",
    "Applications:\n",
    "✓ Enhancing low-contrast images\n",
    "✓ Improving visibility of details\n",
    "✓ Preprocessing for analysis\n",
    "\"\"\")\n",
    "\n",
    "def demonstrate_histogram_equalization(dicom_obj):\n",
    "    \"\"\"\n",
    "    Demonstrate histogram equalization\n",
    "    \"\"\"\n",
    "    \n",
    "    img = dicom_obj.pixel_array.astype(float)\n",
    "    \n",
    "    # Normalize to 0-255 for equalization\n",
    "    img_normalized = ((img - img.min()) / (img.max() - img.min()) * 255).astype(np.uint8)\n",
    "    \n",
    "    # Compute histogram\n",
    "    hist, bins = np.histogram(img_normalized.flatten(), bins=256, range=[0, 256])\n",
    "    \n",
    "    # Compute cumulative distribution function (CDF)\n",
    "    cdf = hist.cumsum()\n",
    "    cdf_normalized = cdf * hist.max() / cdf.max()  # Normalize for display\n",
    "    \n",
    "    # Equalization\n",
    "    cdf_masked = np.ma.masked_equal(cdf, 0)\n",
    "    cdf_masked = (cdf_masked - cdf_masked.min()) * 255 / (cdf_masked.max() - cdf_masked.min())\n",
    "    cdf_final = np.ma.filled(cdf_masked, 0).astype('uint8')\n",
    "    \n",
    "    # Apply equalization\n",
    "    img_equalized = cdf_final[img_normalized]\n",
    "    \n",
    "    # Create visualization\n",
    "    fig, axes = plt.subplots(2, 3, figsize=(18, 10))\n",
    "    \n",
    "    # Original image\n",
    "    axes[0, 0].imshow(img_normalized, cmap='gray')\n",
    "    axes[0, 0].set_title('Original Image', fontweight='bold', fontsize=12)\n",
    "    axes[0, 0].axis('off')\n",
    "    \n",
    "    # Original histogram\n",
    "    axes[0, 1].hist(img_normalized.flatten(), bins=256, range=[0, 256], \n",
    "                    color='steelblue', alpha=0.7, edgecolor='black')\n",
    "    axes[0, 1].set_title('Original Histogram', fontweight='bold', fontsize=12)\n",
    "    axes[0, 1].set_xlabel('Pixel Intensity')\n",
    "    axes[0, 1].set_ylabel('Frequency')\n",
    "    axes[0, 1].grid(True, alpha=0.3)\n",
    "    \n",
    "    # CDF\n",
    "    axes[0, 2].plot(cdf_normalized, color='darkblue', linewidth=2)\n",
    "    axes[0, 2].set_title('Cumulative Distribution Function', fontweight='bold', fontsize=12)\n",
    "    axes[0, 2].set_xlabel('Pixel Intensity')\n",
    "    axes[0, 2].set_ylabel('Cumulative Frequency')\n",
    "    axes[0, 2].grid(True, alpha=0.3)\n",
    "    axes[0, 2].set_xlim([0, 256])\n",
    "    \n",
    "    # Equalized image\n",
    "    axes[1, 0].imshow(img_equalized, cmap='gray')\n",
    "    axes[1, 0].set_title('Equalized Image', fontweight='bold', fontsize=12)\n",
    "    axes[1, 0].axis('off')\n",
    "    \n",
    "    # Equalized histogram\n",
    "    axes[1, 1].hist(img_equalized.flatten(), bins=256, range=[0, 256],\n",
    "                    color='coral', alpha=0.7, edgecolor='black')\n",
    "    axes[1, 1].set_title('Equalized Histogram', fontweight='bold', fontsize=12)\n",
    "    axes[1, 1].set_xlabel('Pixel Intensity')\n",
    "    axes[1, 1].set_ylabel('Frequency')\n",
    "    axes[1, 1].grid(True, alpha=0.3)\n",
    "    \n",
    "    # Comparison\n",
    "    axes[1, 2].plot(cdf_final, color='red', linewidth=2, label='Mapping Function')\n",
    "    axes[1, 2].set_title('Equalization Mapping Function', fontweight='bold', fontsize=12)\n",
    "    axes[1, 2].set_xlabel('Input Intensity')\n",
    "    axes[1, 2].set_ylabel('Output Intensity')\n",
    "    axes[1, 2].grid(True, alpha=0.3)\n",
    "    axes[1, 2].legend()\n",
    "    axes[1, 2].set_xlim([0, 256])\n",
    "    axes[1, 2].set_ylim([0, 256])\n",
    "    \n",
    "    plt.suptitle('Histogram Equalization Process', fontsize=16, fontweight='bold', y=1.00)\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    # Statistics comparison\n",
    "    print(\"\\n📊 Comparison Statistics:\")\n",
    "    print(\"=\"*60)\n",
    "    print(f\"{'Metric':<30} {'Original':>12} {'Equalized':>12}\")\n",
    "    print(\"-\"*60)\n",
    "    print(f\"{'Mean':<30} {img_normalized.mean():>12.2f} {img_equalized.mean():>12.2f}\")\n",
    "    print(f\"{'Std Dev':<30} {img_normalized.std():>12.2f} {img_equalized.std():>12.2f}\")\n",
    "    print(f\"{'Min':<30} {img_normalized.min():>12.2f} {img_equalized.min():>12.2f}\")\n",
    "    print(f\"{'Max':<30} {img_normalized.max():>12.2f} {img_equalized.max():>12.2f}\")\n",
    "    print(f\"{'Dynamic Range':<30} {np.ptp(img_normalized):>12.2f} {np.ptp(img_equalized):>12.2f}\")\n",
    "    print(\"=\"*60)\n",
    "\n",
    "demonstrate_histogram_equalization(dicom_data)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6edfe3e0",
   "metadata": {},
   "source": [
    "## Ενότητα 32 — Ανάλυση Αντίθεσης και Φωτεινότητας\n",
    "\n",
    "### Δύο διαφορετικές έννοιες — μη τις μπερδεύετε\n",
    "\n",
    "Στην καθημερινή ομιλία λέμε «αυτή η εικόνα είναι σκοτεινή» ή «έχει χαμηλή αντίθεση» χωρίς πολλή σκέψη. Στην ποσοτική ανάλυση όμως είναι **δύο εντελώς ξεχωριστές μετρικές**:\n",
    "\n",
    "| Έννοια | Τι μετρά | Σχέση με στατιστική |\n",
    "|--------|----------|----------------------|\n",
    "| **Φωτεινότητα** (brightness) | Συνολικό «επίπεδο» της εικόνας | Μέση τιμή των pixel |\n",
    "| **Αντίθεση** (contrast) | Διαφορά μεταξύ φωτεινών και σκοτεινών περιοχών | Διασπορά των pixel |\n",
    "\n",
    "> 💡 **Φανταστείτε:** Δύο εικόνες με την **ίδια** μέση τιμή. Η μία έχει pixel στην περιοχή 100–110 (πολύ χαμηλή αντίθεση — όλα μοιάζουν). Η άλλη έχει pixel σε όλη την κλίμακα 0–255 (υψηλή αντίθεση). Ίδια φωτεινότητα, εντελώς διαφορετική αντίθεση.\n",
    "\n",
    "### Δύο τρόποι να μετρήσετε αντίθεση\n",
    "\n",
    "Στον κώδικα υπολογίζονται και οι δύο γνωστές μετρικές:\n",
    "\n",
    "#### 1. RMS contrast\n",
    "$$\n",
    "C_{\\text{RMS}} = \\sigma_I\n",
    "$$\n",
    "Απλή τυπική απόκλιση (στην κανονικοποιημένη εικόνα). **Δίνει βάρος σε όλα τα pixel** και είναι πιο αντιπροσωπευτική για φυσικές/ιατρικές εικόνες.\n",
    "\n",
    "#### 2. Michelson contrast\n",
    "$$\n",
    "C_{\\text{Michelson}} = \\dfrac{I_{\\max} - I_{\\min}}{I_{\\max} + I_{\\min}}\n",
    "$$\n",
    "Λαμβάνει υπόψη **μόνο** τις ακραίες τιμές. Παραδοσιακά χρησιμοποιείται για **περιοδικά** μοτίβα (π.χ. test patterns σε οπτική). Σε ιατρικές εικόνες είναι ευαίσθητο σε outliers.\n",
    "\n",
    "### Τι κάνει ο κώδικας\n",
    "\n",
    "Δημιουργεί **πέντε εκδοχές** της ίδιας εικόνας για να δείτε **οπτικά** τη διαφορά:\n",
    "\n",
    "1. **Αρχική** — αναφορά.\n",
    "2. **Αυξημένη φωτεινότητα** — `image + 0.2`. Όλα τα pixel ανεβαίνουν.\n",
    "3. **Μειωμένη φωτεινότητα** — `image - 0.2`. Όλα τα pixel πέφτουν.\n",
    "4. **Αυξημένη αντίθεση** — `(image - 0.5) * 1.5 + 0.5`. Τα pixel «τραβιούνται» μακριά από το κέντρο.\n",
    "5. **Μειωμένη αντίθεση** — `(image - 0.5) * 0.5 + 0.5`. Τα pixel «συμπιέζονται» γύρω από το κέντρο.\n",
    "\n",
    "Στους τίτλους εμφανίζονται οι **νέες** τιμές brightness και contrast, ώστε να δείτε ποσοτικά πώς αλλάζουν.\n",
    "\n",
    "> 📌 **Σύνδεση με κλινική πρακτική:** Αυτό ακριβώς που κάνει ένας ακτινολόγος όταν ρυθμίζει το **window/level** σε έναν σταθμό εργασίας — αλλάζει εικονικά την αντίθεση και τη φωτεινότητα **χωρίς να πειράζει τα δεδομένα**, για καλύτερη οπτική ερμηνεία.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "346143a7",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║          CONTRAST AND BRIGHTNESS ANALYSIS                      ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "Understanding contrast and brightness:\n",
    "- Brightness: Overall intensity level (related to mean)\n",
    "- Contrast: Difference between light and dark areas (related to std dev)\n",
    "\"\"\")\n",
    "\n",
    "def analyze_contrast_brightness(dicom_obj):\n",
    "    \"\"\"\n",
    "    Analyze and visualize contrast and brightness\n",
    "    \"\"\"\n",
    "    \n",
    "    img = dicom_obj.pixel_array.astype(float)\n",
    "    \n",
    "    # Normalize\n",
    "    img_norm = (img - img.min()) / (img.max() - img.min())\n",
    "    \n",
    "    # Calculate metrics\n",
    "    brightness = img_norm.mean()\n",
    "    contrast = img_norm.std()\n",
    "    \n",
    "    # Michelson contrast (for images with distinct bright and dark regions)\n",
    "    max_val = img_norm.max()\n",
    "    min_val = img_norm.min()\n",
    "    michelson_contrast = (max_val - min_val) / (max_val + min_val) if (max_val + min_val) > 0 else 0\n",
    "    \n",
    "    # RMS contrast\n",
    "    rms_contrast = img_norm.std()\n",
    "    \n",
    "    # Create variations\n",
    "    # Increase brightness\n",
    "    img_bright = np.clip(img_norm + 0.2, 0, 1)\n",
    "    \n",
    "    # Decrease brightness\n",
    "    img_dark = np.clip(img_norm - 0.2, 0, 1)\n",
    "    \n",
    "    # Increase contrast\n",
    "    img_high_contrast = np.clip((img_norm - 0.5) * 1.5 + 0.5, 0, 1)\n",
    "    \n",
    "    # Decrease contrast\n",
    "    img_low_contrast = np.clip((img_norm - 0.5) * 0.5 + 0.5, 0, 1)\n",
    "    \n",
    "    # Visualize\n",
    "    fig, axes = plt.subplots(2, 3, figsize=(16, 10))\n",
    "    \n",
    "    images = [\n",
    "        (img_norm, 'Original'),\n",
    "        (img_bright, 'Increased Brightness'),\n",
    "        (img_dark, 'Decreased Brightness'),\n",
    "        (img_high_contrast, 'Increased Contrast'),\n",
    "        (img_low_contrast, 'Decreased Contrast'),\n",
    "    ]\n",
    "    \n",
    "    for idx, (image, title) in enumerate(images):\n",
    "        row = idx // 3\n",
    "        col = idx % 3\n",
    "        \n",
    "        axes[row, col].imshow(image, cmap='gray')\n",
    "        \n",
    "        # Calculate metrics for each variation\n",
    "        b = image.mean()\n",
    "        c = image.std()\n",
    "        \n",
    "        axes[row, col].set_title(f'{title}\\nBrightness: {b:.3f}, Contrast: {c:.3f}',\n",
    "                                fontweight='bold', fontsize=10)\n",
    "        axes[row, col].axis('off')\n",
    "    \n",
    "    # Add metrics display\n",
    "    axes[1, 2].axis('off')\n",
    "    metrics_text = f\"\"\"\n",
    "    CONTRAST METRICS\n",
    "    ─────────────────────\n",
    "    \n",
    "    Brightness (Mean):\n",
    "      {brightness:.4f}\n",
    "    \n",
    "    RMS Contrast (Std):\n",
    "      {rms_contrast:.4f}\n",
    "    \n",
    "    Michelson Contrast:\n",
    "      {michelson_contrast:.4f}\n",
    "    \n",
    "    Dynamic Range:\n",
    "      {np.ptp(img_norm):.4f}\n",
    "    \n",
    "    ─────────────────────\n",
    "    Interpretation:\n",
    "    • Brightness: [0-1]\n",
    "      0.5 = medium\n",
    "    • Contrast: [0-∞]\n",
    "      Higher = more contrast\n",
    "    • Michelson: [0-1]\n",
    "      1 = maximum contrast\n",
    "    \"\"\"\n",
    "    \n",
    "    axes[1, 2].text(0.1, 0.5, metrics_text, fontsize=10, family='monospace',\n",
    "                   verticalalignment='center',\n",
    "                   bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))\n",
    "    \n",
    "    plt.suptitle('Contrast and Brightness Analysis', fontsize=16, fontweight='bold')\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "analyze_contrast_brightness(dicom_data)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9d664b0",
   "metadata": {},
   "source": [
    "## Ενότητα 33 — Ανάλυση Θορύβου\n",
    "\n",
    "### Τι είναι ο θόρυβος και από πού προέρχεται;\n",
    "\n",
    "**Θόρυβος** είναι η ανεπιθύμητη τυχαία διακύμανση των τιμών των pixel που **δεν** σχετίζεται με την υποκείμενη ανατομία. Σε ιατρικές εικόνες προέρχεται από:\n",
    "\n",
    "- **Θερμικός θόρυβος** — από τα ηλεκτρονικά κυκλώματα.\n",
    "- **Κβαντικός θόρυβος (Poisson)** — εγγενής στη φύση των φωτονίων (CT, mammography).\n",
    "- **Θόρυβος ψηφιοποίησης** — όρια ακρίβειας του ADC.\n",
    "- **Κίνηση ασθενούς** — δεν είναι «θόρυβος» τεχνικά, αλλά εμφανίζεται με παρόμοιο τρόπο.\n",
    "\n",
    "### Γιατί μας ενδιαφέρει;\n",
    "\n",
    "- **Διαγνωστική ακρίβεια:** Πολύς θόρυβος καλύπτει μικρές βλάβες.\n",
    "- **Αυτόματη ανάλυση:** Αλγόριθμοι segmentation, registration και AI παίρνουν χειρότερα αποτελέσματα σε θορυβώδεις εικόνες.\n",
    "- **Quality assurance:** Σε κλινικά μηχανήματα, ο τακτικός έλεγχος SNR είναι μέρος του πρωτοκόλλου ποιότητας.\n",
    "\n",
    "### Τρεις μέθοδοι εκτίμησης θορύβου\n",
    "\n",
    "#### Μέθοδος 1: Τυπική απόκλιση σε ομοιογενή περιοχή\n",
    "Σε μια **καθαρή** περιοχή (π.χ. αέρας στις γωνίες) η μόνη πηγή μεταβλητότητας είναι ο θόρυβος. Άρα:\n",
    "$$\n",
    "\\sigma_{\\text{noise}} \\approx \\sigma_{\\text{corner}}\n",
    "$$\n",
    "Ο κώδικας παίρνει και τις τέσσερις γωνίες, υπολογίζει τη std σε καθεμιά, και κρατά τη μέση τιμή.\n",
    "\n",
    "> ⚠️ **Προσοχή:** Αν τυχόν μια γωνία ΔΕΝ είναι φόντο (π.χ. ένας ώμος ασθενούς πέφτει στη γωνία), η μέθοδος δίνει υπερεκτίμηση. Γι' αυτό βλέπετε ότι κρατάμε και την **ελάχιστη** corner std — πιο ανθεκτική στην εξαίρεση.\n",
    "\n",
    "#### Μέθοδος 2: MAD (Median Absolute Deviation)\n",
    "$$\n",
    "\\text{MAD} = \\text{median}(|x - \\text{median}(x)|)\n",
    "$$\n",
    "Πλεονέκτημα: εξαιρετικά **ανθεκτική** σε outliers. Για κανονική κατανομή, η σχέση με την τυπική απόκλιση είναι:\n",
    "$$\n",
    "\\sigma \\approx 1{,}4826 \\times \\text{MAD}\n",
    "$$\n",
    "\n",
    "#### Μέθοδος 3: Gradient-based\n",
    "Ο θόρυβος εμφανίζεται ως υψηλο-συχνοτικές διακυμάνσεις. Άρα η τυπική απόκλιση των διαφορών μεταξύ γειτονικών pixel (`np.diff`) σχετίζεται άμεσα με το επίπεδο θορύβου.\n",
    "\n",
    "### Το SNR και η κλίμακα ποιότητας\n",
    "\n",
    "$$\n",
    "\\text{SNR} = \\dfrac{\\mu_{\\text{signal}}}{\\sigma_{\\text{noise}}}, \\quad \\text{SNR}_{\\text{dB}} = 20 \\log_{10}(\\text{SNR})\n",
    "$$\n",
    "\n",
    "Σε **decibel** (dB) η κλίμακα γίνεται πιο διαισθητική:\n",
    "\n",
    "| SNR (dB) | Ποιότητα |\n",
    "|----------|----------|\n",
    "| < 10 dB | Φτωχή — δύσκολα διαγνωστικά |\n",
    "| 10–20 dB | Αποδεκτή για βασική απεικόνιση |\n",
    "| 20–30 dB | Καλή — διαγνωστική ποιότητα |\n",
    "| > 30 dB | Άριστη — ιδανική για ποσοτική ανάλυση |\n",
    "\n",
    "### Τι παράγει ο κώδικας\n",
    "\n",
    "Έξι οπτικοποιήσεις:\n",
    "\n",
    "1. **Αρχική εικόνα** — αναφορά.\n",
    "2. **Heatmap κλίσης (gradient magnitude)** — εκεί όπου εμφανίζεται **ταυτόχρονα** θόρυβος και πραγματικές ακμές.\n",
    "3. **Ιστόγραμμα κλίσεων** σε λογαριθμική κλίμακα — η ουρά του υποδηλώνει επίπεδο θορύβου.\n",
    "4. **Εικόνα με σημειωμένες τις γωνίες** που χρησιμοποιήθηκαν για εκτίμηση.\n",
    "5. **Πίνακας μετρικών** — όλες οι μετρήσεις σε ένα μέρος.\n",
    "6. **Διάγραμμα ποιότητας** — που πέφτει η εικόνα μας στην παραπάνω κλίμακα.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "58074c99",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                    NOISE ANALYSIS                              ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "Noise in medical images affects:\n",
    "✓ Image quality\n",
    "✓ Diagnostic accuracy\n",
    "✓ Automated analysis algorithms\n",
    "\n",
    "Common types:\n",
    "- Gaussian noise\n",
    "- Salt & pepper noise\n",
    "- Poisson noise\n",
    "\"\"\")\n",
    "\n",
    "def estimate_noise_level(dicom_obj, method='std_uniform_region'):\n",
    "    \"\"\"\n",
    "    Estimate noise level in the image\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        DICOM object\n",
    "    method : str\n",
    "        Method for noise estimation\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    dict : Noise metrics\n",
    "    \"\"\"\n",
    "    \n",
    "    img = dicom_obj.pixel_array.astype(float)\n",
    "    \n",
    "    noise_metrics = {}\n",
    "    \n",
    "    # Method 1: Standard deviation in a uniform region (background)\n",
    "    # Assume corners are relatively uniform background\n",
    "    corner_size = min(img.shape) // 10\n",
    "    corners = [\n",
    "        img[:corner_size, :corner_size],  # Top-left\n",
    "        img[:corner_size, -corner_size:],  # Top-right\n",
    "        img[-corner_size:, :corner_size],  # Bottom-left\n",
    "        img[-corner_size:, -corner_size:]  # Bottom-right\n",
    "    ]\n",
    "    \n",
    "    corner_stds = [corner.std() for corner in corners]\n",
    "    noise_metrics['corner_std_mean'] = np.mean(corner_stds)\n",
    "    noise_metrics['corner_std_min'] = np.min(corner_stds)\n",
    "    \n",
    "    # Method 2: Median Absolute Deviation (MAD) - robust to outliers\n",
    "    median = np.median(img)\n",
    "    mad = np.median(np.abs(img - median))\n",
    "    noise_metrics['mad'] = mad\n",
    "    noise_metrics['estimated_std_from_mad'] = 1.4826 * mad  # Conversion factor for normal distribution\n",
    "    \n",
    "    # Method 3: Gradient-based noise estimation\n",
    "    # Noise often shows up in high-frequency components\n",
    "    grad_x = np.abs(np.diff(img, axis=1))\n",
    "    grad_y = np.abs(np.diff(img, axis=0))\n",
    "    noise_metrics['gradient_std_x'] = grad_x.std()\n",
    "    noise_metrics['gradient_std_y'] = grad_y.std()\n",
    "    \n",
    "    # Signal-to-Noise Ratio\n",
    "    signal = img.mean()\n",
    "    noise = noise_metrics['estimated_std_from_mad']\n",
    "    noise_metrics['snr'] = signal / noise if noise > 0 else float('inf')\n",
    "    noise_metrics['snr_db'] = 20 * np.log10(noise_metrics['snr']) if noise_metrics['snr'] > 0 else float('inf')\n",
    "    \n",
    "    return noise_metrics\n",
    "\n",
    "def visualize_noise_analysis(dicom_obj):\n",
    "    \"\"\"\n",
    "    Visualize noise characteristics\n",
    "    \"\"\"\n",
    "    \n",
    "    img = dicom_obj.pixel_array.astype(float)\n",
    "    \n",
    "    # Estimate noise\n",
    "    noise_metrics = estimate_noise_level(dicom_obj)\n",
    "    \n",
    "    # Compute gradients\n",
    "    grad_x = np.diff(img, axis=1)\n",
    "    grad_y = np.diff(img, axis=0)\n",
    "    grad_magnitude = np.sqrt(grad_x[:-1, :]**2 + grad_y[:, :-1]**2)\n",
    "    \n",
    "    # Create visualization\n",
    "    fig, axes = plt.subplots(2, 3, figsize=(16, 10))\n",
    "    \n",
    "    # Original image\n",
    "    axes[0, 0].imshow(img, cmap='gray')\n",
    "    axes[0, 0].set_title('Original Image', fontweight='bold')\n",
    "    axes[0, 0].axis('off')\n",
    "    \n",
    "    # Gradient magnitude (edge/noise)\n",
    "    im1 = axes[0, 1].imshow(grad_magnitude, cmap='hot')\n",
    "    axes[0, 1].set_title('Gradient Magnitude\\n(Edges + Noise)', fontweight='bold')\n",
    "    axes[0, 1].axis('off')\n",
    "    plt.colorbar(im1, ax=axes[0, 1])\n",
    "    \n",
    "    # Histogram of gradients\n",
    "    axes[0, 2].hist(grad_magnitude.flatten(), bins=50, color='orange', alpha=0.7, edgecolor='black')\n",
    "    axes[0, 2].set_title('Gradient Histogram', fontweight='bold')\n",
    "    axes[0, 2].set_xlabel('Gradient Magnitude')\n",
    "    axes[0, 2].set_ylabel('Frequency')\n",
    "    axes[0, 2].set_yscale('log')\n",
    "    axes[0, 2].grid(True, alpha=0.3)\n",
    "    \n",
    "    # Corner regions for noise estimation\n",
    "    corner_size = min(img.shape) // 10\n",
    "    img_corners = img.copy()\n",
    "    \n",
    "    # Mark corners\n",
    "    from matplotlib.patches import Rectangle\n",
    "    axes[1, 0].imshow(img, cmap='gray')\n",
    "    corners_coords = [\n",
    "        (0, 0), (img.shape[1]-corner_size, 0),\n",
    "        (0, img.shape[0]-corner_size), (img.shape[1]-corner_size, img.shape[0]-corner_size)\n",
    "    ]\n",
    "    \n",
    "    for x, y in corners_coords:\n",
    "        rect = Rectangle((x, y), corner_size, corner_size, \n",
    "                        linewidth=2, edgecolor='red', facecolor='none')\n",
    "        axes[1, 0].add_patch(rect)\n",
    "    \n",
    "    axes[1, 0].set_title('Corner Regions\\n(for noise estimation)', fontweight='bold')\n",
    "    axes[1, 0].axis('off')\n",
    "    \n",
    "    # Noise metrics display\n",
    "    axes[1, 1].axis('off')\n",
    "    metrics_text = f\"\"\"\n",
    "    NOISE METRICS\n",
    "    ══════════════════════════\n",
    "    \n",
    "    Corner STD (mean):\n",
    "      {noise_metrics['corner_std_mean']:.2f}\n",
    "    \n",
    "    MAD (Median Abs Dev):\n",
    "      {noise_metrics['mad']:.2f}\n",
    "    \n",
    "    Estimated Noise (σ):\n",
    "      {noise_metrics['estimated_std_from_mad']:.2f}\n",
    "    \n",
    "    Gradient STD (X):\n",
    "      {noise_metrics['gradient_std_x']:.2f}\n",
    "    \n",
    "    Gradient STD (Y):\n",
    "      {noise_metrics['gradient_std_y']:.2f}\n",
    "    \n",
    "    Signal-to-Noise Ratio:\n",
    "      {noise_metrics['snr']:.2f}\n",
    "    \n",
    "    SNR (dB):\n",
    "      {noise_metrics['snr_db']:.2f} dB\n",
    "    \n",
    "    ══════════════════════════\n",
    "    Higher SNR = Better quality\n",
    "    Typical good SNR > 20 dB\n",
    "    \"\"\"\n",
    "    \n",
    "    axes[1, 1].text(0.1, 0.5, metrics_text, fontsize=9, family='monospace',\n",
    "                   verticalalignment='center',\n",
    "                   bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))\n",
    "    \n",
    "    # SNR visualization\n",
    "    snr_categories = ['Poor\\n(<10 dB)', 'Fair\\n(10-20 dB)', 'Good\\n(20-30 dB)', 'Excellent\\n(>30 dB)']\n",
    "    snr_values = [10, 20, 30, 40]\n",
    "    colors = ['red', 'orange', 'yellow', 'green']\n",
    "    \n",
    "    bars = axes[1, 2].barh(snr_categories, snr_values, color=colors, alpha=0.6, edgecolor='black')\n",
    "    \n",
    "    # Mark current SNR\n",
    "    current_snr = noise_metrics['snr_db']\n",
    "    if current_snr < 10:\n",
    "        marker_pos = 0\n",
    "    elif current_snr < 20:\n",
    "        marker_pos = 1\n",
    "    elif current_snr < 30:\n",
    "        marker_pos = 2\n",
    "    else:\n",
    "        marker_pos = 3\n",
    "    \n",
    "    axes[1, 2].plot([current_snr], [marker_pos], 'r*', markersize=20, \n",
    "                   label=f'Current: {current_snr:.1f} dB')\n",
    "    \n",
    "    axes[1, 2].set_xlabel('SNR (dB)')\n",
    "    axes[1, 2].set_title('SNR Quality Assessment', fontweight='bold')\n",
    "    axes[1, 2].legend()\n",
    "    axes[1, 2].grid(True, alpha=0.3, axis='x')\n",
    "    \n",
    "    plt.suptitle('Comprehensive Noise Analysis', fontsize=16, fontweight='bold')\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "visualize_noise_analysis(dicom_data)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84dee32f",
   "metadata": {},
   "source": [
    "## Ενότητα 34 — Σύνταξη Ολοκληρωμένης Στατιστικής Αναφοράς\n",
    "\n",
    "### Από τα κομμάτια στο σύνολο\n",
    "\n",
    "Μέχρι εδώ, μάθαμε **μεμονωμένες** τεχνικές: ιστογράμματα, στατιστικά, χωρική ανάλυση, θόρυβο. Σε πραγματική κλινική ή ερευνητική χρήση πρέπει να τα συνθέσουμε όλα σε **μία αναφορά** που:\n",
    "\n",
    "- Συγκεντρώνει τα μεταδεδομένα του ασθενούς και της εξέτασης.\n",
    "- Δίνει στατιστικά χαρακτηριστικά της εικόνας.\n",
    "- Αξιολογεί την ποιότητα.\n",
    "- Παρουσιάζει τα παραπάνω σε αναγνώσιμη μορφή για διαφορετικά ακροατήρια (γιατρός, ερευνητής, μηχανικός).\n",
    "\n",
    "### Τι κάνει η `generate_complete_dicom_report`\n",
    "\n",
    "Επιστρέφει ένα **nested dictionary** με τέσσερα τμήματα:\n",
    "\n",
    "1. **`metadata`** — Modality, Patient ID, Study Date, μέγεθος εικόνας, pixel spacing, slice thickness.\n",
    "2. **`basic_stats`** — Όλα τα στατιστικά της Ενότητας 27.\n",
    "3. **`histogram_stats`** — Skewness, kurtosis, CV.\n",
    "4. **`quality_metrics`** — SNR, εκτίμηση θορύβου από MAD.\n",
    "5. **`spatial_stats`** — Brightness, RMS contrast, dynamic range.\n",
    "\n",
    "Ταυτόχρονα παράγει **ένα ολοκληρωμένο γραφικό dashboard** με:\n",
    "\n",
    "- Την εικόνα.\n",
    "- Το ιστόγραμμα με σημειωμένη μέση τιμή και διάμεσο.\n",
    "- Box plot.\n",
    "- Πίνακες μεταδεδομένων, στατιστικών και ποιότητας.\n",
    "- Προφίλ έντασης μέσης γραμμής.\n",
    "\n",
    "> 💡 **Καλή πρακτική κώδικα:** Παρατηρήστε ότι η συνάρτηση δέχεται προαιρετικό `save_path`. Έτσι η ίδια λειτουργία μπορεί να (α) εμφανίσει την αναφορά στο Jupyter για εξερεύνηση, ή (β) να την αποθηκεύσει σε αρχείο για παράδοση. Αυτό λέγεται **separation of concerns** και είναι θεμελιώδες σε επαγγελματικό κώδικα.\n",
    "\n",
    "### Γιατί μας νοιάζει το JSON output\n",
    "\n",
    "Στο τέλος του κελιού βλέπετε `print(json.dumps(report_data, indent=2))`. Γιατί JSON;\n",
    "\n",
    "- **Διαλειτουργικότητα:** Άλλα προγράμματα μπορούν να διαβάσουν το JSON και να επεξεργαστούν τα αποτελέσματα.\n",
    "- **Αρχειοθέτηση:** Σε μελέτες με 1000+ εικόνες, αποθηκεύουμε ένα JSON ανά εικόνα και τα συγκεντρώνουμε σε database.\n",
    "- **Reproducibility:** Ο επόμενος ερευνητής βλέπει ακριβώς τι μετρήθηκε.\n",
    "\n",
    "> 🧠 **Σκέψη για τον φοιτητή:** Σε κάθε δικό σας project, σχεδιάζετε από την αρχή «τι αναφορά θα παράγω;». Αυτό συχνά διαμορφώνει σωστά και τον υπόλοιπο κώδικα.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b7fcf158",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║          GENERATING COMPLETE STATISTICAL REPORT                ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\"\"\")\n",
    "\n",
    "def generate_complete_dicom_report(dicom_obj, save_path=None):\n",
    "    \"\"\"\n",
    "    Generate a complete statistical and quality report for a DICOM image\n",
    "    \n",
    "    Parameters:\n",
    "    -----------\n",
    "    dicom_obj : pydicom.dataset.FileDataset\n",
    "        DICOM object to analyze\n",
    "    save_path : str, optional\n",
    "        Path to save the report\n",
    "    \n",
    "    Returns:\n",
    "    --------\n",
    "    dict : Complete report data\n",
    "    \"\"\"\n",
    "    \n",
    "    img = dicom_obj.pixel_array\n",
    "    \n",
    "    # Collect all analyses\n",
    "    report = {\n",
    "        'metadata': {},\n",
    "        'basic_stats': {},\n",
    "        'histogram_stats': {},\n",
    "        'quality_metrics': {},\n",
    "        'spatial_stats': {}\n",
    "    }\n",
    "    \n",
    "    # Metadata\n",
    "    report['metadata'] = {\n",
    "        'Modality': dicom_obj.get('Modality', 'N/A'),\n",
    "        'Patient_ID': dicom_obj.get('PatientID', 'N/A'),\n",
    "        'Study_Date': dicom_obj.get('StudyDate', 'N/A'),\n",
    "        'Image_Size': f\"{dicom_obj.Rows} x {dicom_obj.Columns}\",\n",
    "        'Pixel_Spacing': str(dicom_obj.get('PixelSpacing', 'N/A')),\n",
    "        'Slice_Thickness': str(dicom_obj.get('SliceThickness', 'N/A'))\n",
    "    }\n",
    "    \n",
    "    # Basic statistics\n",
    "    flat_img = img.flatten()\n",
    "    report['basic_stats'] = {\n",
    "        'Mean': float(flat_img.mean()),\n",
    "        'Median': float(np.median(flat_img)),\n",
    "        'Std_Dev': float(flat_img.std()),\n",
    "        'Variance': float(flat_img.var()),\n",
    "        'Min': float(flat_img.min()),\n",
    "        'Max': float(flat_img.max()),\n",
    "        'Range': float(np.ptp(flat_img)),\n",
    "        'Q1': float(np.percentile(flat_img, 25)),\n",
    "        'Q3': float(np.percentile(flat_img, 75)),\n",
    "        'IQR': float(np.percentile(flat_img, 75) - np.percentile(flat_img, 25))\n",
    "    }\n",
    "    \n",
    "    # Histogram statistics\n",
    "    from scipy import stats as scipy_stats\n",
    "    report['histogram_stats'] = {\n",
    "        'Skewness': float(scipy_stats.skew(flat_img)),\n",
    "        'Kurtosis': float(scipy_stats.kurtosis(flat_img)),\n",
    "        'CV_Percent': float((flat_img.std() / flat_img.mean()) * 100) if flat_img.mean() != 0 else 0,\n",
    "    }\n",
    "    \n",
    "    # Quality metrics\n",
    "    noise_metrics = estimate_noise_level(dicom_obj)\n",
    "    report['quality_metrics'] = {\n",
    "        'SNR': float(noise_metrics['snr']),\n",
    "        'SNR_dB': float(noise_metrics['snr_db']),\n",
    "        'Estimated_Noise_Level': float(noise_metrics['estimated_std_from_mad']),\n",
    "        'MAD': float(noise_metrics['mad'])\n",
    "    }\n",
    "    \n",
    "    # Spatial statistics\n",
    "    img_norm = (img - img.min()) / (img.max() - img.min())\n",
    "    report['spatial_stats'] = {\n",
    "        'Brightness': float(img_norm.mean()),\n",
    "        'Contrast_RMS': float(img_norm.std()),\n",
    "        'Dynamic_Range': float(np.ptp(img_norm))\n",
    "    }\n",
    "    \n",
    "    # Create comprehensive visualization\n",
    "    fig = plt.figure(figsize=(18, 14))\n",
    "    gs = fig.add_gridspec(4, 3, hspace=0.35, wspace=0.3)\n",
    "    \n",
    "    # 1. Main image\n",
    "    ax1 = fig.add_subplot(gs[0:2, 0:2])\n",
    "    ax1.imshow(img, cmap='gray')\n",
    "    ax1.set_title(f'DICOM Image - {report[\"metadata\"][\"Modality\"]}', \n",
    "                 fontsize=14, fontweight='bold')\n",
    "    ax1.axis('off')\n",
    "    \n",
    "    # 2. Histogram\n",
    "    ax2 = fig.add_subplot(gs[0, 2])\n",
    "    ax2.hist(flat_img, bins=50, color='steelblue', alpha=0.7, edgecolor='black')\n",
    "    ax2.axvline(report['basic_stats']['Mean'], color='red', linestyle='--', \n",
    "               linewidth=2, label=f\"Mean: {report['basic_stats']['Mean']:.1f}\")\n",
    "    ax2.axvline(report['basic_stats']['Median'], color='green', linestyle='--',\n",
    "               linewidth=2, label=f\"Median: {report['basic_stats']['Median']:.1f}\")\n",
    "    ax2.set_title('Intensity Distribution', fontsize=10, fontweight='bold')\n",
    "    ax2.legend(fontsize=8)\n",
    "    ax2.grid(True, alpha=0.3)\n",
    "    \n",
    "    # 3. Box plot\n",
    "    ax3 = fig.add_subplot(gs[1, 2])\n",
    "    ax3.boxplot(flat_img, vert=True, patch_artist=True,\n",
    "               boxprops=dict(facecolor='lightblue'))\n",
    "    ax3.set_title('Box Plot', fontsize=10, fontweight='bold')\n",
    "    ax3.set_ylabel('Intensity')\n",
    "    ax3.grid(True, alpha=0.3, axis='y')\n",
    "    \n",
    "    # 4. Metadata table\n",
    "    ax4 = fig.add_subplot(gs[2, 0])\n",
    "    ax4.axis('off')\n",
    "    metadata_text = \"METADATA\\n\" + \"─\"*30 + \"\\n\"\n",
    "    for key, value in report['metadata'].items():\n",
    "        metadata_text += f\"{key.replace('_', ' ')}: {value}\\n\"\n",
    "    ax4.text(0.1, 0.5, metadata_text, fontsize=9, family='monospace',\n",
    "            verticalalignment='center',\n",
    "            bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.7))\n",
    "    \n",
    "    # 5. Basic statistics table\n",
    "    ax5 = fig.add_subplot(gs[2, 1])\n",
    "    ax5.axis('off')\n",
    "    stats_text = \"BASIC STATISTICS\\n\" + \"─\"*30 + \"\\n\"\n",
    "    for key, value in report['basic_stats'].items():\n",
    "        stats_text += f\"{key.replace('_', ' ')}: {value:.2f}\\n\"\n",
    "    ax5.text(0.1, 0.5, stats_text, fontsize=9, family='monospace',\n",
    "            verticalalignment='center',\n",
    "            bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))\n",
    "    \n",
    "    # 6. Quality metrics table\n",
    "    ax6 = fig.add_subplot(gs[2, 2])\n",
    "    ax6.axis('off')\n",
    "    quality_text = \"QUALITY METRICS\\n\" + \"─\"*30 + \"\\n\"\n",
    "    for key, value in report['quality_metrics'].items():\n",
    "        quality_text += f\"{key.replace('_', ' ')}: {value:.2f}\\n\"\n",
    "    ax6.text(0.1, 0.5, quality_text, fontsize=9, family='monospace',\n",
    "            verticalalignment='center',\n",
    "            bbox=dict(boxstyle='round', facecolor='lightcoral', alpha=0.7))\n",
    "    \n",
    "    # 7. Intensity profile\n",
    "    ax7 = fig.add_subplot(gs[3, :])\n",
    "    mid_row = img.shape[0] // 2\n",
    "    profile = img[mid_row, :]\n",
    "    ax7.plot(profile, linewidth=2, color='darkblue')\n",
    "    ax7.fill_between(range(len(profile)), profile, alpha=0.3)\n",
    "    ax7.set_title(f'Intensity Profile (Row {mid_row})', fontsize=10, fontweight='bold')\n",
    "    ax7.set_xlabel('Column Position')\n",
    "    ax7.set_ylabel('Intensity')\n",
    "    ax7.grid(True, alpha=0.3)\n",
    "    \n",
    "    # Overall title\n",
    "    plt.suptitle('COMPREHENSIVE DICOM IMAGE ANALYSIS REPORT', \n",
    "                fontsize=18, fontweight='bold', y=0.98)\n",
    "    \n",
    "    if save_path:\n",
    "        plt.savefig(save_path, dpi=150, bbox_inches='tight')\n",
    "        print(f\"✓ Report saved to: {save_path}\")\n",
    "    \n",
    "    plt.show()\n",
    "    \n",
    "    return report\n",
    "\n",
    "# Generate complete report\n",
    "print(\"\\n\" + \"=\"*70)\n",
    "print(\"GENERATING COMPREHENSIVE REPORT...\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "report_data = generate_complete_dicom_report(dicom_data)\n",
    "\n",
    "# Print summary\n",
    "print(\"\\n📋 REPORT SUMMARY:\")\n",
    "print(json.dumps(report_data, indent=2))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9a0a7dcc",
   "metadata": {},
   "source": [
    "## Ενότητα 35 — Ασκήσεις Πρακτικής Εξάσκησης\n",
    "\n",
    "### Γιατί ασκήσεις;\n",
    "\n",
    "Δεν μαθαίνεται η ανάλυση εικόνων διαβάζοντας. Μαθαίνεται **τρέχοντας κώδικα, σπάζοντάς τον, και διορθώνοντας τα σφάλματα**. Οι παρακάτω οκτώ ασκήσεις είναι σχεδιασμένες ώστε να καλύπτουν διαφορετικές πτυχές αυτού που μάθαμε:\n",
    "\n",
    "| # | Άσκηση | Τι εξασκεί |\n",
    "|---|--------|-----------|\n",
    "| 1 | Στατιστική Ανάλυση | Βασική ποσοτικοποίηση + έλεγχος κανονικότητας |\n",
    "| 2 | Manipulation Ιστογράμματος | Επίδραση παραμέτρων στην ερμηνεία |\n",
    "| 3 | Region Analysis | Χωρική ανάλυση + heatmaps |\n",
    "| 4 | Noise Assessment | Κριτική σύγκριση μεθόδων |\n",
    "| 5 | Profile Analysis | Μέτρηση σε φυσικές μονάδες (mm) |\n",
    "| 6 | Comparative Analysis | Σύγκριση modalities |\n",
    "| 7 | Quality Control | Σχεδιασμός pipeline |\n",
    "| 8 | Advanced Visualization | Σύνθεση παρουσίασης |\n",
    "\n",
    "### Συμβουλές για να δουλέψετε σωστά\n",
    "\n",
    "> 📌 **Πάντοτε:**\n",
    "> 1. **Επικυρώνετε τα δεδομένα εισόδου** — το πρώτο πράγμα που πρέπει να ελέγχετε είναι αν το DICOM φορτώθηκε σωστά (`dicom_obj.pixel_array.shape`, `dicom_obj.Modality`).\n",
    "> 2. **Σχολιάζετε τον κώδικα** — γράψτε τι κάνετε ΚΑΙ γιατί. Σε έναν μήνα δεν θα το θυμάστε.\n",
    "> 3. **Συγκρίνετε με αναμενόμενες τιμές** — π.χ. αν η μέση τιμή ενός CT φόντου σας βγει 500, κάτι πάει στραβά (φόντο = αέρας ≈ −1000 HU).\n",
    "> 4. **Οπτικοποιείτε ενδιάμεσα βήματα** — μην αφήνετε τίποτα ως «μαύρο κουτί».\n",
    "> 5. **Σκέφτεστε την κλινική σημασία** — δεν φτιάχνουμε γραφήματα για τα γραφήματα. Πάντα ρωτάτε «τι θα έλεγε ένας γιατρός βλέποντας αυτό;».\n",
    "\n",
    "### Πώς να παραδώσετε\n",
    "\n",
    "Σε κάθε άσκηση ζητείται:\n",
    "- **Λειτουργικός κώδικας** σε notebook ή script.\n",
    "- **Σχόλια** που εξηγούν τη λογική.\n",
    "- **Ποιοτική ερμηνεία** των αποτελεσμάτων (όχι μόνο αριθμοί — τι σημαίνουν).\n",
    "\n",
    "> 💡 Η καλύτερη παράδοση είναι αυτή που, αν τη δει κάποιος που δεν ξέρει DICOM, θα την καταλάβει πλήρως.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "99caa478",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║              ENHANCED PRACTICE EXERCISES                       ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "📝 EXERCISE 1: Statistical Analysis\n",
    "   Task: Load a DICOM image and:\n",
    "   a) Calculate all central tendency measures (mean, median, mode)\n",
    "   b) Calculate all dispersion measures (std, variance, range, IQR)\n",
    "   c) Determine if the distribution is normal (using Q-Q plot)\n",
    "   d) Identify and explain any outliers\n",
    "\n",
    "📝 EXERCISE 2: Histogram Manipulation\n",
    "   Task: \n",
    "   a) Create histograms with different bin sizes (10, 50, 100, 200)\n",
    "   b) Compare how bin size affects interpretation\n",
    "   c) Apply histogram equalization\n",
    "   d) Compare before/after statistics\n",
    "\n",
    "📝 EXERCISE 3: Region Analysis\n",
    "   Task:\n",
    "   a) Divide image into 4x4 grid\n",
    "   b) Calculate mean and std for each region\n",
    "   c) Create heatmaps showing spatial variation\n",
    "   d) Identify regions with highest/lowest contrast\n",
    "\n",
    "📝 EXERCISE 4: Noise Assessment\n",
    "   Task:\n",
    "   a) Estimate noise level using multiple methods\n",
    "   b) Calculate SNR\n",
    "   c) Compare corner regions for noise uniformity\n",
    "   d) Suggest if image quality is adequate\n",
    "\n",
    "📝 EXERCISE 5: Profile Analysis\n",
    "   Task:\n",
    "   a) Extract horizontal, vertical, and diagonal profiles\n",
    "   b) Calculate gradients (rate of change)\n",
    "   c) Identify edges and transitions\n",
    "   d) Measure distances in physical units (mm)\n",
    "\n",
    "📝 EXERCISE 6: Comparative Analysis\n",
    "   Task:\n",
    "   a) Load 3 different DICOM images (different modalities if possible)\n",
    "   b) Create comparative statistics table\n",
    "   c) Generate histograms side-by-side\n",
    "   d) Explain differences based on modality\n",
    "\n",
    "📝 EXERCISE 7: Quality Control\n",
    "   Task:\n",
    "   a) Create a quality control pipeline\n",
    "   b) Check for: artifacts, proper contrast, noise levels\n",
    "   c) Generate automatic quality report\n",
    "   d) Flag images that fail quality criteria\n",
    "\n",
    "📝 EXERCISE 8: Advanced Visualization\n",
    "   Task:\n",
    "   a) Create a dashboard with: image, histogram, statistics, profiles\n",
    "   b) Add interactive elements (if possible)\n",
    "   c) Include both spatial and statistical views\n",
    "   d) Export as a report\n",
    "\n",
    "💡 TIPS FOR EXERCISES:\n",
    "   - Always validate your inputs\n",
    "   - Document your code with comments\n",
    "   - Compare results with expected values\n",
    "   - Visualize intermediate steps\n",
    "   - Think about clinical relevance\n",
    "\"\"\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "90fc12d6",
   "metadata": {},
   "source": [
    "## Ενότητα 36 — Συνοπτικός Οδηγός Αναφοράς\n",
    "\n",
    "### Τι είναι αυτή η ενότητα;\n",
    "\n",
    "Ένα **cheat sheet** που μπορείτε να έχετε δίπλα σας όταν προγραμματίζετε. Μαζευτήκαμε σε ένα μέρος όλες τις βασικές συναρτήσεις που χρησιμοποιήσαμε — οργανωμένες ανά κατηγορία.\n",
    "\n",
    "### Πώς να το χρησιμοποιείτε\n",
    "\n",
    "- **Πρώτη γραμμή άμυνας:** Όταν δεν θυμάστε πώς υπολογίζεται κάτι, κοιτάτε εδώ πριν ψάξετε στο Google.\n",
    "- **Reference για ασκήσεις και projects:** Αντιγράφετε τα μοτίβα και τα προσαρμόζετε.\n",
    "- **Σύνδεση εννοιών με κώδικα:** Κάθε στατιστική έννοια (μέση τιμή, IQR, kurtosis...) έχει τη δική της κλήση συνάρτησης.\n",
    "\n",
    "### Οι κατηγορίες\n",
    "\n",
    "| Κατηγορία | Τι περιλαμβάνει |\n",
    "|-----------|------------------|\n",
    "| **Central Tendency** | mean, median, mode |\n",
    "| **Dispersion** | std, variance, range, IQR, CV |\n",
    "| **Percentiles** | quartiles και custom percentiles |\n",
    "| **Distribution Shape** | skewness, kurtosis |\n",
    "| **Histogram** | basic, normalized, cumulative |\n",
    "| **Image Quality** | SNR (linear & dB), contrast, brightness |\n",
    "| **DICOM Access** | direct, safe (`.get`), tag access, pixel access |\n",
    "| **Common Operations** | normalize, window, threshold, gradient |\n",
    "\n",
    "### Επόμενα βήματα μετά αυτό το μάθημα\n",
    "\n",
    "Έχετε πλέον τις βάσεις για να εξερευνήσετε:\n",
    "\n",
    "1. **Segmentation** — αυτόματη οριοθέτηση οργάνων ή βλαβών.\n",
    "2. **Registration** — ευθυγράμμιση εικόνων διαφορετικών εξετάσεων.\n",
    "3. **Radiomics** — εξαγωγή εκατοντάδων χαρακτηριστικών για ML.\n",
    "4. **Deep Learning σε ιατρικές εικόνες** — CNN, U-Net, transformers.\n",
    "5. **Multi-modal ανάλυση** — συνδυασμός CT/MRI/PET.\n",
    "\n",
    "> 🎓 **Τελική σκέψη:** Η ανάλυση ιατρικών εικόνων είναι μια από τις περιοχές της AI με μεγαλύτερο **κλινικό και ηθικό βάρος**. Αυτό που σήμερα μαθαίνετε ως «παιχνίδι με αριθμούς» μπορεί αύριο να επηρεάζει διαγνώσεις. Πάντα να σκέφτεστε: σωστά δεδομένα, σωστές παραδοχές, διαφανής αναφορά.\n",
    "\n",
    "🏥 **Καλή ανάλυση!**\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a821c3c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                  QUICK REFERENCE GUIDE                         ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "📚 STATISTICAL MEASURES QUICK REFERENCE:\n",
    "\n",
    "CENTRAL TENDENCY:\n",
    "├─ Mean:     np.mean(array)\n",
    "├─ Median:   np.median(array)\n",
    "└─ Mode:     scipy.stats.mode(array)\n",
    "\n",
    "DISPERSION:\n",
    "├─ Std Dev:  np.std(array)\n",
    "├─ Variance: np.var(array)\n",
    "├─ Range:    np.ptp(array)  # peak-to-peak\n",
    "├─ IQR:      Q3 - Q1\n",
    "└─ CV:       (std/mean) * 100\n",
    "\n",
    "PERCENTILES:\n",
    "├─ Quartiles: np.percentile(array, [25, 50, 75])\n",
    "├─ Custom:    np.percentile(array, p)\n",
    "└─ Quantile:  np.quantile(array, q)\n",
    "\n",
    "DISTRIBUTION SHAPE:\n",
    "├─ Skewness: scipy.stats.skew(array)\n",
    "└─ Kurtosis: scipy.stats.kurtosis(array)\n",
    "\n",
    "HISTOGRAM:\n",
    "├─ Basic:    plt.hist(array, bins=50)\n",
    "├─ Normed:   plt.hist(array, density=True)\n",
    "└─ Cumul:    np.cumsum(counts)\n",
    "\n",
    "IMAGE QUALITY:\n",
    "├─ SNR:      mean / std\n",
    "├─ SNR (dB): 20 * log10(SNR)\n",
    "├─ Contrast: std of normalized image\n",
    "└─ Bright:   mean of normalized image\n",
    "\n",
    "DICOM ACCESS:\n",
    "├─ Direct:   dicom_obj.TagName\n",
    "├─ Safe:     dicom_obj.get('TagName', default)\n",
    "├─ Tag num:  dicom_obj[0x0008, 0x0060].value\n",
    "└─ Pixel:    dicom_obj.pixel_array\n",
    "\n",
    "COMMON OPERATIONS:\n",
    "├─ Normalize:  (img - min) / (max - min)\n",
    "├─ Window:     np.clip(img, wc-ww/2, wc+ww/2)\n",
    "├─ Threshold:  img > threshold\n",
    "└─ Gradient:   np.gradient(img)\n",
    "\"\"\")\n",
    "\n",
    "print(\"\"\"\n",
    "╔════════════════════════════════════════════════════════════════╗\n",
    "║                    TUTORIAL COMPLETE!                          ║\n",
    "╚════════════════════════════════════════════════════════════════╝\n",
    "\n",
    "🎉 Congratulations! You now have comprehensive knowledge of:\n",
    "\n",
    "✓ DICOM image fundamentals\n",
    "✓ Statistical analysis techniques\n",
    "✓ Histogram interpretation and manipulation\n",
    "✓ Quality assessment methods\n",
    "✓ Noise analysis\n",
    "✓ Contrast and brightness evaluation\n",
    "✓ Region-based analysis\n",
    "✓ Profile analysis\n",
    "✓ Complete reporting\n",
    "\n",
    "🚀 NEXT STEPS:\n",
    "1. Practice with real DICOM datasets\n",
    "2. Experiment with different modalities\n",
    "3. Build your own analysis tools\n",
    "4. Explore advanced topics (segmentation, registration, etc.)\n",
    "5. Apply these techniques to research projects\n",
    "\n",
    "📖 Keep this notebook as a reference for future work!\n",
    "\n",
    "Happy analyzing! 🏥💻📊\n",
    "\"\"\")\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
