File size: 8,921 Bytes
82b8273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import os
# import ffmpeg
import numpy as np
import pandas as pd
import json

#####
# Requires the above packages, if not installed: pip install <package name>
#####



def dir_convert_mp32wav(directory, keep_file=False):
    '''
    Processes a directory, applying file_convert_mp32wav to every mp3 file
    Parameters:
    - str directory: path to directory
    - bool keep_file: whether to delete original mp3 file or not
    '''
    res = np.array([file_convert_mp32wav(os.path.join(directory, f), keep_file=keep_file) for f in os.listdir(directory) 
                    if os.path.splitext(f)[-1] == '.mp3']).sum(axis=0)
    
    print(f'Directory {directory} processed, {res} conversion and deletions')

    
def file_convert_mp32wav(input_file, keep_file=False):
    '''
    Converts a sound file from mp3 to wav using ffmpeg
    Parameters:
    - str input_file: path to file to convert
    - bool keep_file: whether to delete original mp3 file or not
    Returns
    - tuple (int convert, int delete) indicating if conversion/deletion was performed or not
    '''
    
    output_file = '.'.join([os.path.splitext(input_file)[0], 'wav'])
    convert = 0
    delete = 0
    
    if not os.path.isfile(output_file):
        # if output file doesn't already exist
        stream = ffmpeg.input(input_file)
        stream = ffmpeg.output(stream, output_file)
        ffmpeg.run(stream)
        convert = 1
        
    if not keep_file:
        os.remove(input_file)
        delete = 1
        
    return convert, delete


def read_txt_file(file, extra_str_label=''):
    '''
    Opens a text file and copy content in a pandas df. Audacity outputs that span 2 lines are put back to a single line
    Parameters:
    - str file: path to txt file
    - str extra_str_label: the substring that is added in label files, depending of the birder
    Returns 
    dataframe containing txt file information
    '''
    
    df = pd.read_table(file, header=None)

    # Separate time and frequency lines (the latter start with "\")
    df['line_type'] = (df[0] == '\\').astype(int)

    # Assign recording id to each line
    df['id'] = [elt for elt in assign_idx(df['line_type'])]
    
    # Suppression of duplicate entries
    df.drop_duplicates(['line_type', 'id'], inplace=True)
    
    # From two to one row per recording
    df = df.loc[df['line_type'] == 0].merge(df.loc[df['line_type'] == 1], on='id').dropna().rename(columns={
    '0_x': 't_start', '1_x': 't_end', '2_x': 'species', '1_y': 'f_start', '2_y': 'f_end'
    })
    df = df[['t_start', 't_end', 'f_start', 'f_end', 'species']]

    df['filename'] = os.path.basename(file).split('.')[0]
    df['filename'] = df['filename'].str.replace(extra_str_label, '')

    for dt_col in ['t_start', 't_end']:
        df[dt_col] = df[dt_col].astype(float)
    
    return df


def create_label_dataset(directory, extra_str_label='', suppress_others=True, suppress_noise=True, suppress_unID=False, is_csv=False):
    '''
    Concatenates text files contents into a pandas df containing recordings information
    Parameters:
    - str directory: path to directory
    - str extra_str_label: the substring that is added in label files, depending of the birder
    - bool suppress_others: whether or not to suppress all non-bird signal
    - bool suppress_noise: whether or not to suppress all background sound or noise
    - bool suppress_others: whether or not to suppress unidentified bird sounds
    Returns:
    dataframe containing recordings information
    '''

    # Bird species to ID dictionary
    dict_dir = r'C:\Users\laeri\NBM'
    with open(os.path.join(dict_dir, 'bird_dict.json'), 'r') as f:
        birds_dict = json.load(f)
    
    # Labels dataframe
    if is_csv:
        labels = pd.read_csv(os.path.join(directory, 'annotations.csv'))
        # Suppress file extension
        labels['filename'] = labels['filename'].str.slice_replace(-4, repl='')
    else:
        df_list = [read_txt_file(os.path.join(directory, f), extra_str_label=extra_str_label) 
            for f in os.listdir(directory) if os.path.splitext(f)[-1] == '.txt']
        labels = pd.concat(df_list)

    # Convert to float and clip
    for freq in ['f_start', 'f_end']:
        labels[freq] = labels[freq].astype(float)
    labels['f_start'] = labels['f_start'].clip(lower=0)
    labels.loc[labels['f_end'] < 0, 'f_end'] = 20000    
    
    # Deduplication of recording labels, label with the largest frequency range is kept
    labels['f_delta'] = labels['f_end'] - labels['f_start']
    labels = labels.sort_values('f_delta', ascending=False).drop_duplicates(['filename', 't_start']).sort_values(['filename', 't_start'])
    del labels['f_delta']
    
    # Clean species
    labels['species'] = labels['species'].map(lambda x: replacements[x] if x in replacements.keys() else x)

    # Assign species to id
    labels['bird_id'] = labels['species'].map(lambda x: birds_dict[x] if x in birds_dict.keys() else np.nan)

    # Other sounds - Attention Background et dérivés (p-ê vent aussi) sont à traiter séparément et ne doivent pas être détectés -> on peut
    # les garder comme samples négatifs (donc label = -1 dans le RPN) contrairement aux autres qui seront plutôt détectés mais classifiés comme
    # "autres sons" par le RCNN.
    noise_labels = ['Bruit de fond', 'Background', 'Backgroud', 'passage moto au loin', 'Back ground', 'Back groung', 'Backgroun', 'Bakground', 'backgroound',
    'background', 'bruit de fond']
    labels.loc[labels['species'].isin(noise_labels), 'bird_id'] = -1

    not_bird_labels = ['Capreolus capreolus', 'Pelophylax sp.', 'Vulpes vulpes', 'Oecanthus pellucens', 'ruspolia nitidula', 'orthoptère', 'voix humaine', 
    'saturation HF par orthoptères', 'Cervus elaphus brame', 'Sus scrofa', 'chien', 'Hannetons par milliers', 'possible battement d\'aile', 'What ??',
    'parasite', 'bruit parasite', 'geophonie', 'Vent geophonie', 'vulpes vulpes', 'Capreolus capreolus ', '0: Bruit parasite', '0: Other biophonia', 
    '0: Other antropophonia']
    mask_others = labels['species'].map(lambda x: 'autre' in x.lower())
    labels.loc[mask_others | labels['species'].isin(not_bird_labels), 'bird_id'] = 0

    # All rarer and unidentified birds (supprimer oiseaux sp. du ds d'apprentissage pour la classif ?)
    max_idx = len(birds_dict)
    labels['bird_id'].fillna(max_idx + 1, inplace=True)
    labels['bird_id'] = labels['bird_id'].astype(int)

    if suppress_noise:
        labels = labels.loc[labels['bird_id'] != -1]
    
    if suppress_others:
        labels = labels.loc[labels['bird_id'] != 0]

    if suppress_unID:
        labels = labels.loc[~labels['species'].isin(['Oiseau sp', 'Parus sp'])]

    labels.index = range(len(labels))
    
    return labels


def assign_idx(col):
    '''
    Function to be used in read_txt_file, increment a recording index each time a new recording is detected, 
    corresponding to a float value in column 0
    '''
    
    idx = -1
    
    for elt in col:
        if elt == 0:
            idx += 1
        yield idx


replacements = {
    'Emberiza ortulana': 'Emberiza hortulana',
    'bernicla bernicla': 'Branta bernicla',
    'Bernicla bernicla': 'Branta bernicla',
    'Grus grus adulte': 'Grus grus',
    'Corvus corone alarme': 'Corvus corone',
    'Phasianus colchicus ': 'Phasianus colchicus',
    'Luscinia megarynchos megarynchos': 'Luscinia megarynchos',
    'Luscinia megarynchos megarynchos': 'Luscinia megarhynchos',
    'Luscinia megarhynchos megarhynchos ': 'Luscinia megarhynchos',
    'Grus grus juvénile': 'Grus grus',
    'Strix aluco chant': 'Strix aluco',
    'tachybaptus ruficollis': 'Tachybaptus ruficollis',
    'Tachybaptus ruficollis ': 'Tachybaptus ruficollis',
    'Burhinus burhinus': 'Burhinus oedicnemus',
    'Erithacus rubecula ': 'Erithacus rubecula',
    'Turdus merula alarme': 'Turdus merula',
    'Luscinia megarhynchos': 'Luscinia megarhynchos',
    'Burhinus oedicnemus ' : 'Burhinus oedicnemus',
    'Gallinula chloropus ': 'Gallinula chloropus',
    'chant Luscinia megarhynchos': 'Luscinia megarhynchos',
    'Anas platychyncos': 'Anas platyrhynchos',
    'Grus grus cris': 'Grus grus',
    'Erithacus rubecola': 'Erithacus rubecula',
    'Anas platyrhynchos ': 'Anas platyrhynchos',
    'Certhia brachydactyla ': 'Certhia brachydactyla',
    'Streptopelia decaocto ': 'Streptopelia decaocto',
    'Strix aluco ': 'Strix aluco',
    'Botaurus stellaris ': 'Botaurus stellaris',
    'Numenius arquata XC570503': 'Numenius arquata',
    'Chevalier sylvain': 'Tringa glareola',
    'caprimulgus europaeus': 'Caprimulgus europaeus',
    'ardea cinerea': 'Ardea cinerea',
    'Cuculus canorus canorus': 'Cuculus canorus',
    'Charadrius dubius curonicus': 'Charadrius dubius',
    'Charadrius curonicus': 'Charadrius dubius',
    'Erithacus rubecula rubecula': 'Erithacus rubecula',
    'Tyto alba alba': 'Tyto alba'
}