Encuentre un gran número de valores consecutivos que cumplan la condición en una matriz numpy

Tengo algunos datos de audio cargados en una matriz numpy y deseo segmentar los datos encontrando partes silenciosas, es decir, partes donde la amplitud de audio está por debajo de un cierto umbral durante un período de tiempo.

Una forma extremadamente simple 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 encuentra partes donde hay al menos MIN_SILENCE elementos consecutivos más pequeños que SILENCE_THRESHOLD.

Ahora, obviamente, el código anterior es horriblemente ineficiente y un terrible abuso de las expresiones regulares. ¿Hay algún otro método que sea más eficiente, pero que resulte en un código igualmente simple y corto?

Aquí hay una solución basada en números.

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

Sin embargo, requiere el doble de memoria que las diversas soluciones basadas en generadores. Siempre que pueda mantener una sola 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() 

Ligeramente descuidado, pero simple y rápido, si no te 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 hasta una amplitud baja, y las regiones ruidosas no. Sintonice ‘sigma’ para afectar el tiempo que debe tener una región ‘tranquila’; sintonice el ‘umbral’ para afectar lo silencioso que debe ser. Esto se ralentiza para sigma grande, en cuyo momento el uso de 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.

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, encontrar las regiones contiguas es tan simple como:

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

Luego, la aplicación de cualquier función a esas regiones se puede hacer, por ejemplo, como:

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

No he probado esto, pero debería estar cerca de lo que está buscando. Un poco más de líneas de código, pero debería ser más eficiente, legible 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 

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 una prueba sencilla:

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

otra forma de hacerlo de forma 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 inicial y i2 el índice final de 1, …, 1 áreas

@ joe-kington Tengo una mejora de velocidad del 20% al 25% sobre la solución np.diff / np.nonzero utilizando argmax lugar (consulte el código a continuación, la condition es booleana)

 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 kilometraje puede variar dependiendo de sus datos.

Además, no estoy completamente 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.