In [1]:
from __future__ import print_function
import numpy as np
In [2]:
%%javascript
// Añadimos script que nos permite mostrar notificaciones bootstrap-growl
$('head').append('<script src="../../rsc/bootstrap-growl/jquery.bootstrap-growl.min.js">');
$('head').append('<link rel="stylesheet" href="Style.css">');
In [3]:
import ipywidgets as widgets
from traitlets import Unicode, validate, Bool, CBool, Any, List, observe
from ipywidgets import Color
from skimage import io
from skimage.transform import resize
class ImageLabelerWidget(widgets.DOMWidget):
_view_name = Unicode('ImageLabelerView').tag(sync=True)
_view_module = Unicode('ImageLabeler').tag(sync=True)
# Atributos sincronos entre js y python
image = Unicode('blank.png').tag(sync=True)
rect = List().tag(sync=True)
rect_to_add = List().tag(sync=True)
text_to_add = List().tag(sync=True)
removed_rect = List().tag(sync=True)
# Intento de carga de imágen incorrecto notificación
error_loading = Unicode('').tag(sync=True)
# Imagen guardada correctamente notificación
saved_successfuly = Unicode('').tag(sync=True)
# Texto a incluir en el rectangulo
text = Unicode('').tag(sync=True)
Phytolith_type = Any().tag(sync=True)
def __init__(self, *pargs, **kwargs):
super(widgets.DOMWidget, self).__init__(*pargs, **kwargs)
self._rectList = []
self._removed_rectList = []
# Para patron observer
self._rectList_observers = []
self._removed_rectList_observers = []
'''Método que se encarga de recibir la notificación
de la adicción de unas nuevas coordenadas a la imagen'''
@observe('rect')
def new_rect(self, change):
rec = change["new"]
self._rectList.append(rec)
# Callback a observers
for callback in self._rectList_observers:
callback(self._rectList)
'''Método que se encarga de recibir la notificación
de la eliminación de unas nuevas coordenadas a la imagen'''
@observe('removed_rect')
def old_rect(self, change):
removed_rec = change["new"]
#Cambiamos indices
self._removed_rectList.append(removed_rec)
# Callback a observers
for callback in self._removed_rectList_observers:
callback(self._removed_rectList)
'''Método para enlazar al observer con
el atributo rectList'''
def rectList_bind_to(self, callback):
self._rectList_observers.append(callback)
'''Método para enlazar al observer con
el atributo removed_rect'''
def removed_rect_bind_to(self, callback):
self._removed_rectList_observers.append(callback)
def get_rectList(self):
return self._rectList
def set_rectList(self, value):
self._rectList = value
In [4]:
import csv, json
from datetime import datetime
''' Rescalamos las coordenadas para que coincidan
las coordenadas de la vista con las de la imagen original'''
def transform_view_coords_to_image_coords(coords, svg_h, svg_w, real_image_h, real_image_w):
# Rescalado de la imagen en la vista
if real_image_h > real_image_w:
coef = real_image_h / svg_h
svg_image_h = svg_h
svg_image_w = round(real_image_w / coef)
else:
coef = real_image_w / svg_w
svg_image_w = svg_w
svg_image_h = round(real_image_h / coef)
# Coeficientes de rescalado
coef_h = real_image_h / svg_image_h
coef_w = real_image_w / svg_image_w
# Calculamos padding en función de
# los valores de ancho y alto de la imagen
# en la vista
svg_padding_h = (svg_h - svg_image_h) / 2
svg_padding_w = (svg_w - svg_image_w) / 2
#TODO comprobar que las coordenadas no se salen de la imagen
new_coords = []
# Por cada rectangulo generamos una nueva imagen
# siempre que se encuentre dentro de la imagen
for rect_x, rect_y, rect_height, rect_width in coords:
rect_x = round((rect_x - svg_padding_h) * coef_h)
rect_y = round((rect_y - svg_padding_w) * coef_w)
rect_height = round(rect_height * coef_h)
rect_width = round(rect_width * coef_w)
rect_x2 = rect_x+rect_height
rect_y2 = rect_y+rect_width
if rect_x > 0 and rect_x2 < real_image_h \
and rect_y2 < real_image_w and rect_y > 0:
new_coords.append([rect_x, rect_x2, rect_y, rect_y2])
return new_coords
In [5]:
''' Rescalamos las coordenadas para que coincidan
las coordenadas de la vista con las de la imagen original'''
def transform_image_coords_to_view_coords(coords, svg_h, svg_w, real_image_h, real_image_w):
# Rescalado de la imagen en la vista
if real_image_h > real_image_w:
coef = real_image_h / svg_h
svg_image_h = svg_h
svg_image_w = round(real_image_w / coef)
else:
coef = real_image_w / svg_w
svg_image_w = svg_w
svg_image_h = round(real_image_h / coef)
# Coeficientes de rescalado
coef_h = real_image_h / svg_image_h
coef_w = real_image_w / svg_image_w
# Calculamos padding en función de
# los valores de ancho y alto de la imagen
# en la vista
svg_padding_h = (svg_h - svg_image_h) / 2
svg_padding_w = (svg_w - svg_image_w) / 2
new_coords = []
# Por cada rectangulo generamos una nueva imagen
# siempre que se encuentre dentro de la imagen
for rect_x, rect_y, rect_height, rect_width in coords:
rect_x = round((rect_x / coef_h) + svg_padding_h)
rect_y = round((rect_y / coef_w) + svg_padding_w)
rect_height = round(rect_height / coef_h)
rect_width = round(rect_width / coef_w)
new_coords.append([rect_x, rect_y, rect_height, rect_width])
return new_coords
In [6]:
def additional_text():
# Timestamp for jpg files
time = datetime.today()
year = time.year
month = time.month
day = time.day
hour = time.hour
minute = time.minute
return str(year) + '_' + str(month) + '_' + str(day) \
+ '_' + str(hour) + '_' + str(minute)
'''Función que genera las imágenes a partir de las coordenadas'''
def save_coords_as_images(coords, image, dest_path, image_name):
# Por cada conjunto de rectangulos generamos una nueva imagen
for rect_x, rect_x2, rect_y, rect_y2 in coords:
name = image_name + str(rect_x) + \
'X'+ str(rect_x2) + 'X2'+str(rect_y) \
+ 'Y' + str(rect_y2) + 'Y2' + ".jpg"
img = image[rect_x:rect_x2,rect_y:rect_y2]
io.imsave(dest_path+name, img)
'''Función que genera un fichero CSV a partir de las coordenadas'''
def save_coords_as_csv(coords, dest_path, image_name):
with open(dest_path+image_name+'.csv', 'w', newline='') as csvfile:
coords_writer = csv.writer(csvfile, delimiter=',',
quotechar='|', quoting = csv.QUOTE_MINIMAL)
# Por cada conjunto de rectangulos generamos una nueva imagen
for rect_x, rect_x2, rect_y, rect_y2 in coords:
coords_writer.writerow([rect_x,rect_x2,rect_y,rect_y2])
'''Función que genera un fichero JSON a partir de las coordenadas'''
def save_coords_as_json(coords, dest_path, image_name):
with open(dest_path + image_name+'.json', 'w') as jsonfile:
coords_dict = dict()
coords_dict[image_name+'.jpg'] = coords
json.dump(coords_dict, jsonfile)
'''Función que genera todos los ficheros anteriores'''
def save_as_all_formats(coords, image, dest_path, image_name):
save_coords_as_images(coords, image, dest_path, image_name)
save_coords_as_csv(coords, dest_path, image_name)
save_coords_as_json(coords, dest_path, image_name)
In [7]:
%%javascript
//Añadimos estilo a las notificaciones:
var error_type ='danger';
var success_type ='success';
var info_type = 'info';
var notify_style = {
ele: 'body',
offset: {from: 'bottom', amount: 20},
align: 'left',
width: 'auto',
delay: 4000,
allow_dismiss: true,
stackup_spacing: 10
}
function notify(notify_string, notify_style, notify_type){
notify_style['type'] = notify_type;
$.bootstrapGrowl(notify_string, notify_style);
}
// JS Widget
//require.undef('ImageLabeler');
// Definición del Widget en javasript
define('ImageLabeler', ["jupyter-js-widgets"], function(widgets) {
var svg;
var startX, startY;
var count = 0;
var rect = null;
var model;
var myThis;
var ImageLabelerView = widgets.DOMWidgetView.extend({
// Renderizar vista
render: function() {
model = this.model;
myThis = this;
// Creamos el SVG
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.id = 'svg';
svg.setAttribute('height', '500px');
svg.setAttribute('width', '700px');
// Asignamos listeners al SVG;
svg.addEventListener("click", this.mouseClick, null);
svg.addEventListener("mousemove", this.mouseMove, null);
// Imágen
var src = "blank.png";
this.loadImage(src);
// Listener para cambios de imagen
this.image_changed();
this.model.on('change:image', this.image_changed, this);
// Listener para dibujar rectangulos dados
// desde Python
this.model.on('change:rect_to_add', this.add_rect, this);
// Listener para intento de cambio de imagen erroneo
this.model.on('change:error_loading', this.error_loading_image, this);
// Listener para notificar guardado de las imágenes
this.model.on('change:saved_successfuly', this.success_saving_labels, this);
// Asignamos a la vista el elemento SVG
this.el = svg;
},
mouseClick: function(eve) {
if (rect !== null) {
//Cada vez que finalizamos el rectangulo
// , le añadimos
model.set('rect',[rect.y['baseVal'].value,
rect.x['baseVal'].value,
rect.height['baseVal'].value,
rect.width['baseVal'].value]);
myThis.add_text(rect.x['baseVal'].value,
rect.y['baseVal'].value);
myThis.touch();
rect = null;
} else {
var text = $(event.target);
if (text.html() != "x"){
var pos = myThis.getMousePos(eve);
startX= pos.x;
startY = pos.y;
if(!rect){
rect = document.createElementNS(
"http://www.w3.org/2000/svg", 'rect');
rect.id = 'rect' + count;
rect.setAttribute('class', 'rect');
count = count + 1;
myThis.updateRect(eve, rect);
myThis.setStyleRect(rect);
svg.appendChild(rect);
}
}else{
var closeId = text.attr("id");
var id = closeId[closeId.length - 1];
var closeParentId = "closeParent" + id;
var rectId = "rect" + id;
var textId = id + "txt";
//Obtenemos coordenadas del rectangulo
var removed_rect = document.getElementById(rectId);
// Transmitimos al kernel, python, el
// rectangulo a eliminar
model.set('removed_rect',[removed_rect.y['baseVal'].value,
removed_rect.x['baseVal'].value,
removed_rect.height['baseVal'].value,
removed_rect.width['baseVal'].value]);
myThis.touch();
// Eliminamos todo lo relativo al rectangulo
document.getElementById(closeParentId).remove();
document.getElementById(rectId).remove();
document.getElementById(textId).remove();
}
}
},
getMousePos: function(evt) {
var htmlPos = svg.getBoundingClientRect();
return {
x: evt.clientX - htmlPos.left,
y: evt.clientY - htmlPos.top
};
},
updateRect: function(eve, rect){
var pos = this.getMousePos(eve);
var endX = pos.x;
var endY = pos.y;
if (endX - startX > 0){
var x = startX
}else{
var x = endX;
}
if (endY - startY > 0){
var y = startY
}else{
var y = endY;
}
var width = Math.abs(endX - startX);
var height = Math.abs(endY - startY);
rect.setAttributeNS(null, 'x', x);
rect.setAttributeNS(null, 'y', y);
rect.setAttributeNS(null, 'height', height);
rect.setAttributeNS(null, 'width', width);
},
mouseMove: function(eve) {
if(rect){
myThis.updateRect(eve, rect);
}
},
// Metodo para la carga y cambio de imagen
loadImage: function(src){
$("rect").remove();
$(".text").remove();
$(".close").remove();
svg.innerHTML = '<image id="image" xlink:href="'+
src +'" x="0" y="0" height="500px" width="700px"/>'
},
setStyleRect: function(rect){
rect.setAttributeNS(null, 'fill', 'transparent');
rect.setAttributeNS(null, 'stroke', 'green');
rect.setAttributeNS(null, 'linewidth', '10px');
},
// Metodo para añadir texto a los rectangulos
add_text: function(x, y){
var text = myThis.model.get('text');
// Añadimos la etiqueta de texto que deseemos al rectangulo
svg.innerHTML += "<text class='text' id='" +
(count-1) + "txt" +"' x='"+ x +"' y='"+
y +"' font-family='Verdana' font-size='14px'>"
+ text +"</text>"
var close = document.createElementNS(
"http://www.w3.org/2000/svg", "text");
close.setAttribute("id","closeParent"+(count-1));
close.setAttribute("class","close");
close.setAttribute("x",x);
close.setAttribute("y",y+14);
close.setAttribute("font-family","Verdana");
close.setAttribute("font-size","15px");
close.innerHTML += "<tspan id='close" +
(count-1) + "' >x</tspan>";
svg.appendChild(close);
},
// Metodo que se lanza con el cambio de imagen
image_changed: function() {
var image = this.model.get('image');
myThis.loadImage(image);
var image_name = image.split(/[/]+/);
image_name = image_name[image_name.length - 1];
if(image_name != "blank.png"){
var notify_msg = "La imagen " + image_name + " ha sido cargada correctamente!";
notify(notify_msg, notify_style, success_type);
}
},
// Metodo que se lanza cuando se intenta cargar un
// fichero distinto a una imagen
error_loading_image: function() {
var file_name = this.model.get('error_loading');
var file_name = file_name.split(/[/]+/);
file_name = file_name[file_name.length - 1];
var notify_msg = "El fichero " + file_name + " no " +
"tiene formato de imagen. Ejemplo: jpeg, jpg, tif, tiff, png ";
notify(notify_msg, notify_style, error_type);
},
// Metodo que se lanza cuando se guardan
// las etiquetas de la imagen
success_saving_labels: function() {
var file_name = this.model.get('image');
var file_name = file_name.split(/[/]+/);
file_name = file_name[file_name.length - 1];
if(file_name != "blank.png"){
var success_saving_labels_msg = " Las etiquetas para la imagen " + file_name + " han " +
"sido guardadas correctamente ";
notify(success_saving_labels_msg, notify_style, success_type);
}
},
// Metodo par dibujar rectangulos
// con coordenadas provistas por Python
add_rect: function() {
var rect_coords = this.model.get('rect_to_add');
var text_for_coords = this.model.get('text_to_add');
for (var i = 0; i < rect_coords.length; i++) {
var new_rect_coords = rect_coords[i];
var x = new_rect_coords[1];
var y = new_rect_coords[0];
var height = new_rect_coords[2];
var width = new_rect_coords[3];
var new_rect = document.createElementNS(
"http://www.w3.org/2000/svg", 'rect');
new_rect.id = 'rect' + count;
new_rect.setAttribute('class', 'rect');
count = count + 1;
new_rect.setAttributeNS(null, 'x', x);
new_rect.setAttributeNS(null, 'y', y);
new_rect.setAttributeNS(null, 'height', height);
new_rect.setAttributeNS(null, 'width', width);
myThis.setStyleRect(new_rect);
svg.appendChild(new_rect);
var text = text_for_coords[i];
// Añadimos la etiqueta de texto que deseemos al rectangulo
svg.innerHTML += "<text class='text' id='" + (count-1) +
"txt" + "' x='"+ x +"' y='"+ y
+"' font-family='Verdana' font-size='14px'>"
+ text +"</text>"
var close = document.createElementNS(
"http://www.w3.org/2000/svg", "text");
close.setAttribute("id","closeParent"+(count-1));
close.setAttribute("class","close");
close.setAttribute("x",x);
close.setAttribute("y",y+14);
close.setAttribute("font-family","Verdana");
close.setAttribute("font-size","15px");
close.innerHTML += "<tspan id='close" +
(count-1) + "' >x</tspan>";
svg.appendChild(close);
}
myThis.touch();
}
});
return {
ImageLabelerView: ImageLabelerView
};
});
In [8]:
# Inicializamos Widget etiquetador de imágenes
image_labeler = ImageLabelerWidget()
In [9]:
# Clases para la gestión de directorios
import Directory_Manager
# Creamos directorios
dir_list = ['Rondel','Bulliform',
'Bilobate','Trichomas',
'Saddle', 'Spherical',
'Cyperaceae']
dir_manager = Directory_Manager.Directory_Manager(dir_list, current_dir= dir_list[0])
In [10]:
# Añadimos un diccionario que nos permita
# añadir las coordenadas segun el tipo de fitolito
# para guardar así la imagenes en el
# directorio que corresponda
coords_dict = dict()
''' Función que indexa las coordenadas al tipo de
fitolito que estamos eligiendo, de manera que añadimos
al diccionario cada una de las coordenadas que dibujamos
correspondiendo a la carpeta en la que sera guardada'''
def update_coords_list(coords_list):
# Coordenadas a añadir
coords_to_add = coords_list[len(coords_list)-1]
# Leemos el directorio seleccionado actualmente
current_dir_path = dir_manager.get_current_dir_path()
current_dir = dir_manager.get_current_dir()
# Si existia ya, añadimos coords a la lista
if current_dir_path in coords_dict:
coords_dict[current_dir_path].append(coords_to_add)
# Sino creamos lista y añadimos coords
else:
coords_dict[current_dir_path] = []
coords_dict[current_dir_path].append(coords_to_add)
# Patrón observe sobre la lista de coordenadas
image_labeler.rectList_bind_to(update_coords_list)
In [11]:
''' Función que se encarga de eliminar del diccionario
anterior las coordenadas del rectangulo que ha sido
eliminado por el usuario'''
def remove_coords_list(coords_list):
# Coordenadas a eliminar
# TODO Tener en cuenta transformación a realizar
coords_to_delete = coords_list[len(coords_list)-1]
# Comprobamos en que clave del
# diccionario se encuentran las
# coordenadas y las eliminamos
for k in coords_dict.keys():
if coords_to_delete in coords_dict[k]:
coords_dict[k].remove(coords_to_delete)
# Patrón observe sobre la lista de coordenadas
image_labeler.removed_rect_bind_to(remove_coords_list)
In [12]:
from IPython.display import display
import fileupload
import PIL.Image
import io as io2
from skimage.color import rgb2gray
''' Función que se encarga de aplicar las operaciones
necesarias para convertir los datos obtenidos del FileUpload
en una imagen'''
def image_converter(image):
image = io2.BytesIO(image)
image = PIL.Image.open(image)
return np.array(image)
In [13]:
from ipywidgets import HBox, VBox, Label, Layout
import re
import os.path
from time import sleep
#Inicializamos Widget de File Upload
upload_widget = fileupload.FileUploadWidget()
upload_widget.description = '(50% width, 80px height) button'
# Creamos el patron para el nombre
# de una imagen
pattern = re.compile("^.*\.(jpeg|jpg|tif|tiff|png)$", re.IGNORECASE)
# TODO Cuando se cambia de imagen
# hay que asegurarse de que las
# distintas variables se inicializan
# a los valores por defecto
# Callback para el cambio de imagen
def _cb(change):
# Limpiamos variables
coords_dict.clear()
change = change['owner']
# Control de que el fichero es una imagen
# y no otro tipo de fichero
if pattern.match(change.filename):
image = image_converter(change.data)
image_path = dir_manager.get_default_dir() + change.filename
# Guardamos imagen
io.imsave(image_path, image)
# Y la cargamos
image = io.imread(image_path)
# Sincronizamos cambio
image_labeler.image = image_path
# Comprobar si la imagen había
# sido previamente cargada.
# Si es así, cargamos etiquetas previas
name = change.filename.split(".")[0]
json_path = dir_manager.get_default_dir() +\
name + '.json'
if os.path.exists(json_path):
with open(json_path) as jsonfile:
old_coords_dict = json.load(jsonfile)[name + ".jpg"]
image_h, image_w, _ = image.shape
# Añadir al diccionario actual todas las coordenadas
# existentes para no eliminarlas al volver
# a guardar imagen
coords = []
text_to_add_with_coords = []
for k, _ in old_coords_dict.items():
#Añadimos los rectangulos
for rect_x, rect_x2, rect_y, rect_y2 in old_coords_dict[k]:
coord = [[rect_x, rect_y, rect_x2 - rect_x, rect_y2 - rect_y]]
coord = transform_image_coords_to_view_coords(coord, 500, 700, image_h, image_w)[0]
# Añadimos coordenadas a la variable en ejecucuión que las maneja
if dir_manager.get_possible_dir(k) in coords_dict:
coords_dict[dir_manager.get_possible_dir(k)].append(coord)
else:
coords_dict[dir_manager.get_possible_dir(k)] = [coord]
image_labeler.text = k
text_to_add_with_coords.append(k)
coords.append(coord)
#image_labeler.rect_to_add = coord
#sleep(2)
image_labeler.text_to_add = text_to_add_with_coords
image_labeler.rect_to_add = coords
# Reasignamos valor por defecto a texto
image_labeler.text = 'Rondel'
else:
# Sino, lanzamos error
image_labeler.error_loading = change.filename
upload_widget.observe(_cb, names='data')
In [14]:
# Selector de fitolito
btns_selector =widgets.ToggleButtons(
options= dir_list,
#description='Tipo de fitolito',
disabled=False,
# 'success', 'info', 'warning', 'danger' or ''
button_style='',
tooltip='Description',
#icon='check'
)
'''Función que se encarga de hacer los cambios
necesarios al cambiar el fitolito que se etiqueta'''
def on_phytolith_change(change):
new_dir = change['new']
dir_manager.change_dir(new_dir)
image_labeler.text = new_dir
# listener del cambio de fitolito
btns_selector.observe(on_phytolith_change,names='value')
#Asignamos al texto del etiquetador por defecto al primer elemento de los botones
image_labeler.text = dir_list[0]
In [15]:
import warnings
# Añadimos el botón que se encarga de guardar
#las imágenes en el directorio correspondiente
save_btn = widgets.Button(
description='Guardar imágenes',
disabled=False,
button_style='',
tooltip='Guardar imágenes',
icon='check'
)
#Ignoramos warnings de calidad de imagen
warnings.filterwarnings('ignore')
'''Función que se encarga de llamar a la función
de guardar las etiquetas como imágenes'''
def on_save_btn_click(ch):
if image_labeler.image != "blank.png":
# Obtenemos el nombre de la imagen
image_path = image_labeler.image
image_name = os.path.split(image_path)[1]
image_name = image_name.split(".")[0]
# Cargamos imágen
image = io.imread(image_path)
# Eliminamos fichero json e imagen previa si
# había sido previamente etiquetada
json_path = dir_manager.get_default_dir() +\
image_name + ".json"
if os.path.exists(image_path) and image_path != "blank.png":
os.remove(image_path)
if os.path.exists(json_path):
os.remove(json_path)
image_name =additional_text() + image_name
image_path = dir_manager.get_default_dir() + image_name + ".jpg"
# Guardamos imagen con
# un poco mas de texto añadido
io.imsave(image_path, image)
image_h, image_w, _ = image.shape
coords_dict_copy = dict()
for path, coords_set in coords_dict.items():
# Transformamos las coordenadas
coords_set = transform_view_coords_to_image_coords(coords_set, 500, 700, image_h, image_w)
# Guardamos la imagenes en sus correspondientes directorios
save_coords_as_images(coords_set, image, path, image_name)
# Realizamos una copía para la posterior realización
# del fichero json con las claves siendo el tipo de fitolito
# y no el path
key = os.path.split(os.path.split(path)[0])[1]
coords_dict_copy[key] = coords_set
save_coords_as_json(coords_dict_copy, dir_manager.get_default_dir(), image_name)
# Notificamos al usuario que las etiquetas
#han sido guardadas correctamente
if image_labeler.saved_successfuly == 'false':
image_labeler.saved_successfuly = 'true'
else:
image_labeler.saved_successfuly = 'false'
# Eliminamos los rectangulos que añadimos para
# evitar redundacia de coordenadas
image_labeler.set_rectList([])
save_btn.on_click(on_save_btn_click)
In [16]:
# Formato de los widgets
right_size_widget = VBox([upload_widget, btns_selector, save_btn])
right_size_widget.width = '10%'
right_size_widget.margin = '5% 5% 5% 5%'
btns_selector.padding = '10%'
btns_selector.add_class("btns_selector")
upload_widget.padding = '10%'
upload_widget.add_class('btn')
image_labeler.margin = '5% 10% 0% 0%'
upload_widget.margin = '0 0 7% 12%'
upload_widget.width = '50'
save_btn.margin = '10%'
In [17]:
w = HBox([image_labeler,right_size_widget])
w.margin = '5%'
w.add_class('widget')
w