domingo, 12 de mayo de 2013

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);
 

lunes, 8 de abril de 2013

Entrada 10: Presentación 1er hito - J21/03/2013

El jueves 21 de marzo tuvieron lugar las presentaciones del primer hito de las prácticas innovadoras. Ésta es la presentación con la que expusimos el proyecto:



Y en cuánto grabemos un vídeo de demostración del sistema, lo subiremos a youtube y os lo pondremos también por aquí.

lunes, 1 de abril de 2013

Entrada 9: Mejorando el procesado - M19/03/2013

En la última reunión con nuestro tutor, nos comentó algunos cambios en los algoritmos de reconocimiento de idioma que mejoran los resultados obtenidos.

El primero consiste en aumentar la frecuencia de muestreo a 16 kHz, ya que originalmente estaba a 8 kHz. De esta manera, se tienen más datos para procesar y la calidad del fichero de audio es mejor. Los cambios que tuvimos que realizar son:

-Cambiar las opciones del comando que graba el audio (arecord), sustituyendo 8000 por 16000 en la opción -r:
$ arecord -D hw:1,0 -f S16_LE -c 1 -r 16000 -B 900000 "ruta_fichero/nombre_fichero.wav"
-Modificar el fichero que llama al programa sfbcep (incluida en el toolkit SPro) para que sepa que el fichero que le pasamos es de 16 kHz y no de 8 kHz (por defecto). El fichero que hay que cambiar es "feature_extractHTK_Albayzin12.sh" y ésta es la línea que hay que cambiar:
OPTS_SFBCEP=" -e -i 200 -u 3800 -Z  -R";
y aquí está la línea editada:
OPTS_SFBCEP=" -e -i 200 -f 16000 -Z -R";
Añadimos la opción -f 16000, indicando que la frecuencia de muestreo es 16 kHz. La opción -u la eliminamos porque por defecto usa la mitad de la frecuencia de muestreo, siguiendo el teorema de Niquist (8000 Hz).

El segundo cambio propuesto consiste en normalizar la señal antes del procesado con SPro, y no una vez empezado éste. Para ello:

-Eliminamos la opción -R del comando anterior, que normaliza la señal, quedando definitivamente:
OPTS_SFBCEP=" -e -i 200 -f 16000 -Z";
-Realizamos la normalización cuando se llama al script fea_process_beforeVAD.m:
function dout=fea_process_beforeVAD(d)
  d=rasta(d); %rasta filtering
  % Normalización
  u = mean(d, 2);
  s = sqrt(var(d,[],2));
  y = zeros(size(d),class(d));
  for i=1:size(y, 1)
     y(i,:) = (d(i,:) - u(i))/s(i);
  end
  %
  d=sdc(y); %shifted delta cepstra - default 7-1-3-7  
  dout=d;
Una vez realizados los cambios, comprobamos que todo funcionase correctamente. Pero encontramos un imprevisto: al ejecutarse arecord, éste nos avisaba de que la frecuencia máxima de grabación era de 11025 Hz. Esto se debe a que la tarjeta de sonido que viene con los micrófonos de la PlayStation 2 no es capaz de grabar a una frecuencia superior a esta. Por tanto decidimos deshacer los cambios respectivos a los 16 kHz. La solución a ésto será conseguir una tarjeta de sonido USB que pueda grabar hasta 16 kHz como mínimo, pero por ahora no va a ser nuestro objetivo principal.