Topic: Esperimento multithread per setacciare grandi quantità di immagini  (Letto 67 volte)

0 Utenti e 1 Visitatore stanno visualizzando questo topic.

Offline Badly

  • python unicellularis
  • *
  • Post: 27
  • Punti reputazione: 0
    • Mostra profilo
Salve a tutti, era un po' che non frequentavo il forum, a causa di numerosi impegni...
Vi spiego la mia situazione, magari qualcuno può chiarirmi bene e mettere una pezza alla mia ignoranza  :confused:.
Ho dovuto recuperare da un hdd rotto, delle foto, quindi ho recuperato i settori non danneggiati e trasferiti in un file.img
Tramite Scalpel (utilizzo linux mint), ho estratto più di 350GB di immagini, tra cui molte immagini che non servivano perché troppo piccole.
Rendendomi conto della quantità immonda di lavoro, ho pensato bene che non potevo controllare a mano 350GB di immagini  :D...
Ho creato così uno script, molto velocemente, scritto anche abbastanza male, ma che funziona e rispetta le mie esigenze:
In sostanza, ho 304 cartelle, con molti file jpg per ogni cartella.
Il programma cerca per ogni cartella, le immagini jpg "grandi" e funzionanti.
Vi allego il primo codice:

import os, time, shutil, tarfile #fnmatch
from PIL import Image
import shutil

all_files=[]
sext = []
temp = ""
print(time.strftime("%H|%M|%S"))

#chiedo all'utente che estensioni vuole cercare, in questo caso a me interessano i jpg, quindi inserisco: .jpg
#dopo aver scelto le estensioni, scrivo 0 e invio
while True:
    sext.append(input("cerca: "))
    if sext[-1] == "0":
        del sext[-1]
        break
sext = tuple(sext)

#naviga fra le cartelle di Files recuperati, e cerca tutti i file che finiscono con "sext", è una tupla di estensioni, in questo caso, mi interessa solo ".jpg"
for parent, directories, filenames in os.walk("/media/mionomeutente/PENNA USB/Files recuperati"):
    for x in filenames:
        if x.endswith(sext):
            fileDaAnalizzare = parent+'/'+x

            #apre il file con estensione specificata, e verifica che l'immagine abbia una certa grandezza
            try:
                im = Image.open(fileDaAnalizzare)
                width, height = im.size
                if(width > 350 and height >350):
                    document_path = os.path.join(parent,x)
                    print(document_path)

                    #copio semplicemente l'immagine che rispetta le mie esigenze nella cartella grandi
                    shutil.copy(document_path, '/media/mionomeutente/PENNA USB/grandi')
               
            except:
                pass
           

print(time.strftime("%H|%M|%S"))


Ecco, potevo anche fermarmi qui, il programma è molto basico, funziona e rispetta le mie esigenze, ma poi mi sono chiesto:
Se fossero invece più thread a fare questa operazione, e non uno, il tempo si ridurrebbe in modo significativo?
(ricordiamoci che sono 350GB che deve confrontare...)
Così, sempre velocemente (ed ovviamente scritto male), ho scritto lo stesso programma, modificato per funzionare a thread,
Posto il codice:


import re, os, threading, sys, shutil, random
from PIL import Image

nThreadz = 0
threadz = []

while (nThreadz <= 0):
    nThreadz = int(input("numero di thread: "))
all_files=[]
sext = []
temp = ""

while True:
    sext.append(input("cerca: "))
    if sext[-1] == "0":
        del sext[-1]
        break
sext = tuple(sext)   



listMatch = []

for parent, directories, filenames in os.walk("/media/mionomeutente/PENNA USB/Files recuperati"):
    listMatch.append(parent)


print("attualmente, %d siti" %len(listMatch))

class scan(threading.Thread):

    def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, daemon=None):
        threading.Thread.__init__(self, group=group, target=target, name=name, daemon=daemon)
        self.args = args
        self.kwargs = kwargs
        return
    def run(self):
        #print(self.args)
        global nThreadz, sext
        #non è detto che il numero di thread sia divisibile con i listMatch

        if (self.args != (nThreadz-1)):
            vadoDa = self.args*(len(listMatch)//nThreadz)
            vadoA = (self.args+1)*(len(listMatch)//nThreadz)
            #print("vado da "+str(vadoDa)+" a "+str(vadoA))
        else:
       
            vadoDa = self.args*(len(listMatch)//nThreadz)
            vadoA = len(listMatch)-1
            #print("vado da "+str(vadoDa)+" a "+str(vadoA))
           #ogni thread cerca in una cartella (304 il totale delle cartelle), e si divide il lavoro
       
        for percorso in listMatch[vadoDa:vadoA]:
            for parent, directories, filenames in os.walk(percorso):
                for x in filenames:
                    if x.endswith(sext):
                        fileDaAnalizzare = parent+'/'+x
                        #print(parent+'/'+x)
                        #apre il file con estensione specificata, e cerca quel pezzo di codice o frase
                        try:
                            im = Image.open(fileDaAnalizzare)
                            width, height = im.size

                            if(width > 350 and height >350):
                                document_path = os.path.join(parent,x)
                                #print('trovata: '+document_path)
                                if(not(os.path.exists('/media/mionomeutente/PENNA USB/grandi/'+x))):
                                    shutil.copy(document_path, '/media/mionomeutente/PENNA USB/grandi')
                                else:
                                    nomeRandom = random.randint(0,1000000000)
                                    nomeRandom = str(nomeRandom)+x
                                    shutil.copy(document_path, '/media/mionomeutente/PENNA USB/grandi/'+nomeRandom)

               
                        except:
                            pass
           

        #sys.exit(0)
           


for x in range(0,nThreadz):
    threadz.append(scan(args=(x)))


for x in range(0,nThreadz):
    threadz[x].start()


Il funzionamento è identico allo script precedente, con la differenza che posso scegliere la quantità di thread per "dividermi" il lavoro.. pensavo.
In realtà ho ottenuto dei risultati molto strani:
all'aumentare dei numeri di thred, i file setacciati non aumentavano in maniera significativa, ma.... diminuivano  :sarcastic:
1 thread = 3181 elementi in 30 secondi
2 thread = 648 elementi in 30 secondi
3 thread = 764 elementi in 30 secondi
304 thread = 166 elementi in 30 secondi
Spero che qualcuno possa trovare interessante il mio topic, e che questo "mistero" mi sia chiarito  ;)
(non escludo chiaramente la possibilità che io abbia commesso qualche errore)

Offline RicPol

  • python sapiens sapiens
  • ******
  • Post: 2.886
  • Punti reputazione: 9
    • Mostra profilo
Re:Esperimento multithread per setacciare grandi quantità di immagini
« Risposta #1 il: Novembre 25, 2019, 11:34 »
Abbi pazienza, ma è troppo scritto male e ingarbugliato per riuscire ad analizzarlo in poco tempo. Già i thread sono incasinati di loro, ma andare a usarli in quel modo vuol dire andarsela a cercare. Probabilmente stai facendo un errore grossolano, tipo avere una lista globale di file da analizzare che ogni thread svuota prima che gli altri thread possano arrivarci... e probabilmente qualcuno che ha l'occhio più lungo del mio riuscirà a beccare a una prima vista esattamente l'errore, nel groviglio di codice che hai scritto... Onestamente però io mi perdo nel garbuglio di variabili con nomi incomprensibili, uso di global, codice libero a livello di modulo... boh?

Il mio consiglio è: cancella completamente quello che hai scritto, e dimenticalo. Pensa per almeno una settimana a tutt'altro. Poi tornaci sopra. Definisci con chiarezza il tuo compito: scansire ricorsivamente una directory; fare un test su ciascun file; se il test è positivo, copiare quel file da un'altra parte. Ora, scrivi delle funzioni elementari che fanno ciascuna un pezzetto elementare del tuo compito. Anche dieci o quindici funzioni, se occorre: ma non deve esserci nessuna funzione che faccia DUE cose. Single responsability principle. Poi scrivi delle altre funzioni che compongono le funzioni elementari che hai scritto, fino a svolgere il tuo compito per intero. Non deve esserci neppure una singola riga di codice che resta a livello di modulo, tranne "if __name__==__main__: main()".
E questo per il caso del thread singolo. Quando hai fatto questo, e il tuo codice è chiaro, testato, documentato, rispecchia la pep8, allora puoi pensare a renderlo multi-thread. A questo punto, molto dipenderà dalla tua conoscenza di come funzionano i thread. Molte delle operazioni che fai sono I/O-bound, che rilasciano il GIL... quindi in effetti dovresti avere un vantaggio nei tempi di esecuzione. E del resto vedi bene che, nel tuo codice, il tempo invece resta sempre quello nonostante l'aumentare dei thread... cosa che, a parte il resto, ti segnala che qualcosa non va con la tua strategia. Un'idea (credo la più comune) sarebbe quella di accendere un nuovo task, ricorsivamente, per ciascuna nuova directory incontrata: puoi usare una Queue per mettere e prelevare i task... A questo punto crei un certo numero di worker threads che consumano i task nella Queue.

Off topic: guarda che il test che fai sulle immagini (altezza e larghezza), oltre che dispendioso (occorre aprire l'immagine con PIL...) è anche abbastanza inutile con delle immagini jpg che possono avere diversi livelli di compressione. Paradossalmente, un test molto più valido per valutare la qualità dell'immagine è semplicemente (e rapidamente) tenere solo le immagini che superano un certo "peso" in kb.

Offline Badly

  • python unicellularis
  • *
  • Post: 27
  • Punti reputazione: 0
    • Mostra profilo
Re:Esperimento multithread per setacciare grandi quantità di immagini
« Risposta #2 il: Novembre 27, 2019, 14:21 »
Probabilmente i miei professori di programmazione mi avrebbero sputato in faccia se avessi mostrato un codice del genere.
Ma era ad uso molto personale e mi serviva sul momento, ed anche alla svelta, non ho prestato attenzione a molte cose.
Ho utilizzato la famosa scuola di pensiero "basta che funzioni" (a discapito dell'eleganza e dell'efficienza).
Comunque riguardandolo un po' con calma, e riorganizzando in moduli, non ho trovato niente di strano, a parte che il valore iniziare della lista (nel threaded) era il path poi principale delle varie sottocartelle, ed effettivamente creava problemi al primo thread, ma anche risolvendo questo problema, rimane il collo di bottiglia.
I thread si dividono la lista equamente, ed in realtà si limitano solo a leggerla, senza svuotarla.
Se ad esempio la lista ha 100 percorsi, e ci sono 4 thread, ogni thread "scannerizza" 25 percorsi (diversi chiaramente da quelli dei fratelli).
Probabilmente con dei task avrei risolto subito, ma il mio intento era quello di farci un "esperimento" con dei thread.
Probabilmente ho tastato il limite della GIL di python con mano, mi sento di attribuire il problema soprattutto a questo.

Comunque, avevo pensato anche io a guardare semplicemente il peso, ma non ero sicuro che questa strategia fosse al 100% attendibile, anche perché in realtà non mi limito solo ai jpg, ma a tutto quello che scalpel riesce a tirarmi fuori dall'hard disk, grazie comunque per il suggerimento.
« Ultima modifica: Novembre 27, 2019, 14:27 da Badly »

Offline RicPol

  • python sapiens sapiens
  • ******
  • Post: 2.886
  • Punti reputazione: 9
    • Mostra profilo
Re:Esperimento multithread per setacciare grandi quantità di immagini
« Risposta #3 il: Novembre 28, 2019, 12:07 »
> Probabilmente i miei professori di programmazione mi avrebbero sputato in faccia se avessi mostrato un codice del genere.
E però postarlo in un forum e chiedere agli altri di darci un'occhiata invece è ok...

> Ho utilizzato la famosa scuola di pensiero "basta che funzioni"
Nella mia limitata esperienza, "basta che funzioni" coi thread non funziona mai... è proprio che i thread sono difficili

> Probabilmente ho tastato il limite della GIL
Mi sento di escluderlo tranquillamente. Come ho detto, la maggior parte di quello che fai è i/O e rilascia il GIL. So che scaricare la colpa sul GIL va sempre di moda, ma in effetti il GIL c'entra sorprendentemente poco con molti scenari comuni.

> Probabilmente con dei task avrei risolto subito
Può essere un'idea da esplorare, ma non è che l'architettura cambierebbe molto. E il problema del tuo codice è prima di tutto l'architettura. (Tra l'altro, per uno scenario del genere mi sentirei quasi di scommettere che una soluzione "ben fatta" coi thread sarebbe più veloce dell'equivalente in multiprocessing... in windows poi...)

> il mio intento era quello di farci un "esperimento" con dei thread
Eh sì, appunto per questo il mio consiglio resta di provare a implementare una architettura ben fatta... così vedi come funzionano i thread... e se ne vale la pena (di solito in scenari come il tuo vale la pena).

Offline Badly

  • python unicellularis
  • *
  • Post: 27
  • Punti reputazione: 0
    • Mostra profilo
Re:Esperimento multithread per setacciare grandi quantità di immagini
« Risposta #4 il: Novembre 28, 2019, 16:37 »
d'accordo
« Ultima modifica: Novembre 28, 2019, 16:43 da Badly »