2010-12-20 10 views
16

Tengo algunos datos de audio cargados en una matriz numpy y deseo segmentar los datos encontrando partes silenciosas, es decir, partes donde la amplitud del audio está por debajo de un cierto umbral sobre aa período en el tiempo.Encontrar una gran cantidad de valores consecutivos cumpliendo la condición en una matriz numpy

Una forma muy sencilla de hacer esto es algo como esto:

values = ''.join(("1" if (abs(x) < SILENCE_THRESHOLD) else "0" for x in samples)) 
pattern = re.compile('1{%d,}'%int(MIN_SILENCE))                   
for match in pattern.finditer(values): 
    # code goes here 

El código anterior se encuentra partes donde hay al menos MIN_SILENCE elementos consecutivos más pequeños que SILENCE_THRESHOLD.

Ahora, obviamente, el código anterior es terriblemente ineficiente y un terrible abuso de expresiones regulares. ¿Hay algún otro método que sea más eficiente, pero aún así resulta en códigos igualmente simples y cortos?

Respuesta

26

Aquí hay una solución basada en numpy.

Creo que (?) Debería ser más rápido que las otras opciones. Espero que esté bastante claro.

Sin embargo, requiere el doble de memoria que las diversas soluciones basadas en generadores. Siempre que pueda mantener una única copia temporal de sus datos en la memoria (para la diferencia), y una matriz booleana de la misma longitud que sus datos (1 bit por elemento), debería ser bastante eficiente ...

import numpy as np 

def main(): 
    # Generate some random data 
    x = np.cumsum(np.random.random(1000) - 0.5) 
    condition = np.abs(x) < 1 

    # Print the start and stop indicies of each region where the absolute 
    # values of x are below 1, and the min and max of each of these regions 
    for start, stop in contiguous_regions(condition): 
     segment = x[start:stop] 
     print start, stop 
     print segment.min(), segment.max() 

def contiguous_regions(condition): 
    """Finds contiguous True regions of the boolean array "condition". Returns 
    a 2D array where the first column is the start index of the region and the 
    second column is the end index.""" 

    # Find the indicies of changes in "condition" 
    d = np.diff(condition) 
    idx, = d.nonzero() 

    # We need to start things after the change in "condition". Therefore, 
    # we'll shift the index by 1 to the right. 
    idx += 1 

    if condition[0]: 
     # If the start of condition is True prepend a 0 
     idx = np.r_[0, idx] 

    if condition[-1]: 
     # If the end of condition is True, append the length of the array 
     idx = np.r_[idx, condition.size] # Edit 

    # Reshape the result into two columns 
    idx.shape = (-1,2) 
    return idx 

main() 
+0

¡Esto resulta en una impresionante aceleración de 20x! No tiene en cuenta la longitud mínima, pero eso es bastante fácil de agregar. El único problema es el aumento del uso de la memoria que hace que no sea factible su uso en alguna situación, así que creo que usaré esto de forma predeterminada y agregaré una opción para usar otro algoritmo cuando la memoria esté baja. – pafcu

+1

Con numpy 1.9, obtengo un 'DeprecationWarning: numpy boolean resta (el operador binario) está en desuso' usando np.diff en la condición booleana. Reemplacé esta línea con 'd = np.subtract (condición [1:], condición [: - 1], dtype = np.float)' para evitar el problema. – daryl

+2

@daryl - ¡Gracias por notar el cambio! Puede ser más claro hacer 'd = np.diff (condition.astype (int))' en su lugar, aunque eso es principalmente una cuestión de preferencia personal. –

3

No he probado esto pero debería estar cerca de lo que está buscando. Ligeramente más líneas de código, pero debe ser más eficiente y fácil de leer, y no abusar de las expresiones regulares :-)

def find_silent(samples): 
    num_silent = 0 
    start = 0 
    for index in range(0, len(samples)): 
     if abs(samples[index]) < SILENCE_THRESHOLD: 
      if num_silent == 0: 
       start = index 
      num_silent += 1 
     else: 
      if num_silent > MIN_SILENCE: 
       yield samples[start:index] 
      num_silent = 0 
    if num_silent > MIN_SILENCE: 
     yield samples[start:] 

for match in find_silent(samples): 
    # code goes here 
+1

Su código se ve bien, excepto que si el silencio se encuentra al final de las muestras, no se encontrará. Debe verificar después del bucle for. –

+0

@Justin: gracias, agregué eso en una edición. –

2

Esto debería devolver una lista de pares (start,length):

def silent_segs(samples,threshold,min_dur): 
    start = -1 
    silent_segments = [] 
    for idx,x in enumerate(samples): 
    if start < 0 and abs(x) < threshold: 
     start = idx 
    elif start >= 0 and abs(x) >= threshold: 
     dur = idx-start 
     if dur >= min_dur: 
     silent_segments.append((start,dur)) 
     start = -1 
    return silent_segments 

y un simple prueba:

>>> s = [-1,0,0,0,-1,10,-10,1,2,1,0,0,0,-1,-10] 
>>> silent_segs(s,2,2) 
[(0, 5), (9, 5)] 
+0

Esto parece ser aproximadamente un 25% más rápido que la solución basada en expresiones regulares. Bonito. Ahora solo demora 9 minutos :-) – pafcu

5

poco descuidado, pero simple y rápido-ish, si no les importa usar scipy:

from scipy.ndimage import gaussian_filter 
sigma = 3 
threshold = 1 
above_threshold = gaussian_filter(data, sigma=sigma) > threshold 

La idea es que las partes silenciosas de los datos se suavizarán a baja amplitud, y las regiones ruidosas no lo harán. Sintonice 'sigma' para afectar el tiempo que debe ser una región 'silenciosa'; sintonizar 'umbral' para afectar cuán silencioso debe ser. Esto se ralentiza para sigma grande, en cuyo punto el suavizado basado en FFT podría ser más rápido.

Esto tiene la ventaja adicional de que los "píxeles calientes" individuales no interrumpirán su búsqueda de silencio, por lo que es un poco menos sensible a ciertos tipos de ruido.

2

otra manera de hacer esto de manera rápida y concisa:

import pylab as pl 

v=[0,0,1,1,0,0,1,1,1,1,1,0,1,0,1,1,0,0,0,0,0,1,0,0] 
vd = pl.diff(v) 
#vd[i]==1 for 0->1 crossing; vd[i]==-1 for 1->0 crossing 
#need to add +1 to indexes as pl.diff shifts to left by 1 

i1=pl.array([i for i in xrange(len(vd)) if vd[i]==1])+1 
i2=pl.array([i for i in xrange(len(vd)) if vd[i]==-1])+1 

#corner cases for the first and the last element 
if v[0]==1: 
    i1=pl.hstack((0,i1)) 
if v[-1]==1: 
    i2=pl.hstack((i2,len(v))) 

ahora i1 contiene el índice I2 principio y el índice final de 1, ..., 1 áreas

3

Hay una solución muy conveniente para esto usando scipy.ndimage.Para una matriz:

a = array([1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0]) 

que puede ser el resultado de una condición aplicada a otra matriz, la búsqueda de las regiones contiguas es tan simple como:

regions = scipy.ndimage.find_objects(scipy.ndimage.label(a)[0]) 

Entonces, la aplicación de cualquier función a aquellas regiones puede ser hecho por ejemplo como:

[np.sum(a[r]) for r in regions] 
1

@ Joe-kington Tengo alrededor del 20% -25% de mejora de velocidad sobre np.diff/np.nonzero solución mediante el uso de argmax lugar (ver código de abajo, condition es booleano)

def contiguous_regions(condition): 
    idx = [] 
    i = 0 
    while i < len(condition): 
     x1 = i + condition[i:].argmax() 
     try: 
      x2 = x1 + condition[x1:].argmin() 
     except: 
      x2 = x1 + 1 
     if x1 == x2: 
      if condition[x1] == True: 
       x2 = len(condition) 
      else: 
       break 
     idx.append([x1,x2]) 
     i = x2 
    return idx 

Por supuesto , su millaje puede variar según sus datos.

Además, no estoy del todo seguro, pero supongo que numpy puede optimizar argmin/argmax sobre matrices booleanas para detener la búsqueda en la primera aparición de True/False. Eso podría explicarlo.

Cuestiones relacionadas