diff --git a/Docs/pdf_combiner.md b/Docs/pdf_combiner.md new file mode 100644 index 0000000..6620f6c --- /dev/null +++ b/Docs/pdf_combiner.md @@ -0,0 +1,60 @@ +## 🖼️ _Generazione PDF da Immagini JPEG_ - **pdf_combiner.py** + +Uno script Python con interfaccia grafica che consente di selezionare immagini JPEG e convertirle in un **PDF ottimizzato**, pronto per l’archiviazione o la stampa. + +--- + +## Descrizione 📄 + +L'**Elaboratore di Documenti Scansionati** permette di importare una o più immagini, migliorarne automaticamente la leggibilità e generare un PDF ordinato e pulito. + +Ideale per: + +- **Digitalizzare documenti cartacei** in modo rapido. +- **Migliorare la leggibilità** di scansioni non perfette. +- **Creare archivi PDF** partendo da immagini di bassa qualità. + +--- + +## Funzionalità 🌟 + +- **Correzione orientamento**: Rileva automaticamente la rotazione delle pagine. +- **Pulizia avanzata**: Rimuove bordi, rumore e migliora la leggibilità del testo. +- **Conversione bianco/nero**: Per un output chiaro, leggibile e leggero. +- **Interfaccia intuitiva**: Selezione immagini con un semplice click. +- **Output ordinato**: Salva un PDF nella cartella delle immagini, con nome e data. + +--- + +### Esempio di utilizzo 🧪 + +1. Avvia lo script. +2. Seleziona una o più immagini `.jpeg` o `.jpg`. +3. Clicca su **"Elabora Documenti"**. +4. Attendi la fine del processo: il PDF sarà generato automaticamente. + +--- + +## Output 📊 + +### Output PDF + +- Nome file: `Documenti_Scansionati_YYYYMMDD_HHMMSS.pdf` +- Posizione: stessa cartella delle immagini originali. + +### Output Terminale / Log + +```plaintext +Immagine 1/3: Analisi immagine... +Immagine 1/3: Correzione orientamento... +Immagine 1/3: Conversione in bianco e nero... +... +Creazione PDF... +Elaborazione completata! +``` + +## Note 📝 + +- Compatibilità: Supporta immagini .jpeg, .jpg e .png. +- Performance: Il processo potrebbe richiedere alcuni secondi per immagine. +- Nitidezza & Contrasto: Le funzioni di aumento sono disabilitate per migliorare la leggibilità del testo. diff --git a/README.md b/README.md index 553385b..5579e81 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@


- versione + versione last-commit repo-top-language repo-language-count @@ -26,15 +26,19 @@ ## 👾 Features -**Versione 3.0** - Nuovo approccio all'esecuzione: non si esegue più uno script isolato, ma tramite un unico punto d’ingresso interattivo. +**Experimental Branch** - Ramo Sperimentale pubblico. + +> **ATTENZIONE** - Questo ramo è da interdersi come sviluppo attivo, è sconsigliato utilizzare la versione degli script presenti in questo ramo in quanto potrebbero avere problemi di funzionamento o non avviarsi del tutto. **Consigliato l'utilizzo a personale esperto.** -Introdotto il file `main.py` che gestisce: +**Versione 3.0** - Nuovo approccio all'esecuzione: non si esegue più uno script isolato, ma tramite un unico punto d’ingresso interattivo. -- L'installazione automatica delle dipendenze dal file `requirements.txt`. -- Un menu interattivo per scegliere quale script eseguire. -- Organizzazione automatica degli script in ordine alfabetico. -- Breve descrizione affiancata a ciascuno script. -- Migliore gestione e chiarezza degli strumenti disponibili. +> Introdotto il file `main.py` che gestisce: +> +> - L'installazione automatica delle dipendenze dal file `requirements.txt`. +> - Un menu interattivo per scegliere quale script eseguire. +> - Organizzazione automatica degli script in ordine alfabetico. +> - Breve descrizione affiancata a ciascuno script. +> - Migliore gestione e chiarezza degli strumenti disponibili. --- @@ -49,7 +53,7 @@ Introdotto il file `main.py` che gestisce: ├── README.md ├── install.ps1 ├── main.py - ├── Docs + ├── Docs ├── pipreqs-config.toml ├── requirements.txt └── scripts @@ -65,6 +69,7 @@ Introdotto il file `main.py` che gestisce: ├── remove.py ├── sposta_file.py ├── transcribe_wav.py + ├── pdf_combiner.py └── trash.py ``` @@ -148,6 +153,11 @@ Introdotto il file `main.py` che gestisce: estensioni.py ❯ Analisi e elenco ordinato delle estensioni file in una directory. + + + pdf_combiner.py + ❯ Genera PDF da immagini Jpeg. + @@ -175,7 +185,7 @@ Utilizza Python-Script seguendo questi step: ❯ git clone https://github.com/Magnetarman/Python-Script ``` -2. utilizza il terminale per Navigare fino alla cartella: +2. Utilizza il terminale per Navigare fino alla cartella: ```sh ❯ cd Python-Script @@ -187,14 +197,38 @@ Utilizza Python-Script seguendo questi step: ❯ py main.py ``` +### ⚙️ Installatione Alternativa + +1. Clona la repository the Python-Script: + +```sh +❯ git clone https://github.com/Magnetarman/Python-Script +``` + +2. Avvia il terminale in **modalita amministratore**, Naviga fino alla cartella:: + +```sh +❯ cd Python-Script +``` + +3. Avvia lo script `install.ps1`: + +```sh +❯ ./install.ps1 +``` + +> Lo script `install.ps1` avviato installerà Python e dipendenze minimali. Successivamente lo scipt si occuperà di avviare automaticamente il `main.py` per utilizzare gli script disponibili. + --- ## 📌 Roadmap - [x] **`V 3.0`**: Creazione 'main.py'. - [x] **`V 3.1`**: Refactor 'Readme.md'. Creazione Cartella 'Docs' con la documentazione di ogni script. -- [ ] **`V 3.2`**: Automatizzare il processo di installazione di Python e dipendenze al 100%. -- [ ] **`V 4.0`**: unificare il tutto in un unico'main.py' con aggiunta di Interfaccia grafica. +- [x] **`V 3.1.1`**: Aggiunta Script PDF Combiner in versione Stabile. +- [x] **`V 3.1.2`**: il wrapper `main.py` aggiunge automaticamente i nuovi script all'avvio. +- [x] **`V 3.2`**: Automatizzare il processo di installazione di Python e dipendenze al 100%. +- [ ] **`V 4.0`**: unificare il tutto in un unico 'main.py' con aggiunta di Interfaccia grafica. --- @@ -229,16 +263,6 @@ Utilizza Python-Script seguendo questi step: 8. **Revisione**: Una volta che la tua PR sarà revisionata e approvata, verrà unita ("merged") nel branch principale. Congratulazioni per il tuo contributo! -

-Contributor Graph -
-

- - - -

-
- --- ## 🎗 Licenza diff --git a/install.ps1 b/install.ps1 index 588f6b7..9b54474 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,63 +1,227 @@ -# Funzione per verificare se PowerShell è in modalità amministratore +# Funzione per verificare privilegi amministratore function Check-Admin { - $isAdmin = [bool]([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - return $isAdmin + return [bool]([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } -# Se PowerShell non è in modalità amministratore, chiedi i privilegi di amministratore +# Richiedi privilegi amministratore se necessario if (-not (Check-Admin)) { Write-Host "PowerShell non è in modalità amministratore. Avvio con privilegi di amministratore..." - - # Crea un nuovo processo PowerShell con privilegi di amministratore Start-Process powershell -ArgumentList "Start-Process powershell -Verb runAs -ArgumentList '$PSCommandPath'" -Verb runAs exit } -# Funzione per verificare se una versione specifica di Python è installata -function Check-PythonVersion { - param ( - [string]$version - ) - - $pythonPath = Get-Command "python$version" -ErrorAction SilentlyContinue - return $pythonPath -ne $null +# Funzione per ottenere versioni Python disponibili +function Get-AvailablePythonVersions { + $versions = @() + try { + $output = py -0 2>&1 + if ($output) { + $versions = $output | Where-Object { $_ -match '-(\d+\.\d+)' } | ForEach-Object { $matches[1] } | Sort-Object -Unique + } + + # Fallback se py -0 non funziona + if ($versions.Count -eq 0) { + @("3.12", "3.11", "3.10", "3.9", "3.8") | ForEach-Object { + try { + if ((py -$_ --version 2>$null) -match "Python $_") { $versions += $_ } + } catch { } + } + } + } catch { } + return $versions } # Funzione per installare Python tramite winget -function Install-Python { - param ( - [string]$version - ) +function Install-PythonIfNeeded { + param([string]$version) Write-Host "Installando Python $version tramite winget..." - switch ($version) { - "3.10" { winget install --id Python.Python.3.10 -e --source winget } - default { Write-Host "Versione Python non supportata. Procedo con Python.Launcher." } + try { + $packageId = switch ($version) { + "3.10" { "Python.Python.3.10" } + "3.11" { "Python.Python.3.11" } + "3.12" { "Python.Python.3.12" } + default { return $false } + } + + winget install --id $packageId -e --source winget --accept-source-agreements --accept-package-agreements --silent | Out-Null + Start-Sleep -Seconds 5 + + $result = py -$version --version 2>$null + if ($result -match "Python $version") { + Write-Host "Python $version installato con successo." + return $true + } + } catch { } + + Write-Host "Installazione di Python $version fallita." + return $false +} + +# Funzione per aggiungere Python al PATH +function Add-PythonToPath { + param([string]$pythonVersion) + + Write-Host "Configurando PATH per Python $pythonVersion..." + try { + $pythonExe = py -$pythonVersion -c "import sys; print(sys.executable)" 2>$null + if (-not $pythonExe) { return $false } + + $pythonPath = Split-Path $pythonExe + $scriptsPath = Join-Path $pythonPath "Scripts" + $currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") + $pathsToAdd = @($pythonPath, $scriptsPath) | Where-Object { $currentPath -notlike "*$_*" } + + if ($pathsToAdd.Count -gt 0) { + $newPath = ($currentPath, $pathsToAdd) -join ";" + [Environment]::SetEnvironmentVariable("PATH", $newPath, "User") + $env:PATH = $newPath + Write-Host "PATH aggiornato: $($pathsToAdd -join ', ')" + return $true + } else { + Write-Host "PATH già configurato correttamente." + return $true + } + } catch { + Write-Host "Errore configurazione PATH: $($_.Exception.Message)" + return $false } } -# Controllo e installazione per Python 3.10 -$pythonVersion = "3.10" -if (-not (Check-PythonVersion -version $pythonVersion)) { - Install-Python -version $pythonVersion +# Funzione per installare dipendenze +function Install-Dependencies { + param([string]$pythonVersion) + + # Verifica disponibilità versione + if (-not ((py -$pythonVersion --version 2>$null) -match "Python $pythonVersion")) { + Write-Host "ERRORE: Python $pythonVersion non disponibile!" + return $false + } + + Write-Host "Installando dipendenze con Python $pythonVersion..." + + # Aggiorna pip + try { + py -$pythonVersion -m pip install --upgrade pip --quiet | Out-Null + Write-Host "Pip aggiornato." + } catch { + Write-Host "Warning: Errore aggiornamento pip." + } + + $scriptDir = Get-Location + $requirementsFile = Join-Path $scriptDir "requirements.txt" + + if (Test-Path $requirementsFile) { + Write-Host "Installando da requirements.txt..." + py -$pythonVersion -m pip install -r $requirementsFile + + if ($LASTEXITCODE -eq 0) { + Write-Host "Dipendenze installate con successo." + return $true + } else { + # Installazione individuale come fallback + Write-Host "Tentativo installazione individuale..." + $success = $true + Get-Content $requirementsFile | Where-Object { $_.Trim() -and -not $_.StartsWith("#") } | ForEach-Object { + Write-Host "Installando: $_" + py -$pythonVersion -m pip install $_ --quiet + if ($LASTEXITCODE -ne 0) { $success = $false } + } + return $success + } + } else { + Write-Host "Installando librerie comuni..." + $libs = @("numpy", "pandas", "matplotlib", "requests") + $success = $true + + $libs | ForEach-Object { + Write-Host "Installando $_..." + py -$pythonVersion -m pip install $_ --quiet + if ($LASTEXITCODE -ne 0) { + Write-Host "Errore con $_" + $success = $false + } + } + return $success + } +} + +# === ESECUZIONE PRINCIPALE === +Write-Host "=== SETUP PYTHON E DIPENDENZE ===" + +# Rileva versioni disponibili +Write-Host "Rilevamento versioni Python..." +$availableVersions = Get-AvailablePythonVersions + +if ($availableVersions.Count -gt 0) { + Write-Host "Versioni trovate: $($availableVersions -join ', ')" } else { - Write-Host "Python $pythonVersion è già installato." + Write-Host "Nessuna versione trovata. Installando Python..." + winget install --id Python.Launcher -e --source winget --accept-source-agreements --accept-package-agreements --silent | Out-Null + + # Installa 3.11 come priorità + if (Install-PythonIfNeeded -version "3.11") { $availableVersions += "3.11" } + elseif (Install-PythonIfNeeded -version "3.10") { $availableVersions += "3.10" } } -# Visualizzare il messaggio "Attendere..." per 2 secondi -Write-Host "Attendere..." -Start-Sleep -Seconds 2 +# Installa versioni target se mancanti +@("3.11", "3.12") | Where-Object { $_ -notin $availableVersions } | ForEach-Object { + if (Install-PythonIfNeeded -version $_) { $availableVersions += $_ } +} -# Ottieni il percorso della cartella in cui è stato eseguito lo script PowerShell -$scriptDir = Get-Location +# Ri-rileva versioni finali +$finalVersions = Get-AvailablePythonVersions +Write-Host "Versioni finali: $($finalVersions -join ', ')" -# Verifica se il file main.py esiste nella stessa cartella dello script PowerShell -$mainScript = Join-Path $scriptDir "main.py" -if (Test-Path $mainScript) { - Write-Host "Eseguo main.py..." +# Seleziona versione ottimale +$versionToUse = @("3.11", "3.12", "3.10", "3.9", "3.8") | Where-Object { $_ -in $finalVersions } | Select-Object -First 1 - # Esegui main.py nella stessa finestra di PowerShell - py $mainScript +if ($versionToUse) { + Write-Host "Usando Python $versionToUse per dipendenze..." + $installResult = Install-Dependencies -pythonVersion $versionToUse + + # Configura PATH + Add-PythonToPath -pythonVersion $versionToUse | Out-Null + + Write-Host "Attendere..." + Start-Sleep -Seconds 2 + + # === ESECUZIONE MAIN.PY === + $scriptDir = Get-Location + $mainScript = Join-Path $scriptDir "main.py" + + Write-Host "Verifica main.py in: $scriptDir" + + if (Test-Path $mainScript) { + Write-Host "Trovato main.py. Eseguendo con Python $versionToUse..." + + # Verifica ed esegui + try { + if ((py -$versionToUse --version 2>$null) -match "Python $versionToUse") { + py -$versionToUse $mainScript + } else { + Write-Host "Fallback a comando generico..." + py $mainScript + } + } catch { + Write-Host "Tentativo con python..." + python $mainScript + } + } else { + Write-Host "main.py non trovato in: $scriptDir" + + # Debug: mostra file Python disponibili + $pyFiles = Get-ChildItem -Path $scriptDir -Filter "*.py" -ErrorAction SilentlyContinue + if ($pyFiles) { + Write-Host "File Python disponibili: $($pyFiles.Name -join ', ')" + } else { + Write-Host "Nessun file Python trovato." + } + } } else { - Write-Host "Il file main.py non è stato trovato nella stessa cartella dello script PowerShell." + Write-Host "ERRORE: Nessuna versione Python utilizzabile!" } + +Write-Host "`n=== COMPLETATO ===" +Write-Host "Premere un tasto per chiudere..." +$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5a61dc5..4d1fd51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ beautifulsoup4==4.13.4 matplotlib==3.10.3 numpy==2.3.1 -pandas==2.3.0 +pandas==2.3.1 pdfkit==1.0.0 -Pillow==11.2.1 +Pillow==11.3.0 +reportlab==4.4.2 Requests==2.32.4 -scikit_learn==1.7.0 +scikit_learn==1.7.1 whisper==1.1.10 diff --git a/scripts/pdf_combiner.py b/scripts/pdf_combiner.py new file mode 100644 index 0000000..b2d804d --- /dev/null +++ b/scripts/pdf_combiner.py @@ -0,0 +1,642 @@ +# Genera PDF da immagini Jpeg. +""" +Script per l'elaborazione di documenti scansionati +Versione: 1.5 +Autore: MagnetarMan + Claude AI +Data: 2025 + +Questo script fornisce un'interfaccia grafica per processare immagini di documenti scansionati +e convertirle in un PDF ottimizzato. +""" + +import os +import sys +import cv2 +import numpy as np +from datetime import datetime +from PIL import Image, ImageFilter, ImageEnhance +import tkinter as tk +from tkinter import filedialog, messagebox, ttk +import threading +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import letter, A4 +from reportlab.lib.utils import ImageReader +import io + +class DocumentProcessor: + def __init__(self): + self.processed_images = [] + self.original_folder = None + + def analyze_image(self, image_path): + """ + Analizza l'immagine per identificare il contenuto del documento + """ + try: + # Carica l'immagine con OpenCV + image = cv2.imread(image_path) + if image is None: + raise ValueError(f"Impossibile caricare l'immagine: {image_path}") + + # Converti in RGB per PIL + image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + return image_rgb + except Exception as e: + print(f"Errore nell'analisi dell'immagine {image_path}: {e}") + return None + + def enhance_sharpness(self, image): + """ + Aumenta la nitidezza dell'immagine (funzione rimossa per problemi di leggibilità) + """ + # Funzione disabilitata: l'aumento della nitidezza rendeva la pagina illegibile + # Ritorna l'immagine senza modifiche + return image + + def detect_and_correct_orientation(self, image): + """ + Rileva e corregge l'orientamento della pagina usando metodi più robusti + """ + # Converte in scala di grigi per l'analisi + gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + + # Applica blur per ridurre il rumore + blurred = cv2.GaussianBlur(gray, (5, 5), 0) + + # Applica soglia per evidenziare il testo + _, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + + # Prova diversi metodi per rilevare l'orientamento + + # Metodo 1: Analisi delle linee usando Hough Transform + edges = cv2.Canny(binary, 50, 150, apertureSize=3) + lines = cv2.HoughLines(edges, 1, np.pi/180, threshold=100) + + angles = [] + if lines is not None: + for line in lines: + rho, theta = line[0] + angle = np.degrees(theta) - 90 + angles.append(angle) + + # Metodo 2: Analisi dei contorni di testo + contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # Filtra e analizza i contorni più significativi + text_contours = [] + for contour in contours: + area = cv2.contourArea(contour) + if area > 50: # Filtra contorni troppo piccoli + x, y, w, h = cv2.boundingRect(contour) + aspect_ratio = w / h + # Considera solo contorni che potrebbero essere testo + if 0.1 < aspect_ratio < 10 and w > 10 and h > 5: + text_contours.append(contour) + + # Calcola angoli dai contorni di testo + for contour in text_contours: + if cv2.contourArea(contour) > 100: + rect = cv2.minAreaRect(contour) + angle = rect[2] + if rect[1][0] < rect[1][1]: # width < height + angle = angle + 90 + angles.append(angle) + + # Metodo 3: Analisi della distribuzione dei pixel + # Calcola le proiezioni orizzontali e verticali + h_projection = np.sum(binary, axis=1) + v_projection = np.sum(binary, axis=0) + + # Calcola la varianza delle proiezioni per diversi angoli + test_angles = [-90, -45, 0, 45, 90] + variances = [] + + for test_angle in test_angles: + if test_angle != 0: + center = (image.shape[1] // 2, image.shape[0] // 2) + rotation_matrix = cv2.getRotationMatrix2D(center, test_angle, 1.0) + rotated_binary = cv2.warpAffine(binary, rotation_matrix, (binary.shape[1], binary.shape[0])) + h_proj = np.sum(rotated_binary, axis=1) + else: + h_proj = h_projection + + # Calcola la varianza della proiezione orizzontale + variance = np.var(h_proj) + variances.append(variance) + + # L'angolo con la varianza maggiore dovrebbe essere quello corretto + if variances: + best_angle_idx = np.argmax(variances) + best_angle = test_angles[best_angle_idx] + angles.append(best_angle) + + # Determina l'angolo finale + if angles: + # Raggruppa angoli simili + angle_groups = {} + for angle in angles: + # Normalizza l'angolo + normalized = angle % 90 + if normalized > 45: + normalized = normalized - 90 + + # Raggruppa per intervalli di 10 gradi + group_key = round(normalized / 10) * 10 + if group_key not in angle_groups: + angle_groups[group_key] = [] + angle_groups[group_key].append(angle) + + # Trova il gruppo con più angoli + if angle_groups: + best_group = max(angle_groups.items(), key=lambda x: len(x[1])) + rotation_angle = np.median(best_group[1]) + + # Determina la rotazione da applicare + if rotation_angle > 60: + final_rotation = 90 + elif rotation_angle > 30: + final_rotation = 45 + elif rotation_angle < -60: + final_rotation = -90 + elif rotation_angle < -30: + final_rotation = -45 + elif abs(rotation_angle) < 5: + final_rotation = 0 + else: + final_rotation = rotation_angle + + # Applica la rotazione se necessaria + if abs(final_rotation) > 2: # Soglia minima per evitare rotazioni inutili + center = (image.shape[1] // 2, image.shape[0] // 2) + rotation_matrix = cv2.getRotationMatrix2D(center, final_rotation, 1.0) + + # Calcola le nuove dimensioni per evitare di tagliare l'immagine + cos_angle = abs(rotation_matrix[0, 0]) + sin_angle = abs(rotation_matrix[0, 1]) + new_width = int((image.shape[0] * sin_angle) + (image.shape[1] * cos_angle)) + new_height = int((image.shape[0] * cos_angle) + (image.shape[1] * sin_angle)) + + # Aggiusta la matrice di rotazione per centrare l'immagine + rotation_matrix[0, 2] += (new_width / 2) - (image.shape[1] / 2) + rotation_matrix[1, 2] += (new_height / 2) - (image.shape[0] / 2) + + # Applica la rotazione + rotated = cv2.warpAffine(image, rotation_matrix, (new_width, new_height), + borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255)) + + print(f"Immagine ruotata di {final_rotation} gradi") + return rotated + + return image + + def convert_to_bw(self, image): + """ + Converte l'immagine in bianco e nero puro + """ + # Converte in scala di grigi + gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + + # Applica soglia adattiva per un migliore risultato + binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, + cv2.THRESH_BINARY, 11, 2) + + # Converte in RGB per mantenere compatibilità + return cv2.cvtColor(binary, cv2.COLOR_GRAY2RGB) + + def clean_noise(self, image): + """ + Applica filtri di pulizia del rumore mantenendo i testi nitidi + """ + # Converte in scala di grigi per l'elaborazione + if len(image.shape) == 3: + gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + else: + gray = image + + # 1. Filtro mediano per rimuovere il rumore salt-and-pepper + # Usa un kernel piccolo per non danneggiare i dettagli del testo + denoised = cv2.medianBlur(gray, 3) + + # 2. Morfologia: apertura per rimuovere piccoli punti di rumore + # Crea un kernel molto piccolo per preservare i caratteri + kernel_opening = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2, 2)) + opened = cv2.morphologyEx(denoised, cv2.MORPH_OPEN, kernel_opening) + + # 3. Morfologia: chiusura per riempire piccoli buchi nei caratteri + kernel_closing = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2, 2)) + closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel_closing) + + # 4. Filtro bilaterale per ridurre ulteriormente il rumore preservando i bordi + # Usa parametri conservativi per mantenere la nitidezza del testo + bilateral = cv2.bilateralFilter(closed, 5, 80, 80) + + # 5. Riapplica una soglia per assicurare il bianco e nero puro + _, clean_binary = cv2.threshold(bilateral, 127, 255, cv2.THRESH_BINARY) + + # 6. Operazione finale di pulizia: rimuove componenti connesse troppo piccole + # Trova tutte le componenti connesse + num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats( + 255 - clean_binary, connectivity=8, ltype=cv2.CV_32S) + + # Crea una maschera per le componenti da mantenere + min_area = 10 # Rimuove componenti più piccole di 10 pixel + mask = np.zeros_like(clean_binary) + + for i in range(1, num_labels): # Salta il background (label 0) + if stats[i, cv2.CC_STAT_AREA] >= min_area: + mask[labels == i] = 255 + + # Applica la maschera + result = clean_binary.copy() + result[mask == 255] = 0 # Rende neri i pixel del testo + + # Converte in RGB per mantenere compatibilità + return cv2.cvtColor(result, cv2.COLOR_GRAY2RGB) + + def enhance_contrast(self, image): + """ + Enfatizza leggermente il contrasto dell'immagine (funzione rimossa per problemi di leggibilità) + """ + # Funzione disabilitata: il contrasto eccessivo rendeva la pagina illegibile + # Ritorna l'immagine senza modifiche + return image + + def remove_borders(self, image): + """ + Rimuove i bordi esterni identificando automaticamente il foglio + """ + # Converte in scala di grigi per l'analisi + gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + + # Applica blur per ridurre il rumore + blurred = cv2.GaussianBlur(gray, (5, 5), 0) + + # Applica soglia per separare il foglio dallo sfondo + _, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + + # Trova i contorni + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + if contours: + # Trova il contorno più grande (presumibilmente il foglio) + largest_contour = max(contours, key=cv2.contourArea) + + # Calcola il rettangolo di delimitazione + x, y, w, h = cv2.boundingRect(largest_contour) + + # Aggiungi un piccolo margine per sicurezza + margin = 10 + x = max(0, x - margin) + y = max(0, y - margin) + w = min(image.shape[1] - x, w + 2 * margin) + h = min(image.shape[0] - y, h + 2 * margin) + + # Ritaglia l'immagine + cropped = image[y:y+h, x:x+w] + + # Verifica che il ritaglio sia ragionevole + if cropped.shape[0] > image.shape[0] * 0.3 and cropped.shape[1] > image.shape[1] * 0.3: + return cropped + + # Se non riesce a trovare un contorno valido, ritorna l'immagine originale + return image + + def process_single_image(self, image_path, progress_callback=None): + """ + Processa una singola immagine attraverso tutte le fasi + """ + try: + if progress_callback: + progress_callback("Analisi immagine...") + + # 1. Analisi iniziale + image = self.analyze_image(image_path) + if image is None: + return None + + if progress_callback: + progress_callback("Aumento nitidezza...") + + # 2. Aumento della nitidezza (disabilitato) + # image = self.enhance_sharpness(image) + + if progress_callback: + progress_callback("Correzione orientamento...") + + # 3. Correzione dell'orientamento + image = self.detect_and_correct_orientation(image) + + if progress_callback: + progress_callback("Conversione in bianco e nero...") + + # 4. Conversione in bianco e nero + image = self.convert_to_bw(image) + + if progress_callback: + progress_callback("Pulizia del rumore...") + + # 5. Pulizia del rumore (nuovo passaggio) + image = self.clean_noise(image) + + if progress_callback: + progress_callback("Enfatizzazione contrasto...") + + # 5. Enfatizzazione del contrasto (disabilitata) + # image = self.enhance_contrast(image) + + if progress_callback: + progress_callback("Rimozione bordi...") + + # 6. Eliminazione dei bordi + image = self.remove_borders(image) + + return image + + except Exception as e: + print(f"Errore nel processamento dell'immagine {image_path}: {e}") + return None + + def create_pdf(self, processed_images, output_path): + """ + Crea un PDF dalle immagini processate + """ + try: + # Crea il canvas PDF + c = canvas.Canvas(output_path, pagesize=A4) + + for i, image_array in enumerate(processed_images): + # Converte l'array numpy in PIL Image + pil_image = Image.fromarray(image_array) + + # Crea un buffer in memoria per l'immagine + img_buffer = io.BytesIO() + pil_image.save(img_buffer, format='PNG') + img_buffer.seek(0) + + # Dimensioni della pagina A4 + page_width, page_height = A4 + + # Calcola le dimensioni dell'immagine mantenendo le proporzioni + img_width, img_height = pil_image.size + aspect_ratio = img_width / img_height + + if aspect_ratio > page_width / page_height: + # L'immagine è più larga, adatta alla larghezza + new_width = page_width + new_height = page_width / aspect_ratio + else: + # L'immagine è più alta, adatta all'altezza + new_height = page_height + new_width = page_height * aspect_ratio + + # Centro l'immagine nella pagina + x = (page_width - new_width) / 2 + y = (page_height - new_height) / 2 + + # Disegna l'immagine sul PDF + c.drawImage(ImageReader(img_buffer), x, y, width=new_width, height=new_height) + + # Aggiungi una nuova pagina se non è l'ultima immagine + if i < len(processed_images) - 1: + c.showPage() + + # Salva il PDF + c.save() + return True + + except Exception as e: + print(f"Errore nella creazione del PDF: {e}") + return False + +class DocumentScannerGUI: + def __init__(self): + self.root = tk.Tk() + self.root.title("Elaboratore Documenti Scansionati") + self.root.geometry("600x500") + self.root.resizable(True, True) + + self.processor = DocumentProcessor() + self.image_paths = [] + self.is_processing = False + + self.setup_gui() + + def setup_gui(self): + """ + Configura l'interfaccia grafica + """ + # Frame principale + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Configura il ridimensionamento + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + main_frame.columnconfigure(1, weight=1) + + # Titolo + title_label = ttk.Label(main_frame, text="Elaboratore Documenti Scansionati", + font=("Arial", 16, "bold")) + title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20)) + + # Sezione selezione file + ttk.Label(main_frame, text="Seleziona Immagini:").grid(row=1, column=0, sticky=tk.W, pady=5) + + self.file_listbox = tk.Listbox(main_frame, height=8) + self.file_listbox.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5) + + # Scrollbar per la listbox + scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.file_listbox.yview) + scrollbar.grid(row=2, column=2, sticky=(tk.N, tk.S), pady=5) + self.file_listbox.configure(yscrollcommand=scrollbar.set) + + # Pulsanti per gestire i file + button_frame = ttk.Frame(main_frame) + button_frame.grid(row=3, column=0, columnspan=3, pady=10) + + ttk.Button(button_frame, text="Aggiungi Immagini", + command=self.add_images).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Rimuovi Selezionata", + command=self.remove_selected).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Svuota Lista", + command=self.clear_list).pack(side=tk.LEFT, padx=5) + + # Pulsante elaborazione + self.process_button = ttk.Button(main_frame, text="Elabora Documenti", + command=self.start_processing, style="Accent.TButton") + self.process_button.grid(row=4, column=0, columnspan=3, pady=20) + + # Barra di progresso + self.progress_var = tk.StringVar() + self.progress_var.set("Pronto per l'elaborazione") + + ttk.Label(main_frame, text="Stato:").grid(row=5, column=0, sticky=tk.W, pady=5) + self.status_label = ttk.Label(main_frame, textvariable=self.progress_var) + self.status_label.grid(row=5, column=1, sticky=tk.W, pady=5) + + self.progress_bar = ttk.Progressbar(main_frame, mode='indeterminate') + self.progress_bar.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5) + + # Informazioni aggiuntive + info_frame = ttk.LabelFrame(main_frame, text="Informazioni", padding="10") + info_frame.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) + + info_text = """Operazioni eseguite su ogni immagine: +1. Analisi e caricamento dell'immagine +2. Correzione automatica dell'orientamento (migliorata) +3. Conversione in bianco e nero +4. Pulizia del rumore (nuovo - rimuove effetto fotocopiatrice) +5. Rimozione automatica dei bordi + +Il PDF finale sarà salvato nella stessa cartella delle immagini. +Nota: Le funzioni di aumento nitidezza e contrasto sono state rimosse per migliorare la leggibilità.""" + + ttk.Label(info_frame, text=info_text, justify=tk.LEFT).pack(anchor=tk.W) + + def add_images(self): + """ + Aggiunge immagini alla lista + """ + file_paths = filedialog.askopenfilenames( + title="Seleziona immagini", + filetypes=[("Immagini JPEG", "*.jpg *.jpeg"), ("Tutte le immagini", "*.jpg *.jpeg *.png")] + ) + + for file_path in file_paths: + if file_path not in self.image_paths: + self.image_paths.append(file_path) + self.file_listbox.insert(tk.END, os.path.basename(file_path)) + + self.update_ui_state() + + def remove_selected(self): + """ + Rimuove l'immagine selezionata dalla lista + """ + selected_indices = self.file_listbox.curselection() + if selected_indices: + index = selected_indices[0] + self.file_listbox.delete(index) + del self.image_paths[index] + self.update_ui_state() + + def clear_list(self): + """ + Svuota la lista delle immagini + """ + self.file_listbox.delete(0, tk.END) + self.image_paths.clear() + self.update_ui_state() + + def update_ui_state(self): + """ + Aggiorna lo stato dell'interfaccia + """ + has_images = len(self.image_paths) > 0 + self.process_button.configure(state=tk.NORMAL if has_images and not self.is_processing else tk.DISABLED) + + if has_images: + self.progress_var.set(f"Pronto per elaborare {len(self.image_paths)} immagini") + else: + self.progress_var.set("Pronto per l'elaborazione") + + def start_processing(self): + """ + Avvia il processo di elaborazione in un thread separato + """ + if not self.image_paths: + messagebox.showwarning("Attenzione", "Seleziona almeno un'immagine da elaborare.") + return + + self.is_processing = True + self.progress_bar.start() + self.update_ui_state() + + # Avvia il thread di elaborazione + thread = threading.Thread(target=self.process_images) + thread.daemon = True + thread.start() + + def process_images(self): + """ + Processa tutte le immagini e crea il PDF + """ + try: + processed_images = [] + total_images = len(self.image_paths) + + # Determina la cartella di output + if self.image_paths: + output_folder = os.path.dirname(self.image_paths[0]) + else: + output_folder = os.path.expanduser("~") + + # Processa ogni immagine + for i, image_path in enumerate(self.image_paths): + def update_progress(message): + self.root.after(0, lambda: self.progress_var.set(f"Immagine {i+1}/{total_images}: {message}")) + + processed_image = self.processor.process_single_image(image_path, update_progress) + + if processed_image is not None: + processed_images.append(processed_image) + else: + self.root.after(0, lambda: messagebox.showerror("Errore", f"Impossibile processare l'immagine: {os.path.basename(image_path)}")) + + if processed_images: + # Crea il nome del file PDF + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + pdf_filename = f"Documenti_Scansionati_{timestamp}.pdf" + pdf_path = os.path.join(output_folder, pdf_filename) + + # Aggiorna il progresso + self.root.after(0, lambda: self.progress_var.set("Creazione PDF...")) + + # Crea il PDF + if self.processor.create_pdf(processed_images, pdf_path): + self.root.after(0, lambda: self.show_success_message(pdf_path)) + else: + self.root.after(0, lambda: messagebox.showerror("Errore", "Errore nella creazione del PDF")) + else: + self.root.after(0, lambda: messagebox.showerror("Errore", "Nessuna immagine è stata processata con successo")) + + except Exception as e: + self.root.after(0, lambda: messagebox.showerror("Errore", f"Errore durante l'elaborazione: {str(e)}")) + finally: + # Ripristina l'interfaccia + self.root.after(0, self.processing_complete) + + def show_success_message(self, pdf_path): + """ + Mostra il messaggio di successo + """ + messagebox.showinfo("Successo", f"Elaborazione completata!\nIl PDF è stato salvato in:\n{pdf_path}") + + def processing_complete(self): + """ + Ripristina l'interfaccia dopo l'elaborazione + """ + self.is_processing = False + self.progress_bar.stop() + self.progress_var.set("Elaborazione completata") + self.update_ui_state() + + def run(self): + """ + Avvia l'applicazione + """ + self.root.mainloop() + +def main(): + """ + Funzione principale + """ + try: + app = DocumentScannerGUI() + app.run() + except Exception as e: + print(f"Errore nell'avvio dell'applicazione: {e}") + messagebox.showerror("Errore", f"Errore nell'avvio dell'applicazione: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/transcribe_wav.py b/scripts/transcribe_wav.py index c10c7f5..6976146 100644 --- a/scripts/transcribe_wav.py +++ b/scripts/transcribe_wav.py @@ -8,8 +8,8 @@ def upgrade_pip_and_install_whisper(): """ Aggiorna pip e installa o reinstalla correttamente whisper. """ - user_name = os.getlogin() - python_path = os.path.join(f"C:\\Users\\{user_name}\\AppData\\Local\\Programs\\Python\\Python310", "python.exe") + user_home = os.environ.get('USERPROFILE') + python_path = os.path.join(user_home, "AppData", "Local", "Programs", "Python", "Python310", "python.exe") if not os.path.exists(python_path): print(f"Errore: Python 3.10 non trovato in {python_path}.") @@ -43,11 +43,11 @@ def ensure_python_3_10(): if sys.version_info[0] != 3 or sys.version_info[1] != 10: print("Forzando l'esecuzione con Python 3.10...") - # Recupera il nome dell'utente corrente - user_name = os.getlogin() + # Recupera il percorso della home directory reale + user_home = os.environ.get('USERPROFILE') # Costruisci il percorso di Python 3.10 dinamicamente - python_path = os.path.join(f"C:\\Users\\{user_name}\\AppData\\Local\\Programs\\Python\\Python310", "python.exe") + python_path = os.path.join(user_home, "AppData", "Local", "Programs", "Python", "Python310", "python.exe") if not os.path.exists(python_path): print(f"Errore: Python 3.10 non trovato in {python_path}. Verifica che Python 3.10 sia installato correttamente.")