lunes, 13 de mayo de 2013

Entrada 17: Presentación 2o hito - L29/04/2013

El día 29 de abril tuvo lugar la presentación del segundo hito, en el cual mostramos los avances que hicimos desde el primer hito en el proyecto. Ésta es la presentación con la que expusimos:



Para realizar la demo dentro de la presentación utilizamos un programa, a modo de "ejecutable". De esta manera teníamos un único fichero en el escritorio al que dábamos doble click, seleccionábamos la opción de ejecutar en un terminal y automáticamente nos lanzaba el servidor que utilizamos para dar soporte a la interfaz gráfica y nos abría el navegador con la vista principal de la aplicación ya dispuesta para funcionar.

A este archivo lo hemos llamado LID_RPi_launcher, y hay que darle permisos de ejecución. Sólo tiene las siguientes líneas:
#!/bin/sh

# Lanzo el servidor
cd "ruta_al_proyecto"
node lidonrpi.js &

# Lanzo la aplicación
#(en firefox, con chromium-browser lo lanza en chrome, etc.)
firefox localhost:3000/lidonrpi/index 2> /dev/null
Y con esto terminamos el segundo hito con lo que ya sólo nos queda el tercero y definitivo, cuya presentación será el próximo 23 de Mayo.

domingo, 12 de mayo de 2013

Entrada 16: Interfaz gráfica de usuario IV - Abril 2013

Y ya sólo nos queda hablar de la pantalla final de la aplicación. Al igual que en los videojuegos ha resultado ser la más difícil. Partimos del botón "Realizar el reconocimiento", y cuando el usuario le da, debe renderizarse el resultado del reconocimiento que habíamos lanzado previamente. Y para ello tiene que haber terminado el reconocimiento.

El problema está en que los shell scripts que realizan la mayor parte del programa los lanzamos con el módulo child process, que lo que hace es crear hilos independientes por lo que es difícil saber cuando van a terminar. Además, nodejs es un lenguaje enfocado a eventos, por lo que no existen monitores (wait()-notify()). Hay, pues, un serio problema de concurrencia.

Tras muchas pruebas y quebraderos de cabeza hemos resuelto el problema utilizando la propia vairable del reultado como flag y un timeout que se repite hasta que  dicha variable tiene un resultado (proceso finalizado) en ese momento renderizamos la vista con el resultado en la siguiente vista, donde <%r1%> es una variable que será sustituída por el resultado:
<div id="resultado">El sistema ha reconocido el idioma como <%r1%>.</div>
<form method="GET" action="/lidonrpi/index">
   <input type="submit" value="Volver">
</form>
Todo el control de los flujos de procesos del programa lo llevamos a cabo en controller.results() de la siguiente forma (de acuerdo a lo descrito):
results: function () {
   var espera = 0;
   var id = setInterval(function(){
      if(result) {
 view.render('results.html', result);
 console.log("He esperado "+espera+"ms");
 model.clean();
 result = "";
 clearInterval(id);
      } else {
 console.log("Llevo esperando "+espera+"ms");
 espera += 500;
      }
   }, 500);
}
donde model.clean(); es una nueva acción del modelo que lanza un script que se encarga de almacenar los archivos que han sido mandados para reconocer en una estructura de directorios en función del idioma reconocido:
clean: function () {
   child = exec('./scripts/limpia.sh '+tmpAudio+' '+result,
   function (error, stdout, stderr) {
      if (error !== null) {
         console.log('exec error: ' + error);
      }
   });
}
y el contenido de limpia.sh:
#!/bin/bash

# Ruta al programa que realiza el reconocimiento
LID_RPi_DIR=/home/mtm/Desktop/PRACTICA_SDG2/SDG2_Octave_sin_normalizar/LID_RPi/audio

#Renombra tmpAudio según el idioma reconocido y lo mueve al directorio correspondiente
mv ${LID_RPi_DIR}/${1}.wav ${LID_RPi_DIR}/${2}/${2}${1}.wav
Una vez hemos seguido los pasos descritos a lo largo de estas cuatro entradas ya sólo nos falta dar permisos de ejecución a todos los shell scripts que utilizamos ($ chmod +x "ficheros.sh") y arrancar el servidor ($ node lidonrpi.js) para tener nuestra aplicación funcionando y con una interfaz gráfica. Para acceder a ella basta con abrir un navegador e ir a la dirección 'localhost:3000/lidonrpi/index'.

En las próximas entradas continuaremos relatando más detalles del proyecto.

Entrada 15: Adaptación de scripts de Matlab a Octave II - Abril 2013

Continuando con la parte de adaptación del código a Octave:

Tamaño Long

El siguiente problema que apareció fue que a la hora de leer un fichero de forma binaria, éste aparecía con tamaño negativo. Tras analizar el script que lo cargaba e ir ejecutando a la vez las instrucciones en Matlab y Octave, comprobamos que para cada programa, a la hora de ejecutar la función fread, la longitud de las variables de tipo long tienen diferente tamaño, ya que octave cargaba 64 bits y matlab 32. La solución fue sustituir en el script long por int32:

-Código original:
nf=fread(fid,1,'long');             % number of frames
fp=fread(fid,1,'long')*1.E-7;       % frame interval
by=fread(fid,1,'short');            % bytes per frame
tc=fread(fid,1,'short');            % type code
-Código para Octave:
nf=fread(fid,1,'int32');            % number of frames
fp=fread(fid,1,'int32')*1.E-7;      % frame interval
by=fread(fid,1,'short');            % bytes per frame
tc=fread(fid,1,'short');            % type code

Java y Octave 

Varios scripts usan librerías de Java. Matlab está programado en Java, por tanto las librerías están bien integradas. Pero en Octave no pasa lo mismo. Para integrar las librerías de Java en Octave hay que instalar (con sudo apt-get install) octave-java. Con esto tenemos las librerías.

Tras dos tardes intentando que funcionasen las funciones, decidimos cambiar de estrategia. Las dos funciones para las que necesitábamos java son: guardar variables en formato gzip y hacer un hash de los path de los ficheros con el algoritmo SHA256.

El primer problema se solucionó de forma muy parecida al de guardar información en ficheros hdf5. Además, simplificó bastante el código, ya que no hace falta manejar streams. Los cambios fueron:

A la hora de crear los ficheros:

-Versión de Matlab:
a         = java.io.FileOutputStream(file_name);
b         = java.util.zip.GZIPOutputStream(a);
writer    = java.io.OutputStreamWriter(b);


for rr = 1:rows
 for cc = 1:cols
   if ~ transposed
     writer.write(sprintf('%f ', data(rr,cc)));
   else
     writer.write(sprintf('%f ', data(cc,rr)));
   end
 end
 writer.write(sprintf('\n'));
end

writer.close();
b.close();
a.close();
-Versión de Octave:
if ~ transposed
  data = data';
end

data = reshape(data,1,size(data,1)*size(data,2));
str = sprintf("save %s -z data", file_name);eval(str);

Y a la hora de cargarlos:

-Versión de Matlab:
row   = 0;

a         = java.io.FileInputStream(file_name);
b         = java.util.zip.GZIPInputStream(a);
reader    = java.io.InputStreamReader(b);
b_reader  = java.io.BufferedReader(reader);

% read line by line
line = b_reader.readLine(); 

while ~ isempty(line)
  row = row + 1;

  line = char(line);
  if ~ transposed
    x(row, :) = sscanf(line, '%f')';
  else
    x(:, row) = sscanf(line, '%f');
  end

  line = b_reader.readLine();
end

reader.close();
b.close();
a.close();
-Versión de Octave:
str=sprintf("load %s", file_name);
eval(str);

if transposed
  data=data';
end

x=data;
El segundo problema relacionado con Java lo intentamos resolver de diversas maneras. Primero con Java, después compilando la función SHA256 para Octave, la cual no nos funcionó correctamente y por último decidimos usar el algoritmo MD5 (mediante la función md5sum, la cual está totalmente integrada en Octave)

Y hasta aquí las entradas de problemas de compatibilidad entre Matlab y Octave.

Entrada 14: Interfaz gráfica de usuario III - Abril 2013

Como podemos ver en la entrada anterior, model.record(); ejecuta un shell script. Dicho shell script lo que hace es llamar a un script que limpia los stats de reconocimientos realizados anteriormente (para tener una ocupación de memoria sostenible), y configura la entrada de audio y lanza la grabación en el archivo .wav que se la pasa como parámetro:
#!/bin/bash

LID_RPi_DIR=ruta_absoluta_al_proyecto
#Borrado de stats antiguos:
sh ${LID_RPi_DIR}/Borrar_temp.sh
# Nombre y ruta del fichero de audio con la grabación.
WAV_NAME=${1}
WAV_NAME_RUTA=${LID_RPi_DIR}/audio/${WAV_NAME}
# Configuración del micrófono para que grabe
amixer -c 1 sset Mic,0 100%,100% unmute cap >/dev/null
# Lanza la grabación
arecord -D hw:1,0 -f S16_LE -c 1 -r 8000 -B 900000 ${WAV_NAME_RUTA}.wav >/dev/null
El script que se encarga de borrar stats antiguos tiene las siguientes instrucciones:
#!/bin/bash

cd "ruta_absoluta_al_proyecto"/ivectoresMFCC
rm data/tmp.prm/* fea/* lbl/*
cd plenty_closed
rm ivec/ascii_out_512G_400i/* log/* results_512G_i400/tmp/* results_512G_i400/* /stats_512_400/*
En la siguiente vista, veremos un cronómetro que nos indica el tiempo que llevamos grabando, y un botón que pone "Parar". Al detener la grabación lo que hacemos es que el propio navegador detiene el cronómetro, cambia el texto del botón a "Realizar el reconocimiento", y manda una petición al servidor para detener la grabación y comenzar a realizar el reconocimiento. La vista recording.html es la siguiente:
<script type="text/javascript" src="/js/timer.js"></script>
<div id="traza">
   Grabando...
</div>
<div id="timer">
   00:00
</div>
<div id="time"></div>
<script type="text/javascript">
   setInterval(function(){timer()}, 1000);//Cronómetro
</script>
<form method="GET" action="/init">
   <input id="stop" type="submit" value="Parar">
</form>
<form method="GET" action="/lidonrpi/results">
   <input id="recognize" type="submit" value="Realizar el reconocimiento" style="display: none">
</form>
<script type="text/javascript">
   $("#stop").on('click', function() {
      $("#stop").attr("style", "display: none");
      $("#recognize").attr("style", "display: yes");
      $("#timer").attr("style", "display: none");
      $("#time").html(decenasMinutos +''+ unidadesMinutos + ':' + decenasSegundos +''+ unidadesSegundos);
      $("#traza").html("Grabación realizada.");
   });
</script>
donde utilizamos jQuery para los cambios en la vista (en app.html importamos la librería) y el script en "/js/timer.js" es el que se encarga de realizar las acciones del cronómetro de la siguiente forma:
var unidadesSegundos = 1;
var decenasSegundos = 0;
var unidadesMinutos = 0;
var decenasMinutos = 0;
function timer() {
   var display = document.getElementById("timer");
   display.innerHTML = decenasMinutos +''+ unidadesMinutos + ':' + decenasSegundos +''+ unidadesSegundos;
   switch(unidadesSegundos) {
      case 9: {
 unidadesSegundos = 0;
 switch(decenasSegundos) {
    case 5: {
       decenasSegundos = 0;
       switch(unidadesMinutos) {
  case 9: {
     unidadesMinutos = 0;
     switch(decenasMinutos) {
        case 5: {
   decenasMinutos = 0;
   break;
        }
        default: { decenasMinutos++; break; }
            }
            break;
         }
         default: { unidadesMinutos++; break; }
       }
       break;
           }
           default: { decenasSegundos++; break; }
 }
        break;
      }
      default: { unidadesSegundos++; break; }
   }
}
Los cambios a realizar en el servidor serán:
  • En controller.init(); dentro del controlador:
model.stop();
model.recognize();
  • En el modelo, creo model.stop() y model.recognize():
stop: function () {
   child = exec('killall arecord',
   function (error, stdout, stderr) {
      if (error !== null) {
         console.log('"killall arecord" exec error: ' + error);
      }
   });
},
recognize: function () {
   child = exec('./scripts/reconoce.sh '+tmpAudio,
   function (error, stdout, stderr) {
      console.log('reconoce.sh -> stdout: ' + stdout);
      console.log('reconoce.sh -> stderr: ' + stderr);
      if (error !== null) {
         console.log('reconoce.sh -> exec error: ' + error);
      }
      result = stdout;//Resultado del reconocimiento
   });
}
El script reconoce.sh que se lanza con model.recognize(); tiene el siguiente contenido:
#!/bin/bash

# Ruta al programa que realiza el reconocimiento
LID_RPi_DIR="ruta_absoluta_al_proyecto"
# Ruta al fichero con los resultados
SCORES=${LID_RPi_DIR}/ivectoresMFCC/plenty_closed/results_512G_i400/plenty_closed_acus_400i_512G_WithLN_hmmHung_isEval__Albayzin_TEST.scores
# Actualizar la lista con el nombre del archivo 
basename $1 .wav > "${LID_RPi_DIR}/prueba.lst"
# Realizar el reconocimiento
${LID_RPi_DIR}/runAll2.sh >/dev/null
# Decidir el idioma reconocido: ESCOGE LA MÁXIMA PUNTUACIÓN.
scores=(`cat ${SCORES}`)
result_n=3
for i in 4 5 6 7 8
   do
      st=$(echo "${scores[result_n]} < ${scores[$i]}" | bc)
      if [ $st -eq 1 ]
 then
 result_n=$i
      fi
done
case $result_n in
   3) result=vasco;;
   4) result=catalán;;
   5) result=inglés;;
   6) result=gallego;;
   7) result=portugués;;
   8) result=español;;
esac
echo $result

sábado, 11 de mayo de 2013

Entrada 13: Interfaz gráfica de usuario II - Abril 2013

La forma de implementar el servidor la vamos a llevar a cabo siguiendo el patrón MVC (modelo vista controlador). El controlador se encarga de realizar las acciones correspondientes según las peticiones que realice el usuario, que se reflejan en el servidor por medio de rutas. De esas acciones, las que tienen que ver con las vistas las realizar el módulo de vista y las que están relacionadas con los flujos de datos y ejecución de procesos las realiza el modelo.

De acuerdo a como definimos el comportamiento de la aplicación al final de la entrada anterior, vamos a tener tres vistas: index.html, recording.html y results.html. La primera será la que renderizará el botón "Graba", la segunda el cronómetro con el botón "Parar" y "Realizar el reconocimiento" y la tercera el resultado en una segunda variable a la que hemos llamado <%r1%> y un botón para volver al inicio de la aplicación.

Cada uno de los botones tiene una ruta asociada y, por tanto, una llamada al controlador para que decida que acciones realizar. Con todo esto, y a falta de definir las acciones, el servidor tendría la siguiente forma:
//Módulos a importar:
var http = require('http');
var URL = require('url');
var fs = require('fs');
var mime = require('mime');
var qs = require('querystring');
//Otros módulos y variables que se usan luego:
var exec = require('child_process').exec;
var child;
var tmpAudio = "";
var result = "";
//Traza para la consola:
var first=true;
if (first) {
   console.log('server inicializado');
   first=false;
}
//Servidor:
http.createServer(function(request, response){ 
   //Modelo: 
   var model = {
      //ACCIONES DEL MODELO
   } 
   //Vista:
   var view = {
      render: function (file,r1) {
  fs.readFile('app.html', 'utf-8', function(err, app) {
     if (!err) {
  fs.readFile(file, 'utf-8', function(err, view) {
        if (!err) {
        var data = app.replace(/<%view%>/, view);
        data = data.replace(/<%r1%>/, r1);
        response.writeHead(200, {
   'Content-Type': 'text/html'
        });
        response.end(data);
     } else {
        view.error(500, "Error al renderizar la vista");
     };
  });
     } else {
  view.error(500, "Error en la interfaz gráfica");
     }
  });
      },
      error: function(code, msg) {
  response.writeHead(code); 
  response.end(msg);
      },
      file: function(file) {
  fs.readFile(file, function(err, data) {
     if (!err) {
        response.writeHead(200, {
  'Content-Type': mime.lookup(file)
        });
        response.end(data);
     } else {
        view.error (500, file + " not found");
     }
  });
      }
   } 
   //Controlador:
   var controller = {
      index: function () {
 view.render('index.html',"");
      },
      recording: function () {
 //BOTÓN "GRABAR"
      },
      init: function () {
 //BOTÓN "PARAR"
      },
      results: function () {
 //BOTÓN "REALIZAR EL RECONOCIMIENTO"
      },
      file: function () { view.file(url.pathname.slice(1)); }
   }
   //Routing: 
   var url = URL.parse(request.url, true);
   var post_data = "";
   request.on('data', function(chunk){ post_data += chunk; });
   request.on('end', function() {
      post_data = qs.parse(post_data);
      var route = (post_data._method || request.method) + ' ' + url.pathname;
         switch (route) {
            case 'GET /lidonrpi/index'     : controller.index(); break;
     case 'GET /lidonrpi/recording' : controller.recording(); break;
     case 'GET /lidonrpi/results'   : controller.results(); break;
     case 'GET /init'               : controller.init(); break;
     default: {
        if (request.method == 'GET') {
           controller.file();
        } else {
    view.error(400, "Petición no contemplada");
        }
     }
         }
   });
}).listen(3000);
La vista index es un botón que lanza el proceso de grabación. Para realizar estas acciones generamos la vista (index.html) y la acción asociada al botón (controller.recording()), que es lanzar el proceso de grabación (model.record()) y renderizar la siguiente vista (view.render('recording.html',"")); de modo que los cambios a realizar son:
  • Index.html:
<form method="GET" action="/lidonrpi/recording">
   <input type="submit" value="Grabar">
</form>
  • En el controlador, en controller.recording():
model.record();
view.render('recording.html',"");
  • En el modelo, creo model.record():
record: function () {
   tmpAudio = new Date().getTime();
   console.log("grabando en " + tmpAudio + ".wav" + '\n');
   child = exec('./scripts/graba.sh '+tmpAudio,
      function (error, stdout, stderr) {
         console.log('graba.sh -> stdout: ' + stdout);       
         console.log('graba.sh -> stderr: ' + stderr);
         if (error !== null) {
            console.log('graba.sh -> exec error: ' + error);
         }
   });
},

domingo, 5 de mayo de 2013

Entrada 12: Interfaz gráfica de usuario I - Abril 2013

El primer problema que se nos planteó a la hora de decidir como hacer la interfaz gráfica fué el lenguaje de programación en el que desarrollarla. Entre las alternativas que se nos ocurrieron estaban Qt, GTK, Swing, etc. pero finalmente optamos por realizar vistas HTML y un servidor local en node.js para que gestionase las vistas y acciones asociadas a cada botón.

La principal ventaja que tiene esta interfaz es que se puede adaptar el servidor local a un servidor remoto de forma que se podría hacer que nuestro sistema se utilizase en una aplicación web.

Junto con la parte de adaptación del programa de Matlab a Octave, este es el otro trabajo que hemos realizado a lo largo del mes de abril. Las sesiones dedicadas a esta parte han sido X-10, M-16, X-17, L-22, X-24, D-28 y L-29, hasta que hemos conseguido una integración entre programa e interfaz que funcionase.

Lo primero  que hacemos es generar el marco común de la aplicación, sobre el que se irán renderizando las distintas vistas. Para ello creamos un documento app.html como se muestra a continuación, donde <%view%> es la variable en la que se va a renderizar cada una de las vistas correspondientes:
<!DOCTYPE html>
<html>
 <head>
  <link rel="icon" href="/images/favicon.ico" />
 <title>LID on RPI</title>
 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
 <script type="text/javascript" src="/js/jquery.min.js"></script>
 <link rel='stylesheet' type='text/css' href='/css/style.css' />
 </head>
 <body>
  <div id="page-wrap">   
   <header>
   <h1>LID_RPi:<br>Sistema de reconocimiento del idioma<br>basado en la Raspberry Pi</h1>
   <img src="/images/raspberry_die.png"><br>
   </header>
   <section id="main">
    <%view%>
   </section>   
   <footer>
   <br><br>
   <div>Proyecto de práctica innovadora para Sistemas Digitales II.</div>
   <div>Autores: Diego Avendaño Peces y Jose Mª Bermudo Mera.</div>
   </footer>
  </div>
 </body>
</html>
Además de este html, hemos creado un estilo básico y un logo para el proyecto, que se mostrará en el centro de la vista. Dicho estilo está programado en una hoja .css, incluyendo apenas un par de detalles como centrar el texto, utilizar un tamaño más grande para el título y cambiar el color de fondo:
body {
 background: #CCCCCC;
 text-align: center;
}
header {
 margin: auto;
 text-align: center;
}
img {
 display: block;
 margin: auto;
}
De esta forma, la vista básica de la interfaz gráfica quedaría con el siguiente aspecto:


Lo siguiente que hacemos es definir las acciones que se deben llevar a cabo en cada momento. Según se abra la aplicación aparecerá la vista descrita con un botón que pone "Grabar". Al pulsar ese botón, se lanzará un proceso que comience la grabación del audio mientras que en la vista se mostrará un cronómetro con el tiempo de grabación y un nuevo botón para detener la misma. Una vez se haya detenido la grabación comenzará el proceso de reconocimiento, mientras que el usuario podrá ver un botón para realizar el reconocimiento. Cuando tengamos el resultado lo mostraremos por pantalla junto con otro botón para volver al inicio de la aplicación.

Todas estas acciones son las que se llevarán a cabo a través de un servidor local. La forma en la que se llevan a cabo la detallamos más a fondo en sucesivas entradas.

Entrada 11: Adaptación de scripts de Matlab a Octave I - Abril 2013

Durante las últimas semanas (desde Semana Santa hasta finales de Abril) nos hemos repartido el trabajo y nos hemos enfocado en hacer una interfaz gráfica y adaptar los algoritmos a Octave.

Yo (Diego) me estoy encargando de arreglar las incompatibilidades entre Matlab y Octave. El objetivo es prescindir de Matlab, ya que al hacerlo en Octave podremos (en principio) ejecutar todo desde la Raspberry Pi, sin necesitar el servidor; y por otro lado no necesitaremos software privativo (bastante caro, además).

Para corregir los errores hemos ido ejecutando el programa principal, sacando la salida del programa (la cual estaba redirigida a /dev/null) por pantalla y viendo qué funciones daban problemas, buscando los fallos y por último, corrigiéndolos.


HDF5

El primer fallo apareció a la hora de tratar con ficheros hdf5. Este tipo de ficheros se utilizan para guardar grandes cantidades de información estructurada (por ejemplo en variables). Matlab tiene un conjunto de funciones dedicadas a cargar (hdf5read), escribir (hdf5write) u obtener información (hdf5info) de estos ficheros. Estas funciones no están implementadas en Octave, por lo que tuvimos que buscar una forma alternativa para manejar este formato.

Googleando, encontramos que Octave sí que podía trabajar con ficheros hdf5, es más, estaban más integrados que en Matlab, ya que simplemente usando la función load (fichero), se cargaban las variables. A la hora de guardar los datos, igual (añadiendo la opción -hdf5 y diciendo qué variables quieres que guarde): save -hdf5 fichero variables. Aquí apareció otro problema, ya que necesitábamos guardar el fichero con el nombre del archivo de audio del cual se había hecho la parametrización, pero en Octave, al guardar no deja poner como nombre de fichero una variable (ya que no la sustituye por su valor). La solución fue crear un string con el comando a ejecutar creado a partir de la función sprintf y ejecutarlo.

Por último, necesitábamos usar la función hdf5info para comprobar si el fichero tenía una variable en concreto. Como load carga por defecto todas las variables, sustituimos la sentencia que contiene hdf5info por una comprobación de que la variable no sea 0 (previamente hemos creado la variable y la hemos igualado a 0).

Así, los cambios son:

  • hdf5read y hdf5info:
-Versión Matlab

MMF.ALFA=double(hdf5read(file, '/ALFA')');
MMF.COV=double(hdf5read(file, '/COV'));
MMF.MEAN=double(hdf5read(file, '/MEAN'));

MMFinfo=hdf5info(file);
Datasets={MMFinfo.GroupHierarchy.Datasets.Name};

if any(ismember(Datasets,'/hlda'))
   MMF.hlda=double(hdf5read(file, '/hlda'));
end
-Versión de Octave
hlda=0;
load (file) MMF.ALFA=double(ALFA); 
MMF.COV=double(COV); 
MMF.MEAN=double(MEAN);   
if(hlda!=0)      
    MMF.hlda=double(hlda);   
end
  • hdf5write:
-Versión Matlab

hdf5write(statsfile, '/N', single(N),'/F', single(F(:)));

-Versión Octave
str = sprintf("save -hdf5 %s N F", statsfile);
eval(str);