Ricerca nel sito web

Classificazione degli oggetti con le CNN utilizzando la libreria Keras Deep Learning


Keras è una libreria Python per il deep learning che racchiude le potenti librerie numeriche Theano e TensorFlow.

Un problema difficile in cui le reti neurali tradizionali falliscono è chiamato riconoscimento degli oggetti. È qui che un modello è in grado di identificare gli oggetti nelle immagini.

In questo post scoprirai come sviluppare e valutare modelli di deep learning per il riconoscimento degli oggetti in Keras. Dopo aver completato questo tutorial, saprai:

  • Informazioni sul set di dati di classificazione degli oggetti CIFAR-10 e su come caricarlo e utilizzarlo in Keras
  • Come creare una semplice rete neurale convoluzionale per il riconoscimento degli oggetti
  • Come aumentare le prestazioni creando reti neurali convoluzionali più profonde

Avvia il tuo progetto con il mio nuovo libro Deep Learning With Python, che include tutorial passo passo e i file codice sorgente Python per tutti esempi.

Cominciamo.

  • Luglio/2016: prima pubblicazione
  • Aggiornamento ottobre 2016: aggiornato per Keras 1.1.0 e TensorFlow 0.10.0.
  • Aggiornamento marzo/2017: aggiornato per Keras 2.0.2, TensorFlow 1.0.1 e Theano 0.9.0.
  • Aggiornamento settembre/2019: API Keras 2.2.5 aggiornata.
  • Aggiornamento luglio/2022: aggiornato per l'API TensorFlow 2.x

Per un tutorial esteso sullo sviluppo di una CNN per CIFAR-10, vedere il post:

  • Come sviluppare una CNN da zero per la classificazione fotografica CIFAR-10

Descrizione del problema CIFAR-10

Il problema di classificare automaticamente le fotografie degli oggetti è difficile a causa del numero quasi infinito di permutazioni di oggetti, posizioni, illuminazione e così via. È un problema difficile.

Questo è un problema ben studiato nella visione artificiale e, più recentemente, un’importante dimostrazione della capacità del deep learning. Un set di dati standard di visione artificiale e deep learning per questo problema è stato sviluppato dal Canadian Institute for Advanced Research (CIFAR).

Il dataset CIFAR-10 è composto da 60.000 foto suddivise in 10 classi (da cui il nome CIFAR-10). Le classi includono oggetti comuni come aeroplani, automobili, uccelli, gatti e così via. Il set di dati viene suddiviso in modo standard, dove 50.000 immagini vengono utilizzate per addestrare un modello e le restanti 10.000 per valutarne le prestazioni.

Le foto sono a colori con componenti rosse, verdi e blu ma sono piccole e misurano 32 x 32 pixel quadrati.

Risultati all’avanguardia si ottengono utilizzando reti neurali convoluzionali molto grandi. Puoi conoscere i risultati all’avanguardia su CIFAR-10 sulla pagina web di Rodrigo Benenson. Le prestazioni del modello sono riportate in termini di accuratezza della classificazione, con prestazioni molto buone superiori al 90%, con prestazioni umane sul problema al 94% e risultati all'avanguardia al 96% al momento della stesura di questo articolo.

Esiste un concorso Kaggle che utilizza il set di dati CIFAR-10. È un buon posto per partecipare alla discussione sullo sviluppo di nuovi modelli per il problema e sulla scelta di modelli e script come punto di partenza.

Caricamento del set di dati CIFAR-10 in Keras

Il set di dati CIFAR-10 può essere facilmente caricato in Keras.

Keras ha la possibilità di scaricare automaticamente set di dati standard come CIFAR-10 e memorizzarli nella directory ~/.keras/datasets utilizzando la funzione cifar10.load_data(). Questo set di dati ha una dimensione di 163 megabyte, pertanto il download potrebbe richiedere alcuni minuti.

Una volta scaricato, le successive chiamate alla funzione caricheranno il dataset pronto per l'uso.

Il set di dati viene archiviato come set di training e test in pick, pronto per l'uso in Keras. Ogni immagine è rappresentata come una matrice tridimensionale, con dimensioni per rosso, verde, blu, larghezza e altezza. Possiamo tracciare le immagini direttamente usando matplotlib.

# Plot ad hoc CIFAR10 instances
from tensorflow.keras.datasets import cifar10
import matplotlib.pyplot as plt
# load data
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
# create a grid of 3x3 images
for i in range(0, 9):
	plt.subplot(330 + 1 + i)
	plt.imshow(X_train[i])
# show the plot
plt.show()

L'esecuzione del codice crea una trama 3×3 di fotografie. Le immagini sono state ingrandite rispetto alla piccola dimensione 32×32, ma puoi vedere chiaramente camion, cavalli e automobili. Puoi anche vedere qualche distorsione in alcune immagini che sono state forzate alle proporzioni quadrate.

Rete neurale convoluzionale semplice per CIFAR-10

Il problema CIFAR-10 viene risolto al meglio utilizzando una rete neurale convoluzionale (CNN).

Puoi iniziare rapidamente definendo tutte le classi e le funzioni di cui avrai bisogno in questo esempio.

# Simple CNN model for CIFAR-10
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Flatten
from tensorflow.keras.constraints import MaxNorm
from tensorflow.keras.optimizers import SGD
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import MaxPooling2D
from tensorflow.keras.utils import to_categorical
...

Successivamente è possibile caricare il set di dati CIFAR-10.

...
# load data
(X_train, y_train), (X_test, y_test) = cifar10.load_data()

I valori dei pixel vanno da 0 a 255 per ciascuno dei canali rosso, verde e blu.

È buona norma lavorare con dati normalizzati. Poiché i valori di input sono ben comprensibili, è possibile normalizzarli facilmente nell'intervallo da 0 a 1 dividendo ciascun valore per l'osservazione massima, ovvero 255.

Tieni presente che i dati vengono caricati come numeri interi, quindi devi convertirli in valori in virgola mobile per eseguire la divisione.

...
# normalize inputs from 0-255 to 0.0-1.0
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train = X_train / 255.0
X_test = X_test / 255.0

Le variabili di output sono definite come un vettore di numeri interi da 0 a 1 per ciascuna classe.

È possibile utilizzare una codifica one-hot per trasformarli in una matrice binaria per modellare al meglio il problema di classificazione. Esistono dieci classi per questo problema, quindi puoi aspettarti che la matrice binaria abbia una larghezza pari a 10.

...
# one hot encode outputs
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
num_classes = y_test.shape[1]

Cominciamo definendo una semplice struttura CNN come base di riferimento e valutando la sua efficacia nel risolvere il problema.

Utilizzerai una struttura con due livelli convoluzionali seguiti da un pooling massimo e da un appiattimento della rete a livelli completamente connessi per fare previsioni.

La struttura della rete di base può essere così riassunta:

  1. Livello di input convoluzionale, 32 mappe di caratteristiche con una dimensione di 3×3, una funzione di attivazione del raddrizzatore e un vincolo di peso della norma massima impostato su 3
  2. Abbandono impostato al 20%
  3. Strato convoluzionale, 32 mappe di caratteristiche con una dimensione di 3×3, una funzione di attivazione del raddrizzatore e un vincolo di peso della norma massima impostato su 3
  4. Strato Max Pool con dimensione 2×2
  5. Strato appiattito
  6. Strato completamente connesso con 512 unità e funzione di attivazione del raddrizzatore
  7. Abbandono impostato al 50%
  8. Livello di output completamente connesso con 10 unità e una funzione di attivazione softmax

Viene utilizzata una funzione di perdita logaritmica con l'algoritmo di ottimizzazione della discesa del gradiente stocastico configurato con un grande slancio e un inizio di decadimento del peso con un tasso di apprendimento di 0,01.

...
# Create the model
model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=(32, 32, 3), padding='same', activation='relu', kernel_constraint=MaxNorm(3)))
model.add(Dropout(0.2))
model.add(Conv2D(32, (3, 3), activation='relu', padding='same', kernel_constraint=MaxNorm(3)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(512, activation='relu', kernel_constraint=MaxNorm(3)))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
# Compile model
epochs = 25
lrate = 0.01
decay = lrate/epochs
sgd = SGD(learning_rate=lrate, momentum=0.9, decay=decay, nesterov=False)
model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
print(model.summary())

È possibile montare questo modello con 25 epoche e una dimensione batch di 32.

È stato scelto un numero limitato di epoche per mantenere in movimento questo tutorial. Di solito, il numero di epoche sarebbe di uno o due ordini di grandezza maggiore per questo problema.

Una volta che il modello è adatto, lo valuti sul set di dati di test e stampi l'accuratezza della classificazione.

...
# Fit the model
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=32)
# Final evaluation of the model
scores = model.evaluate(X_test, y_test, verbose=0)
print("Accuracy: %.2f%%" % (scores[1]*100))

Mettendo tutto insieme, l'esempio completo è elencato di seguito.

# Simple CNN model for the CIFAR-10 Dataset
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Flatten
from tensorflow.keras.constraints import MaxNorm
from tensorflow.keras.optimizers import SGD
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import MaxPooling2D
from tensorflow.keras.utils import to_categorical
# load data
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
# normalize inputs from 0-255 to 0.0-1.0
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train = X_train / 255.0
X_test = X_test / 255.0
# one hot encode outputs
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
num_classes = y_test.shape[1]
# Create the model
model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=(32, 32, 3), padding='same', activation='relu', kernel_constraint=MaxNorm(3)))
model.add(Dropout(0.2))
model.add(Conv2D(32, (3, 3), activation='relu', padding='same', kernel_constraint=MaxNorm(3)))
model.add(MaxPooling2D())
model.add(Flatten())
model.add(Dense(512, activation='relu', kernel_constraint=MaxNorm(3)))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
# Compile model
epochs = 25
lrate = 0.01
decay = lrate/epochs
sgd = SGD(learning_rate=lrate, momentum=0.9, decay=decay, nesterov=False)
model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
model.summary()
# Fit the model
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=32)
# Final evaluation of the model
scores = model.evaluate(X_test, y_test, verbose=0)
print("Accuracy: %.2f%%" % (scores[1]*100))

L'esecuzione di questo esempio fornisce i risultati seguenti. Innanzitutto viene riepilogata la struttura della rete, il che conferma che il progetto è stato implementato correttamente.

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #
=================================================================
 conv2d (Conv2D)             (None, 32, 32, 32)        896

 dropout (Dropout)           (None, 32, 32, 32)        0

 conv2d_1 (Conv2D)           (None, 32, 32, 32)        9248

 max_pooling2d (MaxPooling2D  (None, 16, 16, 32)       0
 )

 flatten (Flatten)           (None, 8192)              0

 dense (Dense)               (None, 512)               4194816

 dropout_1 (Dropout)         (None, 512)               0

 dense_1 (Dense)             (None, 10)                5130

=================================================================
Total params: 4,210,090
Trainable params: 4,210,090
Non-trainable params: 0
_________________________________________________________________

L'accuratezza e la perdita della classificazione vengono stampate dopo ogni epoca sia sui set di dati di training che di test.

Nota: i risultati possono variare a causa della natura stocastica dell'algoritmo o della procedura di valutazione o delle differenze nella precisione numerica. Considera l'idea di eseguire l'esempio alcune volte e confrontare il risultato medio.

Il modello viene valutato sul set di prova e raggiunge una precisione del 70,5%, che non è eccellente.

...
Epoch 20/25
1563/1563 [==============================] - 34s 22ms/step - loss: 0.3001 - accuracy: 0.8944 - val_loss: 1.0160 - val_accuracy: 0.6984
Epoch 21/25
1563/1563 [==============================] - 35s 23ms/step - loss: 0.2783 - accuracy: 0.9021 - val_loss: 1.0339 - val_accuracy: 0.6980
Epoch 22/25
1563/1563 [==============================] - 35s 22ms/step - loss: 0.2623 - accuracy: 0.9084 - val_loss: 1.0271 - val_accuracy: 0.7014
Epoch 23/25
1563/1563 [==============================] - 33s 21ms/step - loss: 0.2536 - accuracy: 0.9104 - val_loss: 1.0441 - val_accuracy: 0.7011
Epoch 24/25
1563/1563 [==============================] - 34s 22ms/step - loss: 0.2383 - accuracy: 0.9180 - val_loss: 1.0576 - val_accuracy: 0.7012
Epoch 25/25
1563/1563 [==============================] - 37s 24ms/step - loss: 0.2245 - accuracy: 0.9219 - val_loss: 1.0544 - val_accuracy: 0.7050
Accuracy: 70.50%

Puoi migliorare significativamente la precisione creando una rete molto più profonda. Questo è ciò che vedrai nella prossima sezione.

Rete neurale convoluzionale più ampia per CIFAR-10

Hai visto che una semplice CNN funziona male su questo problema complesso. In questa sezione, esaminerai l'aumento delle dimensioni e della complessità del tuo modello.

Progettiamo una versione approfondita della semplice CNN sopra. Puoi introdurre un ulteriore ciclo di convoluzioni con molte più mappe di funzionalità. Utilizzerai lo stesso modello dei livelli Convoluzionale, Dropout, Convoluzionale e Max Pooling.

Questo schema verrà ripetuto tre volte con mappe da 32, 64 e 128 caratteristiche. L'effetto è un numero crescente di mappe di caratteristiche con dimensioni sempre più piccole dati i livelli massimi di pooling. Infine, verrà utilizzato un livello Dense aggiuntivo e più grande all'estremità di output della rete nel tentativo di tradurre meglio il gran numero di mappe di caratteristiche in valori di classe.

Di seguito una sintesi della nuova architettura di rete:

  • Livello di input convoluzionale, 32 mappe di caratteristiche con una dimensione di 3×3 e una funzione di attivazione del raddrizzatore
  • Strato di abbandono al 20%
  • Strato convoluzionale, 32 mappe di caratteristiche con una dimensione di 3×3 e una funzione di attivazione del raddrizzatore
  • Strato Max Pool con dimensione 2×2
  • Strato convoluzionale, 64 mappe di caratteristiche con una dimensione di 3×3 e una funzione di attivazione del raddrizzatore
  • Strato di abbandono al 20%.
  • Strato convoluzionale, 64 mappe di caratteristiche con una dimensione di 3×3 e una funzione di attivazione del raddrizzatore
  • Strato Max Pool con dimensione 2×2
  • Strato convoluzionale, 128 mappe di caratteristiche con una dimensione di 3×3 e una funzione di attivazione del raddrizzatore
  • Strato di abbandono al 20%
  • Strato convoluzionale, 128 mappe di caratteristiche con una dimensione di 3×3 e una funzione di attivazione del raddrizzatore
  • Strato Max Pool con dimensione 2×2
  • Strato appiattito
  • Strato di abbandono al 20%
  • Strato completamente connesso con 1024 unità e funzione di attivazione del raddrizzatore
  • Strato di abbandono al 20%
  • Strato completamente connesso con 512 unità e funzione di attivazione del raddrizzatore
  • Strato di abbandono al 20%
  • Livello di output completamente connesso con 10 unità e una funzione di attivazione softmax

Puoi definire molto facilmente questa topologia di rete in Keras come segue:

...
# Create the model
model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=(32, 32, 3), activation='relu', padding='same'))
model.add(Dropout(0.2))
model.add(Conv2D(32, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D())
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
model.add(Dropout(0.2))
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D())
model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(Dropout(0.2))
model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D())
model.add(Flatten())
model.add(Dropout(0.2))
model.add(Dense(1024, activation='relu', kernel_constraint=MaxNorm(3)))
model.add(Dropout(0.2))
model.add(Dense(512, activation='relu', kernel_constraint=MaxNorm(3)))
model.add(Dropout(0.2))
model.add(Dense(num_classes, activation='softmax'))
# Compile model
epochs = 25
lrate = 0.01
decay = lrate/epochs
sgd = SGD(learning_rate=lrate, momentum=0.9, decay=decay, nesterov=False)
model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
model.summary()
...

È possibile adattare e valutare questo modello utilizzando la stessa procedura dall'alto e lo stesso numero di epoche ma una dimensione batch maggiore di 64, trovata attraverso alcuni esperimenti minori.

...
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=64)
# Final evaluation of the model
scores = model.evaluate(X_test, y_test, verbose=0)
print("Accuracy: %.2f%%" % (scores[1]*100))

Mettendo tutto insieme, l'esempio completo è elencato di seguito.

# Large CNN model for the CIFAR-10 Dataset
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Flatten
from tensorflow.keras.constraints import MaxNorm
from tensorflow.keras.optimizers import SGD
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import MaxPooling2D
from tensorflow.keras.utils import to_categorical
# load data
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
# normalize inputs from 0-255 to 0.0-1.0
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train = X_train / 255.0
X_test = X_test / 255.0
# one hot encode outputs
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
num_classes = y_test.shape[1]
# Create the model
model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=(32, 32, 3), activation='relu', padding='same'))
model.add(Dropout(0.2))
model.add(Conv2D(32, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D())
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
model.add(Dropout(0.2))
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D())
model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(Dropout(0.2))
model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D())
model.add(Flatten())
model.add(Dropout(0.2))
model.add(Dense(1024, activation='relu', kernel_constraint=MaxNorm(3)))
model.add(Dropout(0.2))
model.add(Dense(512, activation='relu', kernel_constraint=MaxNorm(3)))
model.add(Dropout(0.2))
model.add(Dense(num_classes, activation='softmax'))
# Compile model
epochs = 25
lrate = 0.01
decay = lrate/epochs
sgd = SGD(learning_rate=lrate, momentum=0.9, decay=decay, nesterov=False)
model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
model.summary()
# Fit the model
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=64)
# Final evaluation of the model
scores = model.evaluate(X_test, y_test, verbose=0)
print("Accuracy: %.2f%%" % (scores[1]*100))

L'esecuzione di questo esempio stampa l'accuratezza e la perdita della classificazione sui set di dati di training e test per ogni epoca.

Nota: i risultati possono variare a causa della natura stocastica dell'algoritmo o della procedura di valutazione o delle differenze nella precisione numerica. Considera l'idea di eseguire l'esempio alcune volte e confrontare il risultato medio.

La stima dell'accuratezza della classificazione per il modello finale è del 79,5%, ovvero nove punti in più rispetto al nostro modello più semplice.

...
Epoch 20/25
782/782 [==============================] - 50s 64ms/step - loss: 0.4949 - accuracy: 0.8237 - val_loss: 0.6161 - val_accuracy: 0.7864
Epoch 21/25
782/782 [==============================] - 51s 65ms/step - loss: 0.4794 - accuracy: 0.8308 - val_loss: 0.6184 - val_accuracy: 0.7866
Epoch 22/25
782/782 [==============================] - 50s 64ms/step - loss: 0.4660 - accuracy: 0.8347 - val_loss: 0.6158 - val_accuracy: 0.7901
Epoch 23/25
782/782 [==============================] - 50s 64ms/step - loss: 0.4523 - accuracy: 0.8395 - val_loss: 0.6112 - val_accuracy: 0.7919
Epoch 24/25
782/782 [==============================] - 50s 64ms/step - loss: 0.4344 - accuracy: 0.8454 - val_loss: 0.6080 - val_accuracy: 0.7886
Epoch 25/25
782/782 [==============================] - 50s 64ms/step - loss: 0.4231 - accuracy: 0.8487 - val_loss: 0.6076 - val_accuracy: 0.7950
Accuracy: 79.50%

Estensioni per migliorare le prestazioni del modello

Hai ottenuto buoni risultati su questo problema molto difficile, ma sei ancora ben lontano dal raggiungere risultati di livello mondiale.

Di seguito sono riportate alcune idee che puoi provare ad applicare ai modelli e a migliorarne le prestazioni.

  • Allenati per più epoche. Ogni modello è stato addestrato per un numero molto piccolo di epoche, 25. È comune addestrare grandi reti neurali convoluzionali per centinaia o migliaia di epoche. Dovresti aspettarti che i miglioramenti delle prestazioni possano essere ottenuti aumentando significativamente il numero di epoche di allenamento.
  • Aumento dei dati immagine. Gli oggetti nell'immagine variano nella loro posizione. Un altro incremento delle prestazioni del modello può probabilmente essere ottenuto utilizzando l’aumento dei dati. Metodi come la standardizzazione, gli spostamenti casuali o i capovolgimenti orizzontali delle immagini possono essere utili.
  • Topologia di rete più approfondita. La rete più grande presentata è profonda, ma per risolvere il problema potrebbero essere progettate reti più grandi. Ciò potrebbe comportare più mappe di funzionalità più vicine all'input e forse un pooling meno aggressivo. Inoltre, le topologie di rete convoluzionali standard che si sono rivelate utili possono essere adottate e valutate sul problema.

Riepilogo

In questo post hai scoperto come creare modelli di deep learning in Keras per il riconoscimento degli oggetti nelle fotografie.

Dopo aver seguito questo tutorial, hai imparato:

  • Informazioni sul set di dati CIFAR-10 e su come caricarlo in Keras e tracciare esempi ad hoc dal set di dati
  • Come addestrare e valutare una semplice rete neurale convoluzionale sul problema
  • Come espandere una semplice rete neurale convoluzionale in una rete neurale convoluzionale profonda per aumentare le prestazioni sul problema difficile
  • Come utilizzare l'aumento dei dati per ottenere un ulteriore impulso al difficile problema del riconoscimento degli oggetti

Hai qualche domanda sul riconoscimento degli oggetti o su questo post? Fai la tua domanda nei commenti e farò del mio meglio per rispondere.

Articoli correlati