codebox / mosaic

Python script for creating photomosaic images
https://codebox.net/pages/photo-mosaic-image-maker
MIT License
537 stars 166 forks source link

list index out of range #6

Closed danieldoyharzabal closed 7 years ago

danieldoyharzabal commented 7 years ago

Hi, im working with this script and i found this problem...

C:\Users\Daniel Doyharzabal\Desktop\Imagenes>python original.py nene_original.jpg C:\ImageOutput Reading tiles from 'C:\ImageOutput'... Processed 2391 tiles. Processing main image... Main image processed. Building mosaic, press Ctrl-C to abort... Process Process-2: Traceback (most recent call last): File "C:\Python27\lib\multiprocessing\process.py", line 258, in _bootstrap self.run() File "C:\Python27\lib\multiprocessing\process.py", line 114, in run self._target(self._args, self._kwargs) File "C:\Users\Daniel Doyharzabal\Desktop\Imagenes\original.py", line 122, in fit_tiles tile_index = tile_fitter.get_best_fit_tile(img_data) File "C:\Users\Daniel Doyharzabal\Desktop\Imagenes\original.py", line 105, in get_best_fit_tile diff = self.__get_tile_diff(img_data, tile_data, min_diff) File "C:\Users\Daniel Doyharzabal\Desktop\Imagenes\original.py", line 92, in __get_tile_diff diff += ((t1[i][0] - t2[i][0])2 + (t1[i][1] - t2[i][1])2 + (t1[i][2] - t2[i][2])2) IndexError: list index out of range Process Process-3: Traceback (most recent call last): File "C:\Python27\lib\multiprocessing\process.py", line 258, in _bootstrap self.run() File "C:\Python27\lib\multiprocessing\process.py", line 114, in run self._target(self._args, self._kwargs) File "C:\Users\Daniel Doyharzabal\Desktop\Imagenes\original.py", line 122, in fit_tiles tile_index = tile_fitter.get_best_fit_tile(img_data) File "C:\Users\Daniel Doyharzabal\Desktop\Imagenes\original.py", line 105, in get_best_fit_tile diff = self.__get_tile_diff(img_data, tile_data, min_diff) File "C:\Users\Daniel Doyharzabal\Desktop\Imagenes\original.py", line 92, in __get_tile_diff diff += ((t1[i][0] - t2[i][0])2 + (t1[i][1] - t2[i][1])2 + (t1[i][2] - t2[i][2])2) IndexError: list index out of range Process Process-4: Traceback (most recent call last): File "C:\Python27\lib\multiprocessing\process.py", line 258, in _bootstrap self.run() File "C:\Python27\lib\multiprocessing\process.py", line 114, in run self._target(*self._args, self._kwargs) File "C:\Users\Daniel Doyharzabal\Desktop\Imagenes\original.py", line 122, in fit_tiles tile_index = tile_fitter.get_best_fit_tile(img_data) File "C:\Users\Daniel Doyharzabal\Desktop\Imagenes\original.py", line 105, in get_best_fit_tile diff = self.__get_tile_diff(img_data, tile_data, min_diff) File "C:\Users\Daniel Doyharzabal\Desktop\Imagenes\original.py", line 92, in __get_tile_diff diff += ((t1[i][0] - t2[i][0])2 + (t1[i][1] - t2[i][1])2 + (t1[i][2] - t2[i][2])2) IndexError: list index out of range

The error does not always appear. At first I thought that by removing the pyc file generated by the interpreter, the error would be solved, but it was not.

I'm working on Windows 10, with python 2.7 64 bit.

codebox commented 7 years ago

Thanks for letting me know about this problem. There are a few things that might cause this, it would be helpful to know the following values when the error happens: len(t1), len(t2), i, len(t1[i]), len(t2[i]) if you can get me these I can investigate further

danieldoyharzabal commented 7 years ago

I could solve the problem with a little help, also add a small GUI with TKinter, I leave the code ... greetings

# -*- coding: utf-8 -*-
"""
Created on Thu Mar 09 19:59:07 2017

@author: Daniel Doyharzabal
"""

import sys, os, tkFileDialog, unicodedata
from multiprocessing import Process, Queue, cpu_count
from Tkinter import *
from PIL import Image
WORKER_COUNT = max(cpu_count() - 1, 1) #selecciona el maximo entre 1 y 1 menos el numero de procesador 
EOQ_VALUE = None #inicializa una variable como un objeto del tipo None (nada)    
salida = 'mosaico.jpeg' #da le nombre al archivo que se crea

class TileProcessor:
    def __init__(self, tiles_directory, tamanio, ENLARGEMENT, TILE_BLOCK_SIZE):
        self.tiles_directory = tiles_directory
        self.tamanio = tamanio
        self.ENLARGEMENT = ENLARGEMENT
        self.TILE_BLOCK_SIZE = TILE_BLOCK_SIZE

    def __process_tile(self, tile_path):
        try:
            img = Image.open(tile_path)
            # los mosaicos deben ser cuadrados, entonces seleciona el minimo entre largo y ancho y los recorta
            w = img.size[0]
            h = img.size[1]
            min_dimension = min(w, h)
            w_crop = (w - min_dimension) / 2
            h_crop = (h - min_dimension) / 2 
            img = img.crop((w_crop, h_crop, w - w_crop, h - h_crop))

            large_tile_img = img.resize((self.tamanio, self.tamanio), Image.ANTIALIAS)
            small_tile_img = img.resize((self.tamanio/self.TILE_BLOCK_SIZE, self.tamanio/self.TILE_BLOCK_SIZE), Image.ANTIALIAS)

            return (large_tile_img.convert('RGB'), small_tile_img.convert('RGB'))
        except:
            return (None, None)

    def get_tiles(self):
        large_tiles = []
        small_tiles = []

        print 'Leyendo mosaicos de \'%s\'...' % (self.tiles_directory, )

        # se buscan los mosaicos recursivamente en las subcarpetas de la direccion
        for root, subFolders, files in os.walk(self.tiles_directory):
            for tile_name in files:
                tile_path = os.path.join(root, tile_name)
                large_tile, small_tile = self.__process_tile(tile_path)
                if large_tile and small_tile:
                    large_tiles.append(large_tile)
                    small_tiles.append(small_tile)

        print 'Se procesaron %s mosaicos.' % (len(large_tiles),)
        return (large_tiles, small_tiles)

class TargetImage:
    def __init__(self, image_path, ENLARGEMENT, TILE_BLOCK_SIZE, tamanio):
        self.image_path = image_path
        self.ENLARGEMENT=ENLARGEMENT
        self.TILE_BLOCK_SIZE=TILE_BLOCK_SIZE
        self.tamanio=tamanio
    def get_data(self):
        print 'Procesando imagen principal...'
        img = Image.open(self.image_path)
        w = img.size[0] * self.ENLARGEMENT  #tamaño ancho
        h = img.size[1]    * self.ENLARGEMENT #tamaño alto
        large_img = img.resize((w, h), Image.ANTIALIAS) #retorna una copia cambiada de tamaño y filtrada con antialias
        w_diff = (w % self.tamanio)/2
        h_diff = (h % self.tamanio)/2

        # si es necesario recorta la imagen ligeramente para utilizar un numero entero de mosaicos horizontales y verticales
        if w_diff or h_diff:
            large_img = large_img.crop((w_diff, h_diff, w - w_diff, h - h_diff)) #recorta la imagen de cada lado es decir 4 datos necesarios
        small_img = large_img.resize((w/self.TILE_BLOCK_SIZE, h/self.TILE_BLOCK_SIZE), Image.ANTIALIAS)
        image_data = (large_img.convert('RGB'), small_img.convert('RGB'))
        print 'Imagen principal procesada.'
        return image_data

class TileFitter:
    def __init__(self, tiles_data):
        self.tiles_data = tiles_data

    def __get_tile_diff(self, t1, t2, bail_out_value):
        diff = 0
        if len(t1) - len(t2): return -1 #parcheado
        for i in range(len(t1)):

            diff += ((t1[i][0] - t2[i][0])**2 + (t1[i][1] - t2[i][1])**2 + (t1[i][2] - t2[i][2])**2)
            if diff > bail_out_value:
                # sabemos que este no es el mejor encaje, entonces no tiene sentido continuar con este mosaico
                return diff 
        return diff

    def get_best_fit_tile(self, img_data):
        best_fit_tile_index = None
        min_diff = sys.maxint
        tile_index = 0

        # va a traves de cada mosaico buscando el mejor encaje para la cuadricula de la imagen representada en 'img_data'
        for tile_data in self.tiles_data:
            diff = self.__get_tile_diff(img_data, tile_data, min_diff)
            if 0 < diff < min_diff: #cambio
                min_diff = diff
                best_fit_tile_index = tile_index
            tile_index += 1

        return best_fit_tile_index

def fit_tiles(work_queue, result_queue, tiles_data):
    # Esta función se ejecuta por los procesos de trabajo, uno en cada núcleo de la CPU
    tile_fitter = TileFitter(tiles_data)

    while True:
        try:
            img_data, img_coords = work_queue.get(True)
            if img_data == EOQ_VALUE:
                break
            tile_index = tile_fitter.get_best_fit_tile(img_data)
            if tile_index:
                result_queue.put((img_coords, tile_index))
        except KeyboardInterrupt:
            break

    # deja que result handler sepa que este procesador ha terminado
    result_queue.put((EOQ_VALUE, EOQ_VALUE))

class ProgressCounter:
    def __init__(self, total):
        self.total = total
        self.counter = 0

    def update(self):
        self.counter += 1
        sys.stdout.write("Progress: %s%% %s" % (100 * self.counter / self.total, "\r"))
        sys.stdout.flush();

class MosaicImage:
    def __init__(self, original_img, tamanio):
        self.tamanio=tamanio

        self.image = Image.new(original_img.mode, original_img.size)
        self.x_tile_count = original_img.size[0] / self.tamanio
        self.y_tile_count = original_img.size[1] / self.tamanio
        self.total_tiles  = self.x_tile_count * self.y_tile_count

    def add_tile(self, tile_data, coords):
        img = Image.new('RGB', (self.tamanio, self.tamanio))
        img.putdata(tile_data)
        self.image.paste(img, coords)

    def save(self, path):
        self.image.save(path)

def build_mosaic(result_queue, all_tile_data_large, original_img_large, tamanio):

    mosaic = MosaicImage(original_img_large, tamanio)

#  parcheado   active_workers = WORKER_COUNT

    while True:
        try:
            img_coords, best_fit_tile_index = result_queue.get()

            if img_coords == EOQ_VALUE:

#  parcheado              active_workers -= 1
#  parcheado              if not active_workers:
#  parcheado                  break
                break
            else:

                tile_data = all_tile_data_large[best_fit_tile_index]
                mosaic.add_tile(tile_data, img_coords)

        except KeyboardInterrupt:
            break
#  parcheado     pass

    mosaic.save(salida)
    print '\nFinalizado, la imagen de salida se encuentra en', salida

def terminal(original_img, tiles, taman, TILE_BLOCK): # terminal determina la parte visual del programa en la terminal de windows
    print 'Construyendo imagen, Ctrl+C para abortar...'
    original_img_large, original_img_small = original_img
    tiles_large, tiles_small = tiles

    mosaic = MosaicImage(original_img_large, taman)

    all_tile_data_large = map(lambda tile : list(tile.getdata()), tiles_large)
    all_tile_data_small = map(lambda tile : list(tile.getdata()), tiles_small)

    work_queue   = Queue(WORKER_COUNT)    
    result_queue = Queue()

    try:

        # comienza el proceso de construccion del mosaico
        p=Process(target=build_mosaic, args=(result_queue, all_tile_data_large, original_img_large, taman))

        p.start()

        # comienza el proceso de encaje de los mosaicos
        for n in range(WORKER_COUNT):
            Process(target=fit_tiles, args=(work_queue, result_queue, all_tile_data_small)).start()
            print("Proceso de construccion inicializado...")

        progress = ProgressCounter(mosaic.x_tile_count * mosaic.y_tile_count)

        for x in range(mosaic.x_tile_count):
            for y in range(mosaic.y_tile_count):
                large_box = (x * taman, y * taman, (x + 1) * taman, (y + 1) * taman)
                small_box = (x * taman/TILE_BLOCK, y * taman/TILE_BLOCK, (x + 1) * taman/TILE_BLOCK, (y + 1) * taman/TILE_BLOCK)
                work_queue.put((list(original_img_small.crop(small_box).getdata()), large_box))
                progress.update()

    except:
        print '\nGuardando imagen parcial, espere...'

    finally:

        # pone estos valores especiales en cola para dejar saber a los CPU que pueden terminar
        for n in range(WORKER_COUNT):
            work_queue.put((EOQ_VALUE, EOQ_VALUE))

def mosaic(): # definicion del metodo mosaic
    tamanio   = int(entero1.get())# alto y ancho de los mosaicos en pixeles
    TILE_MATCH_RES = int(entero2.get())    # resolulcion de encaje de mosaicos (valores altos mejoran el resultado pero requieren mas procesamiento)
    ENLARGEMENT    = int(entero3.get())    # la imagen mosaico sera esta cantidad de veces mas grande (en largo y ancho) que la imagen original    
    TILE_BLOCK_SIZE = tamanio / max(min(TILE_MATCH_RES, tamanio), 1) #tamaño de bloque de mosaico
    path=tkFileDialog.askdirectory() #seleccionar carpeta de mosaicos
    label_6.config(text=path)
    archivo=tkFileDialog.askopenfilename() #seleccionar que archivo abrir como imagen principal
    label_7.config(text=archivo)
    tiles_path=path 
    img_path=archivo 

    tiles_data = TileProcessor(tiles_path, tamanio, ENLARGEMENT, TILE_BLOCK_SIZE).get_tiles() # utiliza la direccion de los mosaicos de entrada para iniciar el procesamiento

    image_data = TargetImage(img_path, ENLARGEMENT, TILE_BLOCK_SIZE, tamanio).get_data()  #
    terminal(image_data, tiles_data, tamanio, TILE_BLOCK_SIZE)

#-------------------------- Interfaz Grafica ----------------------------------------------#
root=Tk()
root.title("Fotomosaico con Python")
root.geometry('650x400+200+200')

label_1= Label(root, text="Recortar Mosaicos")
label_2= Label(root, text="Resolucion Mosaicos")
label_3= Label(root, text="Agrandamiento")
label_4= Label(root, text="Direccion de Mosaicos")
label_5= Label(root, text="Direccion de Imagen")
label_6= Label(root, text="...")
label_7= Label(root, text="...")
label_8= Label(root, text="...")

x=StringVar()
y=StringVar()
z=StringVar()

entero1= Entry(root, textvariable='x')
entero2= Entry(root, textvariable='y')
entero3= Entry(root, textvariable='z')

label_1.grid(row=0, column=0, sticky=E)
label_2.grid(row=1, column=0, sticky=E)
label_3.grid(row=2, column=0, sticky=E)
label_4.grid(row=3, column=0, sticky=E)
label_5.grid(row=4, column=0, sticky=E)
label_6.grid(row=3, column=1, sticky=W) #direccion mosaicos
label_7.grid(row=4, column=1, sticky=W) #direccion imagen

entero1.grid(row=0, column=1)
entero2.grid(row=1, column=1)
entero3.grid(row=2, column=1)

boton_3=Button(root, text="Aceptar", command= mosaic )
boton_3.grid(column= 4, row=5)

if __name__ == '__main__': #verifica si el modulo ha sido importado o si es ejecutado como modulo principal (main)

    root.mainloop() #ejecuta la GUI