diff --git "a/IMPETOM-Cl\303\255nico/ImpetomC.py" "b/IMPETOM-Cl\303\255nico/ImpetomC.py" new file mode 100644 index 0000000000000000000000000000000000000000..4d7873cd56f4cfc64465fd998e98c1339a13bea8 --- /dev/null +++ "b/IMPETOM-Cl\303\255nico/ImpetomC.py" @@ -0,0 +1,394 @@ +import threading +import time +from datetime import date, datetime +import numpy as np +import re +from pyeit.eit import jac +from pyeit.eit.interp2d import sim2pts + +from Archivos import Archivos +from Arduino import Arduino +from ImagenesDicom import ImagenesDicom +import os +from ImpetomCError import ImpetomCError +from ImpetomCElectrodeError import ImpetomCElectrodeError +import matplotlib.pyplot as plt + +#modulos de pyeit +import pyeit.mesh as mesh +from pyeit.eit.fem import Forward +from pyeit.eit.utils import eit_scan_lines +from pyeit.mesh.shape import thorax,circle + +flag_InstanceStudy = True + +class ImpetomC: + """ + Permite manejar todas las funcionalidades de la aplicación ImpetomC + + - Generación y guardado de la imagen tomográfica en formato dicom + - Conexión del arduino ImpetomC al puerto serie + + """ + + #Parametros para las imagenes reconstruidas + cmap=plt.cm.RdYlBu # color para las imagenes generadas + shape = "thorax" + lamb=0.01 + + #vectores de datos + datos = np.zeros(208) # Variable para el manejo de los datos de entrada desde Arduino ImpetomC + archiVH = Archivos("homo_py.txt")# vector homogéneo por defecto. Puede cambiarse utilizando la función actualizarVectorDeDatosHomogeneo + vHomogeneo = archiVH.leerArchivo() + + #Variables para el manejo del dispositivo ImpetomC + ard = None # se actualiza con startUp() o __arduinoConnect() + arduinoComunicationError = None # Variable para manejar el error de que se desconecte el Arduino de la pc mientras adquiere datos + + # Variables globales utilizadas para el manejo de problemas asosciados a los electrodos + voltajesElevados = 0 + voltajesElevadosSeguidos = 0 + tiraMedidaElevada = 0 + + #Parámetros del sistema + rootDir=os.path.dirname(os.path.abspath("ImpetomC.py")).replace("ImpetomC.py","") #Ubicación del directorio donde se guardaran las imágenes generadas + today = date.today().strftime("%b-%d-%Y") + + #Establecimiento de los parámetros de la imagen dicom + imgDicom = ImagenesDicom(p=0.3,lamb=lamb,file_path_jpg= os.path.join(rootDir, "IMG.jpg"),today=today,rootDir=rootDir, shape=shape) + + + @classmethod + def startUp(cls): + """ + Esta función permite hacer el start up de la aplicación. Utilizarla para dar inicio a la aplicación + + :exception ImpetomCError: Devuelve una excepción en caso de encontrar problemas con la conexión a el dispositivo Arduino + """ + + cls.__arduinoConnect() + + @classmethod + def __arduinoConnect(cls): + """ + Permite establecer la conexión con el Arduino con el software de ImpetomC + + :exception ImpetomCError: Devuelve una excepción con el mensaje correspondiente + """ + + cls.ard = Arduino(baudrate = 9600, timeout = 0.1,vThreshold = 4.0) #para cambiar el valor del threshold de voltaje cambiar vThreshold + cls.arduinoComunicationError = None + + + @classmethod + def reset(cls): + """ + Este método permite realizar el reseteo de la aplicacion, devolviendo la aplicacion a sus valores por defecto + """ + + cls.datos = np.zeros(208) + cls.ard.cerrarPuertoSerie() + archiVH = Archivos("homo_py.txt") # vector homogéneo por defecto. Puede cambiarse utilizando la función actualizarVectorDeDatosHomogeneo + cls.vHomogeneo = archiVH.leerArchivo() + cls.startUp() + + @classmethod + def actualizarVectorDeDatosHomogeneo(cls): + """ + Permite volver a cargar el vector de datos homogeneo en modo reconstrucción para un mejor detalle del vector + """ + + cls.ard.cambiarModoDeOperacion("Reconstruccion") + datosRecalibrados, ve, fa, vS, ta = cls.ard.obtenerMedidas() + archiCalibrar = Archivos("datos_calibrar.txt") + archiCalibrar.escribirArchivo(datosRecalibrados) + cls.vHomogeneo = archiCalibrar.leerArchivo() + + + @classmethod + def __verificarParametrosParaImagen(cls,mode,shape,vmax,vmin,screenWidth,p,sclEnable): + """ + Permite verificar que los parámetros de entrada para la reconstrucción tomográfica sean correctos + + :param mode: Tipo string, debe contener la palabra "reconstruccion" o "setup" + :param shape: Tipo String, debe contener la palabra "thorax" o "circle" + :param vmax: Tipo float, máximo valor para la escala de la imagen + :param vmin: Tipo float, mÃnimo valor para la escala de la imagen + :param screenWidth: Tipo Int, ancho de la pantalla del usuario + :param p: Parámetro para la reconstrucción con JAC, se recomienda que el valor esté en 0.1 y 0.9 + :param SclEnable: Debe ser un booleano. True para utilizar los valores vmax y vmin y false para no utilizarlos + + :returns: **meshDensity** - Retorna la densidad de mesh a utilizar, esto depende de el modo de trabajo de la aplicación y el shape utilizado + + **retShape** - Retorna un shape reconocible por las funcionalidades de pyEIT + + :exception ImpetomCError: Devuelve una excepción con el mensaje de error correspondiente + """ + + mode=mode.lower() + if mode == "reconstruccion": # Determino el mesh density + meshDensity= 0.045 + elif mode == "setup": + if shape == "thorax": # Determino mesh de setup según modelo + meshDensity = 0.07 + + elif shape == "circle": + meshDensity = 0.1 + else: + raise ImpetomCError("Forma para la reconstrucción inválida, debe ser \"Thorax\" o \"Circle\"") + else: + raise ImpetomCError("Modo de reconstrucción inválido. Debe ser \"Reconstruccion\" o \"Setup\"") + + shape=shape.lower() + if shape == "thorax": # Determino la forma de la reconstrucción + retShape=thorax + + elif shape == "circle": + retShape = circle + + else: + raise ImpetomCError("Forma para la reconstrucción inválida, debe ser \"Thorax\" o \"Circle\"") + + if type(vmax) != float or type(vmin) != float or type(screenWidth) != int or type(p) != float or type(sclEnable) != bool: + raise ImpetomCError("vmax, vmin, p deben ser floats , screenWidth debe ser un int y sclEnable un bool") + elif screenWidth <= 0 or p <=0 : + raise ImpetomCError("screenWidth y p deben ser mayores que 0") + return [meshDensity,retShape] + + @classmethod + def __obtenerimagen(cls,vectorDatos,SclEnable,vmax,vmin,shape,mode,p,screenWidth): + """ + Pemite generar una imágen tomográfica a partir de un vector de datos, entre otros parámetros + + :param vectorDatos: Vector de 208 valores de tensión para reconstruir la imágen tomográfica + :param SclEnable: Debe ser un booleano. True para utilizar los valores vmax y vmin y false para no utilizarlos + :param vmax: Máximo valor para la escala de la imagen + :param vmin: MÃnimo valor para la escala de la imagen + :param shape: Forma de la reconstrucción "thorax" o "circle" + :param mode: Modo en el que fueron tomadas las medidas de vectorDatos ("reconstruccion" o "setup") + :param p: Parámetro para la reconstrucción con JAC, se recomienda que el valor esté entre 0.1 y 0.9 + :param screenWidth: Ancho de la pantalla del usuario + + :exception ImpetomCError: Devuelve una excepción con el mensaje de error correspondiente + """ + + meshDensity, correctShape = cls.__verificarParametrosParaImagen(sclEnable=SclEnable,vmax=vmax,vmin=vmin,shape=shape,mode=mode,p=p,screenWidth=screenWidth) + mesh_obj, el_pos = mesh.create(16, h0=meshDensity, fd=correctShape) + pts = mesh_obj["node"] + tri = mesh_obj["element"] + x, y = pts[:, 0], pts[:, 1] + + if shape == "circle": + el_pos = el_pos + 12 # Electrodo 1 en las "6" del reloj + for i in range(4, 16): + el_pos[i] = el_pos[i] - 16 + + el_dist, step = 1, 1 + ex_mat = eit_scan_lines(16, el_dist) + eit = jac.JAC(mesh_obj, el_pos, ex_mat=ex_mat, step=step, perm=1.0, parser="fmmu") + eit.setup(p=p, lamb=cls.lamb, method="kotre") + + ds_n = eit.solve(vectorDatos, cls.vHomogeneo, normalize=True) + ds = sim2pts(pts, tri, np.real(ds_n)) + + fig, ax = plt.subplots(1, 1, constrained_layout=True) + fig.set_size_inches(7, 5) + + if (SclEnable): + im = ax.tripcolor(x, y, tri, ds, shading="flat", cmap= cls.cmap, vmax=vmax, vmin=vmin) + else: + im = ax.tripcolor(x, y, tri, ds, shading="flat", cmap=cls.cmap) + + if shape == "thorax": # Electrodos Thorax + for i, e in enumerate(el_pos): + if i <= 4: + ax.annotate(str(i + 1), xy=(x[e] - 0.05, y[e] - 0.05), color="w") + elif i <= 7: + ax.annotate(str(i + 1), xy=(x[e] - 0.04, y[e]), color="w") + else: + if i <= 11: + ax.annotate(str(i + 1), xy=(x[e], y[e]), color="w") + else: + ax.annotate(str(i + 1), xy=(x[e], y[e] - 0.05), color="w") + elif shape == "circle": # Electrodos Circle + for i, e in enumerate(el_pos): + if 1 <= i <= 7: + if i <= 3: + ax.annotate(str(i + 1), xy=(x[e] - 0.05, y[e] - 0.05), color="w") + else: + ax.annotate(str(i + 1), xy=(x[e] - 0.05, y[e]), color="w") + elif i >= 14: + ax.annotate(str(i + 1), xy=(x[e] + 0.04, y[e] - 0.05), color="w") + elif i == 0: + ax.annotate(str(i + 1), xy=(x[e], y[e] - 0.05), color="w") + else: + ax.annotate(str(i + 1), xy=(x[e], y[e]), color="w") + + ax.axis("off") + cbar = fig.colorbar(im) + cbar_yticks = plt.getp(cbar.ax.axes, 'yticklabels') + plt.setp(cbar_yticks, color='w') + ax.set_aspect("equal") + fig.patch.set_facecolor('black') + fig.savefig('IMG.jpg', dpi=(100/1200)*screenWidth) + plt.close(fig) + + @classmethod + def generarImagenTomografica(cls,mode,shape,screenWidth,sclEnable,vmax,vmin,p,frecuenciaDDS): + """ + Permite generar la imagen tomográfica y la guarda en la raÃz del proyecto como IMG.jpg + + :param mode: Modo en el que debe operar el dispositivo ImpetomC + :param shape: Forma de la reconstrucción "thorax" o "circle" + :param screenWidth: Ancho de la pantalla del usuario + :param SclEnable: Debe ser un booleano. True para utilizar los valores vmax y vmin y false para no utilizarlos + :param vmax: Máximo valor para la escala de la imagen + :param vmin: MÃnimo valor para la escala de la imagen + :param p: Parámetro para la reconstrucción con JAC, se recomienda que el valor este en 0.1 y 0.9 + :param frecuenciaDDS: Valor de frecuencia a la que debera operar el DDS del dispositvo ImpetomC + + :exception ImpetomCElectrodeError: Devuelve una excepción con una mensaje que indica el problema detectado con los electrodos + :exception ImpetomCError: Devuelve una excepción con un mensaje de error por errores distintos al anterior + """ + + global voltajesElevados, voltajesElevadosSeguidos, tiraMedidaElevada + if(type(frecuenciaDDS) != str): + raise ImpetomCError("La frecuencia del dds debe ser un String") + if cls.datos[0] == 0: #Verifico si es la primera vez que se ejecuta la aplicación o si el dispositivo se desconectó + cls.__tomarMedidasParaImagen(mode, frecuenciaDDS) + threading.Thread(target=cls.__tomarMedidasParaImagen(mode,frecuenciaDDS)).start() + cls.__obtenerimagen(vectorDatos=cls.datos,screenWidth=screenWidth,SclEnable=sclEnable,vmax=vmax,vmin=vmin,p=p,shape=shape,mode=mode) + + if voltajesElevados >= 26 and voltajesElevadosSeguidos >= 13: + if tiraMedidaElevada[0] != 0: + imprimir_ele = [] + for dato in tiraMedidaElevada: + if dato != 0: + imprimir_ele.append(int(dato)) + raise ImpetomCElectrodeError("Se detectó un falso contacto en al menos un electrodo. Revise los electrodos " + str(imprimir_ele)) + elif voltajesElevados > 64: + raise ImpetomCElectrodeError("Se detectaron varios electrodos con tensiones elevadas, se recomienda regular la ganancia del dispositivo.",regularGanancia=True) + elif voltajesElevados <= 13: + raise ImpetomCElectrodeError("Se detectaron varios electrodos con tensiones bajas, se recomienda regular la ganancia del dispositivo.", regularGanancia=True) + elif cls.arduinoComunicationError == None: # verifico que no se haya desconectado el dispositivo durante la adquisición + threading.Thread(target=cls.__tomarMedidasParaImagen(mode,frecuenciaDDS)).start() + tiImagen=time.time() + cls.__obtenerimagen(vectorDatos=cls.datos,screenWidth=screenWidth,SclEnable=sclEnable,vmax=vmax,vmin=vmin,p=p,shape=shape,mode=mode) + tfImagen=time.time() + if voltajesElevados >= 26 and voltajesElevadosSeguidos >= 13: + if tiraMedidaElevada[0] != 0: + imprimir_ele = [] + for dato in tiraMedidaElevada: + if dato != 0: + imprimir_ele.append(int(dato)) + raise ImpetomCElectrodeError("Se detectó un falso contacto en al menos un electrodo. Revise los electrodos " + str(imprimir_ele)) + elif voltajesElevados > 64: + raise ImpetomCElectrodeError(msg="Se detectaron varios electrodos con tensiones elevadas, se recomienda regular la ganancia del dispositivo.",regularGanancia=True) + elif voltajesElevados <= 13: + raise ImpetomCElectrodeError(msg="Se detectaron varios electrodos con tensiones bajas, se recomienda regular la ganancia del dispositivo.",regularGanancia=True) + elif cls.arduinoComunicationError != None:# Si se desconectó el dispositivo durante la adquisición + cls.datos= np.zeros(208) #Reinicio el vector de datos + cls.ard.datos=np.zeros(208) + raise ImpetomCError(cls.arduinoComunicationError) + + @classmethod + def __tomarMedidasParaImagen(cls,mode,frecuenciaDDS): + """ + Utiliza al objeto arduino generado para comunicarse con este y obtener las medidas de tension necesarias para realizar la reconstruccion tomográfica del paciente. + Las medidas obtenidos son guardadas en la variable datos + + :param mode: Modo de operación para el dispositvo ImpetomC + :param frecuenciaDDS: Frecuencia de operación para el dispositvo DDS + + """ + + global arduinoComunicationError,voltajesElevados, voltajesElevadosSeguidos,tiraMedidaElevada + cls.ard.cambiarModoDeOperacion(mode) + cls.ard.cambiarFrecuenciaDDS(frecuenciaDDS) + try: + tidatos = time.time() + cls.datos,voltajesElevados,frecuenciaArd,voltajesElevadosSeguidos,tiraMedidaElevada= cls.ard.obtenerMedidas() + archi = Archivos("img.txt") + archi.escribirArchivo(cls.datos) + tfdatos = time.time() + cls.arduinoComunicationError = None + except ImpetomCError as e:# Escribo el msj de error en arduinoComunicationError + cls.arduinoComunicationError = str(e) + + @classmethod + def __verificarParametrosParaHistoriaClinica(cls,pacientName,pacientCi,p,birthDate,sex,medida,medidaZ,shape,mode): + """ + Permite verificar que los parámetros de entrada para la reconstrucción tomográfica sean correctos + + :parameter pacientName: Tipo string, no debe estar vacÃo + :param pacientCi: Tipo String, no debe estar vacÃo y ser una cadena de números + :param p: Tipo float, debe ser mayor que 0 + :param birthDate: Debe ser un String válido a convertir a fecha en el formato dd-mm-yyyy + :param sex: Tipo String, debe ser "M", "F" o "Otro" + :param medida: Medida de la circunferencia del thorax del paciente + :param medidaZ: Altura aproximada donde está colocado el cinturón de electrodos. El cero se marca desde la apófisis del esternón + :param shape: Forma de la reconstrucción "thorax" o "circle" + + :exception ImpetomCError: Devuelve una excepción con el mensaje de error correspondiente + """ + + if(type(pacientName) != str or len(pacientName) <=1 ): + raise ImpetomCError("El nombre debe ser un string y no debe estar vacÃo") + if(type(pacientCi) != str or not re.match('^[0-9]*$',pacientCi)): + raise ImpetomCError("La CI solo puede contener números") + if (type(p)!=float or p <= 0): + raise ImpetomCError("p debe ser un float y mayor que 0") + if (type(medida) != str or not re.match('^[0-9]*$', medida)): + if type(medida) == int and int(medida) < 0 :# explcitamente me fijo si el numero es positivo + raise ImpetomCError("La medida de la circuferencia del tórax solo puede contener números positivos") + elif type(medida )!= int: + raise ImpetomCError("La medida de la circuferencia del tórax solo puede contener números") + if (type(medidaZ) != str or not re.match('^[0-9]*$', medidaZ.strip("-"))): + if type(medidaZ) != int: + raise ImpetomCError("La medida de la altura de colocación del cinturón debe ser un número entero") + + mode=mode.lower() + if type(mode) != str: + raise ImpetomCError("El atributo Mode debe ser un string") + elif mode != "reconstruccion" and mode != "setup": + raise ImpetomCError("Modo de reconstrucción inválido. Debe ser \"Reconstruccion\" o \"Setup\"") + shape=shape.lower() + if shape != "thorax" and shape != "circle": + raise ImpetomCError("Forma para la reconstrucción inválida, debe ser \"Thorax\" o \"Circle\"") + try: + patBirthDate = datetime.strptime(birthDate, "%Y%m%d") + except ValueError: + raise ImpetomCError("La fecha de nacimiento no es válida") + sex=sex.lower() + if (sex != "m" and sex != "f" and sex!="otro"): + raise ImpetomCError ("El sexo del paciente debe ser M, F u Otro") + + @classmethod + def guardarImagenEnHistoriaClinica(cls,pacientName,pacientCi,p,pacBirthDate,pacSex, medida, shape, medidaZ,mode): + """ + Permite el guardado de las imágenes obtenidas en la carpeta correspondiente a la historia clónica del paciente, solamente cuando el dispositivo se encuentra en modo Reconstrucción + + :param pacientName: Nombre del paciente + :param pacientCi: Ci del paciente + :param p: valor de p utilizado durante la reconstrucción + :param birthDate: Fecha de nacimiento del paciente + :param pacSex: Sexo biologico del paciente + :param medida: Medida de la circunferencia del thorax del paciente + :param shape: Forma de la reconstrucción "thorax" o "circle" + :param medidaZ: Altura aproximada donde está colocado el cinturón de electrodos. El cero se marca desde la apófisis del esternón + :param mode: Modo de trabajo del dispositivo. Puede ser "Reconstruccion" o "Setup". Solo guarda imágenes con formato Dicom en caso de estar en modo "Reconstruccion" + """ + + cls.__verificarParametrosParaHistoriaClinica(pacientName,pacientCi,p,pacBirthDate,pacSex,medida,medidaZ,shape,mode) + medida=int(medida) + medidaZ=int(medidaZ) + dir_dcm = "\\Pacients_Data\\" + pacientName.strip('\n') + "_" + pacientCi.strip('\n') + "\\" + cls.today + + try: # intento crear la capeta donde se guardaran las imágenes tomográficas del paciente + os.makedirs(cls.rootDir + dir_dcm) + except OSError: # en caso de que el directorio exista no importa + pass + finally: # guardo la imagen en la carpeta del paciente con un nombre que depende de la hora, minutos y segundos + mode = mode.lower() + if mode == "reconstruccion": + cls.imgDicom.generarImagenDicom(pacientName, pacientCi, pacSex, pacBirthDate, p, cls.lamb, cls.today, medida, shape, medidaZ)