Lab

Convertire foto in bianco e nero ad alto contrasto con Python

14 min di lettura
Stampe fotografiche monocromatiche appese ad asciugare sotto la luce calda di una camera oscura
Windows macOS Linux
Foto: Annushka Ahuja · Pexels · Licenza Pexels

Pacchetto Python (.zip) · versione 1.0.0 · verificato

bn-foto-locale.zip

Script autonomo che gira solo sul tuo computer. Nessun upload, nessuna telemetria.

Formato
Archivio ZIP (.zip)
Dimensione
8,2 KB
Python
3.9 o superiore
Licenza
MIT
Richiede
opencv-python, numpy, Pillow
Rete
Offline

Crea, rinomina o sposta file solo se lo chiedi tu (per esempio per il report o la quarantena). Il codice è leggibile riga per riga nel file scaricato.

Verifica integrità

SHA-256 di bn-foto-locale.zip

eb5d657837cab4e8391b4a0afcbdb89658f15f2b671094cef57902363887974a

Calcola lo SHA-256 del file scaricato e confrontalo con questo: Confronta hash · Come si verifica

Trasforma intere cartelle di foto in bianco e nero con Python, OpenCV e una pipeline a 7 stadi (Rec.709, istogramma, CLAHE). Pacchetto ZIP per Windows e Mac.

Hai una cartella con trenta o quaranta foto — ritratti di famiglia, scatti di un evento o immagini per un progetto — e vuoi trasformarle in un bianco e nero pulito, tridimensionale e coerente. Se usi il comando «Scala di grigi» del visualizzatore di sistema ottieni quasi sempre un grigio piatto e spento; se invece le carichi su un convertitore online gratuito, finisci per spedire foto personali e volti su un server esterno solo per applicare un filtro.

Risposta breve: con il pacchetto Python di questo Lab trasformi un’intera cartella di foto in bianco e nero sul tuo computer, con un doppio clic su Windows o Mac e senza caricare nulla online. A differenza di una conversione standard, lo script converti_bn.py legge l’istogramma di ogni singola immagine e applica una pipeline fotografica adattiva in 7 stadi (bilanciamento del bianco, luminanza Rec.709, livelli sui percentili, curva Gamma, micro-contrasto locale CLAHE, maschera di nitidezza e curva tonale a S), adattando i parametri se lo scatto di partenza è scuro, sovraesposto o a basso contrasto.

ℹ️ Nota / Prerequisito operativo: Ti basta avere Python 3.9 o superiore installato sul computer. Il pacchetto .zip scaricabile qui sopra include già lo script converti_bn.py, il file requirements.txt, le due cartelle di lavoro e i file di avvio automatico Avvia conversione.bat (Windows) e Avvia conversione.command (macOS e Linux), che al primo lancio creano da soli l’ambiente virtuale .venv e installano le librerie necessarie.

Perché una normale «scala di grigi» appiattisce le foto

Quando un programma converte una foto a colori in bianco e nero facendo una semplice media tra Rosso, Verde e Blu — oppure azzerando la saturazione — tratta allo stesso modo uno scatto controluce, un ritratto in ombra e una foto scattata sotto una lampadina gialla. Il risultato è quella patina grigia “slavata” in cui mancano sia un nero profondo sia un bianco luminoso.

Pellicola fotografica e stampe fissate con una molletta metallica sotto la luce rossa di una camera oscura
Come in camera oscura, il segreto di un buon bianco e nero è calibrare punto di nero, punto di bianco e contrasto locale in base all'esposizione di partenza. Foto: Ron Lach · Pexels

Lo script di questo progetto lavora invece come una piccola camera oscura automatica: prima misura la foto, poi decide come svilupparla. Ogni immagine attraversa sette passaggi consecutivi in memoria (su matrici float32 con NumPy e OpenCV) prima di essere salvata su disco alla massima qualità.

Stadio della pipeline Tipo di intervento Cosa fa concretamente sui pixel
1. Bilanciamento del bianco Funzione bilanciamento() Pre-filtro RGB Applica l'ipotesi Grey-World ai tre canali colore per eliminare le dominanti calde o fredde prima ancora di calcolare i grigi.
2. Luminanza percettiva Rec.709 Funzione luminanza() Pesi occhi umani Converte in monocromatico pesando il Verde al 71,52%, il Rosso al 21,26% e il Blu al 7,22% (standard ITU-R BT.709), restituendo incarnati e fogliame naturali.
3. Analisi dell'istogramma Funzione analizza() Adattivo Calcola mediana (P50), escursione dinamica (P95 - P5) e percentuale di ombre chiuse o luci bruciate, classificando lo scatto come scuro, bilanciato o chiaro.
4. Espansione dei livelli Funzione livelli() Punto nero/bianco Rimappa l'istogramma sui percentili scelti dall'analisi (tra 0,5% e 99,5%), evitando che un singolo riflesso blocchi il contrasto dell'intera foto.
5. Correzione Gamma via LUT Funzione gamma() Mezzi toni Usa una Look-Up Table a 256 valori per schiarire i mezzi toni nelle foto sottoesposte (gamma 0.92) o dare densità a quelle troppo chiare (gamma 1.08).
6. Micro-contrasto CLAHE Funzione clahe() Locale 8×8 Divide l'immagine in una griglia 8×8 ed equalizza il contrasto locale con un limite anti-rumore dinamico (da 1.8 fino a 4.0 per le foto piatte).
7. Nitidezza e «Ritocchino» nitidezza() + ritocchino() Finitura S-Curve Applica una maschera di contrasto (raggio 1,2 px), recupera morbidamente ombre estreme (<52) e alte luci (>212) e chiude con una curva a S sui mezzi toni.

Cosa contiene il pacchetto ZIP e cosa fa sul tuo computer

Quando scarichi ed estrai bn-foto-locale.zip dal riquadro in cima alla pagina, ottieni una cartella chiamata bn-foto-locale già pronta con questa struttura:

  • converti_bn.py — lo script Python completo e commentato.
  • requirements.txt — l’elenco delle tre librerie richieste (opencv-python>=4.8.0, numpy>=1.24.0, Pillow>=10.0.0).
  • Avvia conversione.bat — il file di avvio con doppio clic per Windows.
  • Avvia conversione.command — il file di avvio con doppio clic per macOS e Linux.
  • foto da trasformare/ — la cartella in cui incollare o trascinare le foto a colori (jpg, jpeg, png, webp, tif, tiff, bmp, gif, heic, heif, avif).
  • foto in bianco e nero/ — la cartella dove compaiono i file convertiti.
  • LEGGIMI.txt — un promemoria sintetico dei comandi anche offline.

Cosa non fa e quando usa la rete. Le foto originali dentro foto da trasformare/ vengono aperte solo in lettura e non vengono mai modificate né cancellate. La connessione a internet serve esclusivamente la prima volta in cui avvii il programma, per scaricare da PyPI le tre librerie dentro la sottocartella .venv. Dal secondo avvio in poi puoi usarlo anche con il Wi-Fi spento. Se oltre a convertire le foto in bianco e nero vuoi assicurarti di cancellare le coordinate GPS prima di pubblicarle, puoi abbinare questo flusso allo script per rimuovere i metadati EXIF dalle foto in locale.

Metodo 1 — Avvio rapido con doppio clic (Windows e macOS)

Questo è il modo più comodo per usare il tool ogni volta che hai nuove foto da convertire, senza dover aprire il terminale e riscrivere i comandi a mano.

Mano sulla tastiera di un portatile con codice sorgente aperto a schermo su una scrivania in legno
I file di avvio inclusi nello ZIP preparano da soli l'ambiente virtuale al primo lancio ed elaborano l'intera cartella in locale. Foto: Lukas · Pexels

Su Windows: usare Avvia conversione.bat

  1. Fai clic destro su bn-foto-locale.zip e scegli Estrai tutto…, poi apri la cartella bn-foto-locale.
  2. Copia una o più foto a colori dentro la sottocartella foto da trasformare.
  3. Torna nella cartella bn-foto-locale e fai doppio clic su Avvia conversione.bat.
  4. Se compare l’avviso blu di Windows SmartScreen («PC protetto da Windows», normale per qualsiasi file .bat scaricato dal browser): clicca su Ulteriori informazioni e poi sul pulsante Esegui comunque.
  5. Cosa succede al primo avvio: si apre la finestra nera del Prompt, lo script crea in automatico la cartella .venv, installa opencv-python, numpy e Pillow (richiede circa 15–30 secondi la prima volta) e subito dopo converte tutte le foto. Dalle volte successive salta l’installazione e converte decine di foto in pochi secondi.
  6. Al termine premi un tasto qualsiasi per chiudere la finestra e apri la cartella foto in bianco e nero.

Su macOS: attivare e usare Avvia conversione.command

Su macOS i file con estensione .command si avviano con un doppio clic nel Finder, ma quando vengono scaricati da internet il sistema applica l’attributo di quarantena di Gatekeeper. Ecco come sbloccarlo una volta sola per sempre:

  1. Estrai bn-foto-locale.zip (basta un doppio clic sul file .zip) e copia le tue foto dentro foto da trasformare.
  2. Apri il Terminale direttamente nella cartella bn-foto-locale (nel Finder fai clic destro sulla cartella bn-foto-locale e scegli Nuovo terminale nella cartella).
  3. Esegui questi due comandi per rendere eseguibile il file e rimuovere il blocco di quarantena del download:
chmod +x "Avvia conversione.command" "converti_bn.py"
xattr -d com.apple.quarantine "Avvia conversione.command" 2>/dev/null
  1. Da questo momento in poi, ogni volta che vorrai convertire nuove foto ti basterà fare doppio clic su Avvia conversione.command nel Finder: si aprirà una finestra del Terminale che al primo giro prepara .venv, converte tutte le immagini e si chiude da sola dopo 6 secondi.

💡 Consiglio pratico — Perché il launcher crea una cartella .venv nascosta: Su macOS (con Python installato via Homebrew) e sulle distribuzioni Linux recenti, installare pacchetti con pip fuori da un ambiente virtuale viene bloccato con l’errore externally-managed-environment (PEP 668). I due file Avvia conversione.bat e Avvia conversione.command creano automaticamente un .venv locale dentro la cartella del progetto: così non sporcano il sistema e non si bloccano mai.

Metodo 2 — Avvio manuale da Terminale o PowerShell

Se preferisci avere il pieno controllo da riga di comando o integrare lo script in una tua automazione, puoi eseguire i passaggi manualmente. Per approfondire come funzionano l’ambiente isolato e il file delle dipendenze puoi leggere le guide su ambienti virtuali Python (venv) e su come usare requirements.txt.

1. Entra nella cartella e prepara l’ambiente virtuale

Su Windows (apri PowerShell dentro la cartella bn-foto-locale):

python -m venv .venv
.venv\Scripts\python.exe -m pip install --upgrade pip
.venv\Scripts\python.exe -m pip install -r requirements.txt
.venv\Scripts\python.exe -c "import cv2, numpy, PIL; print('OpenCV:', cv2.__version__, '| NumPy:', numpy.__version__)"

Su macOS e Linux (apri il Terminale dentro la cartella bn-foto-locale):

python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -c "import cv2, numpy, PIL; print('OpenCV:', cv2.__version__, '| NumPy:', numpy.__version__)"

L’ultimo comando di verifica deve stampare i numeri di versione senza errori, per esempio:

OpenCV: 4.10.0 | NumPy: 2.1.2

2. Lancia la conversione e leggi il report

Metti qualche immagine dentro foto da trasformare ed esegui lo script.

Su Windows:

.venv\Scripts\python.exe converti_bn.py

Su macOS e Linux:

.venv/bin/python converti_bn.py

Per ogni immagine vedrai in tempo reale il profilo di esposizione rilevato dall’istogramma (scura, bilanciata o chiara), la mediana P50 e l’ampiezza dinamica P95 - P5:

==============================================================
  CONVERSIONE FOTO IN BIANCO E NERO — PIPELINE A 7 STADI
==============================================================

Trovate 3 immagini. Elaborazione in corso...

  → architettura-milano.jpg ... OK → architettura-milano.jpg  [esposizione bilanciata, P50=118, dinamica=174]
  → cortile-ombra.png ... OK → cortile-ombra.png  [esposizione scura, P50=74, dinamica=112]
  → ritratto-controluce.webp ... OK → ritratto-controluce.webp  [esposizione chiara, P50=172, dinamica=148]

--------------------------------------------------------------
Completate: 3  |  Errori: 0  |  Tempo: 0.41s
Cartella di output: /Users/alex/bn-foto-locale/foto in bianco e nero
--------------------------------------------------------------

Il codice completo di converti_bn.py, spiegato pezzo per pezzo

Qui sotto trovi il codice integrale contenuto nel pacchetto .zip, commentato in ogni funzione così puoi verificarlo o personalizzare le soglie a tuo gusto:

Album fotografico aperto con stampe monocromatiche d'epoca raccolte e ordinate su un piano di lavoro
Elaborare un'intera serie di scatti con la stessa pipeline garantisce una resa monocromatica uniforme su tutto l'archivio. Foto: T6 Adventures · Pexels
#!/usr/bin/env python3
"""
converti_bn.py — Conversione fotografica adattiva in Bianco e Nero (7 stadi).
Autore: Alex Lunalbi (ai riparo)
Licenza: MIT
"""

from __future__ import annotations

import argparse
import time
from pathlib import Path

import cv2
import numpy as np
from PIL import Image, ImageOps

# Supporto opzionale per file .heic / .heif (iPhone) se pillow-heif è installato
try:
    import pillow_heif  # type: ignore

    pillow_heif.register_heif_opener()
except ImportError:
    pass

BASE_DIR = Path(__file__).resolve().parent
DEFAULT_INPUT_DIR = BASE_DIR / "foto da trasformare"
DEFAULT_OUTPUT_DIR = BASE_DIR / "foto in bianco e nero"

SUPPORTED = {
    ".jpg", ".jpeg", ".png", ".tif", ".tiff",
    ".bmp", ".webp", ".gif", ".heic", ".heif", ".avif",
}

# Coefficienti di luminanza percettiva ITU-R BT.709
LUMA_R, LUMA_G, LUMA_B = 0.2126, 0.7152, 0.0722


def crea_cartelle(input_dir: Path, output_dir: Path) -> None:
    """Crea le cartelle di ingresso e di uscita se non esistono ancora."""
    input_dir.mkdir(parents=True, exist_ok=True)
    output_dir.mkdir(parents=True, exist_ok=True)


def trova_immagini(origine: Path) -> list[Path]:
    """Restituisce l'elenco ordinato delle immagini supportate (da cartella o file singolo)."""
    if origine.is_file():
        return [origine] if origine.suffix.lower() in SUPPORTED else []
    if not origine.is_dir():
        return []
    return sorted(
        (
            p for p in origine.iterdir()
            if p.is_file() and not p.name.startswith(".") and p.suffix.lower() in SUPPORTED
        ),
        key=lambda p: p.name.lower(),
    )


def carica(percorso: Path) -> np.ndarray | None:
    """
    Carica un'immagine come matrice BGR a 8 bit.
    Usa Pillow prima per rispettare il tag EXIF Orientation (foto verticali da smartphone);
    fallback su OpenCV imdecode (compatibile con percorsi Windows accentati/Unicode).
    """
    try:
        with Image.open(percorso) as pil_img:
            pil_corretta = ImageOps.exif_transpose(pil_img)
            rgb = np.array(pil_corretta.convert("RGB"), dtype=np.uint8)
            return cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
    except Exception:
        pass

    try:
        buffer = np.fromfile(str(percorso), dtype=np.uint8)
        img = cv2.imdecode(buffer, cv2.IMREAD_COLOR)
        if img is not None:
            return img
    except Exception:
        pass

    return None


def bilanciamento(bgr: np.ndarray) -> np.ndarray:
    """Stadio 1 — Bilanciamento del bianco automatico (ipotesi Grey-World)."""
    img = bgr.astype(np.float32)
    media = img.reshape(-1, 3).mean(axis=0)
    img *= media.mean() / (media + 1e-6)
    return np.clip(img, 0, 255).astype(np.uint8)


def luminanza(bgr: np.ndarray) -> np.ndarray:
    """Stadio 2 — Conversione in scala di grigi secondo lo standard Rec.709."""
    b, g, r = cv2.split(bgr.astype(np.float32))
    luma = LUMA_R * r + LUMA_G * g + LUMA_B * b
    return np.clip(luma, 0, 255).astype(np.uint8)


def analizza(gray: np.ndarray) -> dict:
    """Stadio 3 — Analisi statistica dell'istogramma (P5, P50, P95, range dinamico)."""
    hist = cv2.calcHist([gray], [0], None, [256], [0, 256]).flatten()
    tot = float(hist.sum()) or 1.0
    cum = np.cumsum(hist) / tot

    p5 = int(np.searchsorted(cum, 0.05))
    p50 = int(np.searchsorted(cum, 0.50))
    p95 = int(np.searchsorted(cum, 0.95))
    rd = max(p95 - p5, 1)

    if p50 < 90:
        sp, hp, valore_gamma, clip_clahe, profilo = 1.0, 99.5, 0.92, 2.8, "scura"
    elif p50 > 165:
        sp, hp, valore_gamma, clip_clahe, profilo = 0.5, 99.0, 1.08, 1.8, "chiara"
    else:
        sp, hp, valore_gamma, clip_clahe, profilo = 1.5, 98.5, 1.0, 2.2, "bilanciata"

    if rd < 120:
        clip_clahe = min(clip_clahe + 1.0, 4.0)

    return {
        "shadow_pct": sp,
        "highlight_pct": hp,
        "clahe_clip": clip_clahe,
        "gamma": valore_gamma,
        "range_dyn": rd,
        "p50": p50,
        "profilo": profilo,
        "ombre_clip": float(hist[:10].sum()) / tot,
        "luci_clip": float(hist[245:].sum()) / tot,
    }


def livelli(gray: np.ndarray, sp: float, hp: float) -> np.ndarray:
    """Stadio 4 — Rimappatura adattiva dei livelli sui percentili."""
    flat = gray.astype(np.float32)
    nero, bianco = float(np.percentile(flat, sp)), float(np.percentile(flat, hp))
    if bianco - nero < 5:
        return gray
    riscalato = (flat - nero) * (255.0 / (bianco - nero))
    return np.clip(riscalato, 0, 255).astype(np.uint8)


def gamma(gray: np.ndarray, g: float) -> np.ndarray:
    """Stadio 5 — Correzione Gamma tramite Look-Up Table (LUT a 256 valori)."""
    if abs(g - 1.0) < 0.01:
        return gray
    inv = 1.0 / g
    tab = np.array([((i / 255.0) ** inv) * 255.0 for i in range(256)], dtype=np.uint8)
    return cv2.LUT(gray, tab)


def clahe(gray: np.ndarray, clip: float) -> np.ndarray:
    """Stadio 6 — Micro-contrasto locale CLAHE su griglia 8x8."""
    equalizzatore = cv2.createCLAHE(clipLimit=clip, tileGridSize=(8, 8))
    return equalizzatore.apply(gray)


def nitidezza(gray: np.ndarray) -> np.ndarray:
    """Stadio 7a — Maschera di contrasto (Unsharp Mask con sigma=1.2)."""
    blur = cv2.GaussianBlur(gray, (0, 0), sigmaX=1.2)
    return np.clip(cv2.addWeighted(gray, 1.25, blur, -0.25, 0), 0, 255).astype(np.uint8)


def ritocchino(gray: np.ndarray, p: dict) -> np.ndarray:
    """Stadio 7b — Chiarezza locale, protezione ombre/luci e curva tonale a S."""
    img = gray.astype(np.float32)
    rd, p50 = p["range_dyn"], p["p50"]

    if rd < 130:
        blur = cv2.GaussianBlur(img, (0, 0), 16)
        img += (img - blur) * (0.07 + (130 - rd) / 1000.0)

    if p["ombre_clip"] > 0.03 or p50 < 88:
        maschera_ombre = cv2.GaussianBlur(np.clip((52.0 - img) / 52.0, 0, 1), (0, 0), 7)
        img += maschera_ombre * 3.5

    if p["luci_clip"] > 0.02 or p50 > 168:
        maschera_luci = cv2.GaussianBlur(np.clip((img - 212.0) / 43.0, 0, 1), (0, 0), 9)
        img -= maschera_luci * 4.0

    t = np.clip(img / 255.0, 0, 1)
    boost = 0.05 if rd < 125 else 0.03
    t = t + boost * t * (1.0 - t) * (t - 0.5) * 4.0
    return np.clip(t * 255.0, 0, 255).astype(np.uint8)


def elabora(bgr: np.ndarray) -> tuple[np.ndarray, dict]:
    """Esegue l'intera pipeline a 7 stadi e restituisce immagine B/N + metriche."""
    g = luminanza(bilanciamento(bgr))
    info = analizza(g)
    g = livelli(g, info["shadow_pct"], info["highlight_pct"])
    g = gamma(g, info["gamma"])
    g = clahe(g, info["clahe_clip"])
    g = nitidezza(g)
    finale = ritocchino(g, info)
    return finale, info


def salva(gray: np.ndarray, out: Path, qualita: int = 98) -> None:
    """Salva l'immagine con imencode + tofile (supporto pieno percorsi Unicode su Windows)."""
    ext = out.suffix.lower()
    if ext in {".jpg", ".jpeg"}:
        parametri = [cv2.IMWRITE_JPEG_QUALITY, qualita, cv2.IMWRITE_JPEG_OPTIMIZE, 1]
        formato = ".jpg"
    elif ext == ".png":
        parametri = [cv2.IMWRITE_PNG_COMPRESSION, 3]
        formato = ".png"
    elif ext == ".webp":
        parametri = [cv2.IMWRITE_WEBP_QUALITY, qualita]
        formato = ".webp"
    elif ext in {".tif", ".tiff"}:
        parametri = []
        formato = ".tiff"
    else:
        out = out.with_suffix(".png")
        parametri = [cv2.IMWRITE_PNG_COMPRESSION, 3]
        formato = ".png"

    successo, buffer = cv2.imencode(formato, gray, parametri)
    if not successo:
        raise RuntimeError(f"Impossibile codificare il file {out.name}")
    buffer.tofile(str(out))


def percorso_output(percorso_in: Path, cartella_out: Path) -> Path:
    if percorso_in.suffix.lower() in {".gif", ".heic", ".heif", ".avif", ".bmp"}:
        return cartella_out / f"{percorso_in.stem}.png"
    return cartella_out / percorso_in.name


def configura_argomenti() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Converte una cartella di foto (o un singolo file) in bianco e nero."
    )
    parser.add_argument("-i", "--input", type=Path, default=DEFAULT_INPUT_DIR,
                        help='Cartella o file di origine (predefinito: "foto da trasformare")')
    parser.add_argument("-o", "--output", type=Path, default=DEFAULT_OUTPUT_DIR,
                        help='Cartella di destinazione (predefinito: "foto in bianco e nero")')
    parser.add_argument("-q", "--qualita", type=int, default=98,
                        help="Qualità di salvataggio JPEG/WebP da 70 a 100 (predefinito: 98)")
    return parser.parse_args()


def main() -> int:
    args = configura_argomenti()
    qualita = max(70, min(100, args.qualita))

    print("=" * 62)
    print("  CONVERSIONE FOTO IN BIANCO E NERO — PIPELINE A 7 STADI")
    print("=" * 62)

    if args.input == DEFAULT_INPUT_DIR and args.output == DEFAULT_OUTPUT_DIR:
        crea_cartelle(DEFAULT_INPUT_DIR, DEFAULT_OUTPUT_DIR)
    else:
        args.output.mkdir(parents=True, exist_ok=True)

    immagini = trova_immagini(args.input)

    if not immagini:
        print(f"\nNessuna immagine trovata in:\n  {args.input}")
        print("\nCome procedere:")
        print("  1. Copia le tue foto dentro la cartella «foto da trasformare»")
        print("  2. Rilancia questo programma")
        print("-" * 62)
        return 0

    ok, errori = 0, 0
    inizio = time.perf_counter()
    print(f"\nTrovate {len(immagini)} immagini. Elaborazione in corso...\n")

    for percorso in immagini:
        print(f"  → {percorso.name} ... ", end="", flush=True)
        bgr = carica(percorso)
        if bgr is None:
            print("ERRORE (file non leggibile o formato non supportato)")
            errori += 1
            continue
        try:
            out = percorso_output(percorso, args.output)
            bn, info = elabora(bgr)
            salva(bn, out, qualita=qualita)
            print(
                f"OK → {out.name}  "
                f"[esposizione {info['profilo']}, P50={info['p50']}, dinamica={info['range_dyn']}]"
            )
            ok += 1
        except Exception as exc:
            print(f"ERRORE ({exc})")
            errori += 1

    durata = time.perf_counter() - inizio
    print("\n" + "-" * 62)
    print(f"Completate: {ok}  |  Errori: {errori}  |  Tempo: {durata:.2f}s")
    print(f"Cartella di output: {args.output}")
    print("-" * 62)

    return 1 if errori else 0


if __name__ == "__main__":
    raise SystemExit(main())

Tre dettagli tecnici che evitano i problemi più frequenti

  1. Foto verticali dello smartphone sempre dritte (ImageOps.exif_transpose): molte fotocamere e telefoni salvano lo scatto in orizzontale e annotano la rotazione solo nel tag EXIF Orientation. La funzione carica() applica prima exif_transpose di Pillow: così un ritratto verticale resta verticale anche dopo il passaggio in OpenCV.
  2. Nomi file e cartelle con accenti su Windows (np.fromfile + cv2.imencode): su Windows le funzioni classiche cv2.imread() e cv2.imwrite() di OpenCV usano le API C ANSI e falliscono se il percorso contiene lettere accentate (come C:\Users\Niccolò\... o Città.jpg). Passare da np.fromfile e buffer.tofile risolve il problema alla radice su tutti i sistemi operativi.
  3. Conversione automatica in .png per formati contenitore: se nella cartella di ingresso metti un file .heic, .avif, .gif o .bmp, percorso_output() salva il risultato come .png lossless nella cartella di uscita, mentre mantiene invariata l’estensione originale per .jpg, .png, .webp e .tiff.

I flag da riga di comando uno per uno

Anche se lo script nasce per funzionare senza argomenti usando le due cartelle predefinite, accetta tre opzioni gestite con argparse:

  • -i / --input: indica una cartella diversa da "foto da trasformare" oppure un singolo file immagine (es. -i "C:\Foto\Vacanze" o -i ritratto.jpg).
  • -o / --output: indica la cartella in cui salvare le foto convertite (se non esiste viene creata automaticamente).
  • -q / --qualita: regola la compressione per i file .jpg e .webp su una scala da 70 a 100 (il valore predefinito è 98, pensato per la stampa e l’archiviazione fotografica; se le foto devono andare su una pagina web puoi scendere a -q 85 per dimezzare il peso in KB).

Esempio — Convertire una cartella esterna salvando per il web (-q 85):

.venv\Scripts\python.exe converti_bn.py -i "C:\Users\Alex\Pictures\Evento" -o "C:\Users\Alex\Pictures\Evento_BN" -q 85

Errori comuni e come risolverli

Su macOS compare «“Avvia conversione.command” non può essere aperto perché proviene da uno sviluppatore non identificato». Perché succede: macOS applica il flag com.apple.quarantine agli script scaricati dal browser. Come risolvere: apri il Terminale nella cartella bn-foto-locale e lancia chmod +x "Avvia conversione.command" seguito da xattr -d com.apple.quarantine "Avvia conversione.command". In alternativa, fai clic destro sul file tenendo premuto il tasto Ctrl e scegli Apri.

I file .heic dell’iPhone danno ERRORE (file non leggibile). Perché succede: Pillow e OpenCV non includono di serie il decoder brevettato HEIF/HEIC. Come risolvere: lo script è già predisposto per agganciare automaticamente pillow-heif se presente. Ti basta installarlo una volta sola nel .venv con .venv\Scripts\python.exe -m pip install pillow-heif (Windows) oppure .venv/bin/python -m pip install pillow-heif (macOS/Linux).

No module named 'cv2' quando lanci python converti_bn.py dal terminale. Perché succede: stai chiamando il Python globale del sistema invece di quello dentro .venv dove sono installate le librerie. Come risolvere: chiama l’interprete del venv (.venv\Scripts\python.exe converti_bn.py su Windows, .venv/bin/python converti_bn.py su macOS/Linux) oppure fai doppio clic sui launcher Avvia conversione.bat / Avvia conversione.command.

Domande Frequenti (FAQ)

Le mie foto originali vengono modificate o sovrascritte?

No. Lo script apre i file dentro foto da trasformare/ esclusivamente in lettura e scrive le nuove immagini dentro foto in bianco e nero/. Anche se rilanci il programma più volte, i tuoi scatti a colori originali restano intatti.

Quante foto posso mettere insieme nella cartella?

Non c’è un limite fisso: lo script elabora un’immagine alla volta e libera la memoria RAM subito dopo il salvataggio. Nei miei test su foto da 12–24 megapixel impiega circa 0,10–0,25 secondi a scatto; una cartella da 100 foto viene completata in meno di mezzo minuto.

Perché il bianco e nero di questo script sembra più nitido e tridimensionale?

Perché combina due interventi complementari: i livelli globali (che assicurano la presenza di un vero punto di nero e di un vero punto di bianco) e l’equalizzazione CLAHE su griglia 8×8 (che esalta il micro-contrasto locale su trame, tessuti, architetture e volti senza bruciare le alte luci).

Posso regolare l’intensità dell’effetto se lo trovo troppo contrastato?

Sì. Ti basta aprire converti_bn.py con qualsiasi editor di testo e guardare la funzione analizza(): abbassando il valore di clip_clahe (per esempio da 2.2 a 1.4) otterrai un bianco e nero più morbido e delicato; aumentando boost nella funzione ritocchino() da 0.05 a 0.08 otterrai invece una curva a S ancora più marcata in stile pellicola ad alto contrasto.

Cosa succede ai metadati EXIF nelle foto convertite?

Durante il passaggio in matrice NumPy e la ricodifica finale con cv2.imencode, i metadati EXIF (inclusi gli eventuali dati GPS dello smartphone) non vengono copiati nel nuovo file generato in foto in bianco e nero/. Se invece vuoi pulire gli EXIF dalle tue foto mantenendole a colori, usa il progetto dedicato per rimuovere i metadati EXIF dalle foto.

Alex Lunalbi

Alex Lunalbi

Security & privacy

Dona su PayPal

Scrivo di sicurezza digitale pratica e codice IA. Su ai riparo ogni procedura è verificata su macchine reali.

Tag
  • python,
  • foto,
  • opencv,
  • privacy,
  • script