Gemma 3 e Python: Crea un Organizzatore di Immagini
Pubblicato da Michele Saba
Introduzione e Obiettivi
Oggi vedremo come utilizzare Python sfruttando l’intelligenza artificiale del modello Gemma 3 di Google per creare un semplice organizzatore di immagini.
Il modello verrà scaricato ed eseguito in locale sul nostro computer, garantendo che le immagini non vengano mai inviate online e tutelando così la privacy dell’utente.
L'organizzatore di immagini, dato in ingresso il percorso di una cartelle contenente file jpeg, sarà in grado di riconoscere se queste contengano o meno delle persone (o dei volti), spostando queste immagini in una cartella PEOPLE (persone). Tutte le altre immagini verranno invece spostate in una seconda cartella OTHER (altro).
Si tratta di un esempio relativamente semplice, ma che vi permetterà di iniziare a prendere confidenza con l'impiego di questi modelli, permettendovi potenzialmente di impiegarli anche in altri contesti più sofisticati.
Nel mio caso farò girare questo script da Mac OS; tuttavia queste guida dovrebbe poter essere seguita da chiunque su qualsiasi sistema.
Di seguito un elenco degli argomenti trattati:
Cos'è Gemma 3?
Software Necessario: LM Studio
Come usare l'IA di Gemma 3 da Python
Codifica delle immagini in base64
Prompting: Interpretare il Contenuto delle Immagini
Processare un'intera cartella di immagini
Cos’è Gemma 3?
Gemma 3 è un modello multimodale sviluppato da Google. A differenza dei classici LLM (Large Language Model), è in grado anche di analizzare immagini e comprenderne il contenuto.
Ciò permette di affrontare task come l’estrazione di informazioni visive o testuali da un’immagine.
Uno dei vantaggi principali è che Gemma è un model open weight, quindi può essere scaricato, eseguito localmente e anche usato per fini commerciali grazie alla sua licenza permissiva.
Gemma è disponibile in vari formati, di cui 3 che offrono capacità multimodali: 4B, 12B e 27B parametri, dove questi numeri rappresentano il numero di parametri della rete espressi in miliardi di parametri (B sta per Billion). Per questo tutorial useremo il modello da 4B, che offre un buon bilanciamento tra prestazioni e requisiti di sistema, il che dovrebbe permettere un po' a tutti/e voi di eseguirlo tranquillamente nella vostra macchina. Dopotutto, per il semplice task che dobbiamo svolgere, questa versione è più che sufficiente!
La versione scelta è inoltre QAT (Quantization-Aware Training), che tiene conto del processo di quantizzazione già in fase di addestramento, migliorando così la qualità del modello compresso rispetto alle versioni quantizzate senza che il modello sia passato per questa fase di awareness durante il training.
Software Necessario: LM Studio
Per usare Gemma in locale impiegheremo LM Studio, un software con interfaccia grafica che permette di avviare e testare modelli direttamente dal proprio computer.
Una volta installato, LM Studio ci consente sia di interagire col modello via GUI sia di esporre un endpoint HTTP (in locale) per integrarlo nei nostri script Python.
All’interno di LM Studio, cerchiamo Gemma 3 e scarichiamo la versione desiderata (nel nostro caso, 4B QAT).

Una volta scaricato, il modello può essere caricato in memoria dal menù a tendina presente nella parte superiore.
Da qui potrete chattarci direttamente dall'interfaccia di LM Studio, oppure iniziare a comunicarci tramite un'API dedicata offerta dal programma.
Accedendo al tab "Developer" è infatti possibile esporre un endpoint su cui comunicare all'indirizzo http://127.0.0.1:1234
Tra i vari endpoint ai quali possiamo accedere, noi useremo /v1/chat/completions

Come usare l'IA di Gemma 3 da Python
Attraverso l’endpoint fornito da LM Studio, possiamo inviare richieste al modello nel formato atteso (prompt, testo / immagini, ecc.), e ricevere una risposta testuale.
Per ogni immagine da analizzare, invieremo:
- Un messaggio di sistema (system prompt) che definirà il comportamento atteso (nel nostro caso, la parola PEOPLE oppure OTHER)
- Un messaggio utente che contiene l’immagine (opportunamente codificata, come vedremo più avanti).
Prima di occuparci dell'invio di immagini, verifichiamo anzitutto di poter comunicare col nostro modello Gemma 3 da Python in formato puramente testuale.
Creiamo un ambiente virtuale, installiamo il modulo requests, e usiamo il seguente codice per chiedere a Gemma: "Ciao, come stai?"
Nel codice definiamo anzitutto l'endpoint sul quale invieremo le richieste, e quindi un system prompt dove definiamo il comportamento di "base" che ci aspettiamo dal nostro modello. Notate come il system prompt non sia strettamente necessario per poter comunicare con questi modelli di nuova generazione, già ottimizzati per ricevere istruzioni e comunicare nel formato chat; tuttavia ci sarà utile successivamente.
Creiamo quindi una funzione call_gemma_api(), dove definiamo una lista di messaggi formattati secondo il formato atteso dal nostro endpoint e da Gemma.
Notate le coppie "role" (nel nostro caso system / user) e "content".
Inviamo quindi una richiesta all'endpoint e mandiamo in print il risultato ottenuto.
import requests
GEMMA_API_ENDPOINT = "http://127.0.0.1:1234/v1/chat/completions"
SYSTEM_PROMPT = "You are a helpful assistant."
def call_gemma_api():
headers = {"Content-Type": "application/json"}
messages = [
{"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
{
"role": "user",
"content": [{"type": "text", "text": "Ciao, come stai?"}],
},
]
payload = {"messages": messages}
try:
print(f"Sending request to {GEMMA_API_ENDPOINT}")
response = requests.post(
GEMMA_API_ENDPOINT, headers=headers, json=payload, timeout=120
)
response.raise_for_status()
result = response.json()
print(result)
except requests.exceptions.RequestException as e:
print(f"Error calling API: {e}")
return None
except Exception as e:
print(f"An unexpected error occurred during API call: {e}")
return None
if __name__ == "__main__":
call_gemma_api()
Leggendo l'output dalla console, possiamo leggere tantissime informazioni.
Notiamo come la risposta vera e propria sia contenuta nella lista "choices"
"Ciao! Sto bene, grazie per avermelo chiesto! E tu, come stai oggi? 😊 \n\n(Hello! I'm fine, thank you for asking! And you, how are you doing today?)"
{
"id": "chatcmpl-n2f4m9qc1nao0bx57vqh6p",
"object": "chat.completion",
"created": 1750239150,
"model": "google/gemma-3-4b",
"choices": [
{
"index": 0,
"logprobs": None,
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": "Ciao! Sto bene, grazie per avermelo chiesto! E tu, come stai oggi? 😊 \n\n(Hello! I'm fine, thank you for asking! And you, how are you doing today?)",
},
}
],
"usage": {"prompt_tokens": 21, "completion_tokens": 44, "total_tokens": 65},
"stats": {},
"system_fingerprint": "google/gemma-3-4b",
}
Con queste informazioni possiamo modificare la nostra funzione per ottenere in return il messaggio in se, estraendolo tramite le chiavi dei dizionari e gli indici delle liste.
def call_gemma_api():
headers = {"Content-Type": "application/json"}
messages = [
{"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
{
"role": "user",
"content": [{"type": "text", "text": "Ciao, come stai?"}],
},
]
payload = {"messages": messages}
try:
print(f"Sending request to {GEMMA_API_ENDPOINT}")
response = requests.post(
GEMMA_API_ENDPOINT, headers=headers, json=payload, timeout=120
)
response.raise_for_status()
result = response.json()
if "message" in result["choices"][0]:
return result["choices"][0]["message"]["content"]
else:
print("Error: Could not find expected text key in API response.")
print("Response received:", result)
return None
except requests.exceptions.RequestException as e:
print(f"Error calling API: {e}")
return None
except Exception as e:
print(f"An unexpected error occurred during API call: {e}")
return None
Codifica delle immagini in base64
A questo punto siamo pronti per passare allo step successivo, ovvero l'invio di immagini.
Definiamo due nuove funzioni:
encode_image_to_base64(), che ricevendo il percorso di sistema di un'immagine presente nel nostro computer, restituisce una versione codificata in base64 della stessa immagine impiegando l'omonima libreria della Standard Library.
generate_encoded_image_url(), che ricevendo la rappresentazione in base64 di un'immagine, ci restituisce un data url per la stessa immagine.
Volendo, provate a mandare in print() i valori restituiti, così da farvi un'idea precisa di cosa invieremo esattamente a Gemma!
import base64
def encode_image_to_base64(image_path: str) -> Optional[str]:
print(f"Encoding image {image_path}...")
try:
with open(image_path, "rb") as image_file:
encoded_image = base64.b64encode(image_file.read())
print("Encoding completed!")
return encoded_image.decode("utf-8")
except Exception as e:
print(f"Failed to encode image: {e}")
return None
def generate_encoded_image_url(base64_image: str) -> str:
return f"data:image/jpeg;base64,{base64_image}"
Prompting: come interpretare il contenuto delle immagini
Siamo ora pronti per modificare il nostro codice per poter inviare le immagini a Gemma e per poter ricevere un'interpretazione del contenuto delle stesse, iniziando con l'invio di una singola immagine.
Per fare ciò:
- Modifichiamo la funzione call_gemma_api() così che possa ricevere uno "image_url" come parametro, che verrà inviato come parte della richiesta.
- Modifichiamo i valori del content del secondo messaggio inviato con "role": "user". type diventa image_url, e invece di text abbiamo ora image_url, opportunamente popolato nella seconda coppia chiave-valore del dizionario.
- Modifichiamo il SYSTEM_PROMPT per richiedere a Gemma di analizzare con cura le immagini che le verranno inviate, restituendo solo la parola PEOPLE qualora nell'immagine siano presenti persone o volti, oppure OTHER in caso contrario
- Nel blocco if __name__ == "__main__", definiziamo il percorso che porta ad un'immagine nel nostro computer, codifichiamo l'immagine, ne otteniamo il data url, e salviamo il valore dell'interpretazione fornita da Gemma in una variabile interpretation, che mandiamo successivamente in print
- Abbiamo inoltre aggiunto i type hints di Python, per rendere il codice più comprensibile (che in Python restano comunque OPZIONALI!)
Il system prompt ha in questo caso un ruolo fondamentale, perché ci permette di limitare e controllare l’output fornito dal modello, così da poterlo impiegare in modo efficace all’interno del nostro programma.
"You will be given an image to analyze carefully. If it contains people or human faces reply PEOPLE, otherwise reply OTHER. You must not include anything in the response except for either PEOPLE or OTHER based on the given image."
Che in italiano si traduce come
"Ti verrà fornita un'immagine da analizzare attentamente. Se contiene persone o volti umani, rispondi PEOPLE, altrimenti rispondi OTHER. Non devi includere nient'altro nella risposta se non PEOPLE oppure OTHER in base all'immagine fornita."
Capire questo meccanismo è essenziale: ci porta dritti al cuore del programma, consentendoci anche di modificarne facilmente il comportamento secondo le nostre esigenze. Con un prompt ben strutturato, infatti, potrete fare molto di più rispetto a una semplice classificazione binaria come questa! Potreste ad esempio chiedere a Gemma di segnalare tutte le immagini che contengono la torre di Pisa dopo una vacanza, oppure specifiche stringhe testuali presenti in documenti, ecc...
import base64
from typing import Optional
import requests
GEMMA_API_ENDPOINT = "http://127.0.0.1:1234/v1/chat/completions"
SYSTEM_PROMPT = (
"You will be given an image to analyze carefully. "
"If it contains people or human faces reply PEOPLE, "
"otherwise reply OTHER. You must not include anything "
"in the response except for either PEOPLE or OTHER based "
"on the given image."
)
def encode_image_to_base64(image_path: str) -> Optional[str]:
print(f"Encoding image {image_path}...")
try:
with open(image_path, "rb") as image_file:
encoded_image = base64.b64encode(image_file.read())
print("Encoding completed!")
return encoded_image.decode("utf-8")
except Exception as e:
print(f"Failed to encode image: {e}")
return None
def generate_encoded_image_url(base64_image: str) -> str:
return f"data:image/jpeg;base64,{base64_image}"
def call_gemma_api(image_url: str) -> Optional[str]:
headers = {"Content-Type": "application/json"}
messages = [
{"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
{
"role": "user",
"content": [{"type": "image_url", "image_url": {"url": image_url}}],
},
]
payload = {"messages": messages}
try:
print(f"Sending request to {GEMMA_API_ENDPOINT}")
response = requests.post(
GEMMA_API_ENDPOINT, headers=headers, json=payload, timeout=120
)
response.raise_for_status()
result = response.json()
if "message" in result["choices"][0]:
return result["choices"][0]["message"]["content"]
else:
print("Error: Could not find expected text key in API response.")
print("Response received:", result)
return None
except requests.exceptions.RequestException as e:
print(f"Error calling API: {e}")
return None
except Exception as e:
print(f"An unexpected error occurred during API call: {e}")
return None
if __name__ == "__main__":
image_path = "/Users/pymike00/Desktop/IMMAGINI/PEOPLE/5ra6stdh1we13e.jpg"
base64_image = encode_image_to_base64(image_path=image_path)
image_url = generate_encoded_image_url(base64_image=base64_image)
interpretation = call_gemma_api(image_url=image_url)
print(interpretation)
Processare un'intera cartella di immagini
Siamo ora pronti per estendere il comportamento del nostro piccolo programma in modo da analizzare un'intera cartella di immagini.
Per fare ciò creiamo una nuova funzione process_folder() che accetta come parametro folder_path, ovvero il percorso di sistema di una cartella contenente immagini.
La funzione:
- Verifica che il percorso passato sia effettivamente una cartella, tramite il modulo OS
- Crea due nuove cartelle PEOPLE e FOLDER (se non già presenti) come sottocartelle della cartella principale
- Crea una lista di tutti i file presenti nel percorso. Questo permetterà di rilanciare il programma successivamente senza che vengano incluse eventuali cartelle PEOPLE e FOLDER già li da una sessione di lavoro precedente
- Per ciascun file di tipo JPEG individuato, effettua l'encoding in base64, ne ottiene lo url e lo invia a Gemma, ottenendone l'interpretazione da parte del modello
- Sposta l'immagine nelle sottocartelle PEOPLE o OTHER tramite il modulo shutil, a seconda dell'interpretazione di Gemma
Modifichiamo inoltre il codice sotto la riga if __name__ == "__main__", per richiedere all'utente il percorso di una cartella e lanciare la funzione process_folder().
E ci siamo! Di seguito il codice completo.
Happy coding!
import base64
import os
import shutil
from typing import Optional
import requests
SUPPORTED_EXTENSIONS = (".jpg", ".jpeg")
GEMMA_API_ENDPOINT = "http://127.0.0.1:1234/v1/chat/completions"
SYSTEM_PROMPT = (
"You will be given an image to analyze carefully. "
"If it contains people or human faces reply PEOPLE, "
"otherwise reply OTHER. You must not include anything "
"in the response except for either PEOPLE or OTHER based "
"on the given image."
)
def encode_image_to_base64(image_path: str) -> Optional[str]:
print(f"Encoding image {image_path}...")
try:
with open(image_path, "rb") as image_file:
encoded_image = base64.b64encode(image_file.read())
print("Encoding completed!")
return encoded_image.decode("utf-8")
except Exception as e:
print(f"Failed to encode image: {e}")
return None
def generate_encoded_image_url(base64_image: str) -> str:
return f"data:image/jpeg;base64,{base64_image}"
def call_gemma_api(image_url: str) -> Optional[str]:
headers = {"Content-Type": "application/json"}
messages = [
{"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
{
"role": "user",
"content": [{"type": "image_url", "image_url": {"url": image_url}}],
},
]
payload = {"messages": messages}
try:
print(f"Sending request to {GEMMA_API_ENDPOINT}")
response = requests.post(
GEMMA_API_ENDPOINT, headers=headers, json=payload, timeout=120
)
response.raise_for_status()
result = response.json()
if "message" in result["choices"][0]:
return result["choices"][0]["message"]["content"]
else:
print("Error: Could not find expected text key in API response.")
print("Response received:", result)
return None
except requests.exceptions.RequestException as e:
print(f"Error calling API: {e}")
return None
except Exception as e:
print(f"An unexpected error occurred during API call: {e}")
return None
def process_folder(folder_path: str) -> None:
if not os.path.isdir(folder_path):
print(f"Error: Folder not found at {folder_path}")
return
people_folder = os.path.join(folder_path, "PEOPLE")
other_folder = os.path.join(folder_path, "OTHER")
os.makedirs(people_folder, exist_ok=True)
os.makedirs(other_folder, exist_ok=True)
print(f"Processing images in folder: {folder_path}")
print(f"Using API endpoint: {GEMMA_API_ENDPOINT}")
print("---")
print()
files_in_folder = [
f
for f in os.listdir(folder_path)
if os.path.isfile(os.path.join(folder_path, f))
]
for filename in files_in_folder:
if not filename.lower().endswith(SUPPORTED_EXTENSIONS):
print(f"Unsupported filename found in folder: {filename}")
print()
continue
image_path = os.path.join(folder_path, filename)
base64_image = encode_image_to_base64(image_path=image_path)
if base64_image:
image_url = generate_encoded_image_url(base64_image=base64_image)
interpretation = call_gemma_api(image_url)
print(f"Gemma Interpretation: {interpretation}")
if not interpretation in ["PEOPLE", "OTHER"]:
print(f"Unknown interpretation from Gemma: {interpretation}")
continue
if interpretation == "PEOPLE":
target_folder = people_folder
print(f"Moving {filename} to the PEOPLE folder")
else:
target_folder = other_folder
print(f"Moving {filename} to the OTHER folder")
target_path = os.path.join(target_folder, filename)
shutil.move(image_path, target_path)
print(f"Moved to: {target_path}")
print("---")
print()
print("Finished processing folder.")
if __name__ == "__main__":
info = "Image Organizer Powered by Gemma 3."
print("#" * len(info))
print(info)
print("#" * len(info))
print()
input_command = "Insert the folder path: "
images_folder_path = input(input_command)
print("-" * len(input_command + images_folder_path))
print()
process_folder(folder_path=images_folder_path)