{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"},{"sourceId":11922182,"sourceType":"datasetVersion","datasetId":7495459}],"dockerImageVersionId":30918,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### What is it?\n\nThis code will create **5 second** 224x224 PNG melspectrograms for BirdCLEF+ 2025. *Runtime about 5 hours.\n- Please find output of this code at the [BirdCLEF+ 2025, MelSPECs (5 sec) Dataset](https://www.kaggle.com/datasets/samvelkoch/birdclef-2025-melspecs-5-sec/)\n- Addon: **10 seconds** Melspectrograms [Code](https://www.kaggle.com/code/samvelkoch/birdclef2025-dataprep-specs-10-sec/) and [Dataset](https://www.kaggle.com/datasets/samvelkoch/birdclef-2025-melspecs-10-sec)\n\nHave fun... Thank you 🙏","metadata":{"execution":{"iopub.status.busy":"2025-03-11T14:24:24.494212Z","iopub.execute_input":"2025-03-11T14:24:24.49444Z","iopub.status.idle":"2025-03-11T14:24:36.406246Z","shell.execute_reply.started":"2025-03-11T14:24:24.494416Z","shell.execute_reply":"2025-03-11T14:24:36.404824Z"}}},{"cell_type":"code","source":"# Audio Processing for BirdCLEF 2025\n# Creating mel-spectrograms from 5-second segments with parallel processing (without saving audio)\nimport os\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport scipy.ndimage\nimport librosa.display\nimport matplotlib.pyplot as plt\nfrom matplotlib import cm\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom PIL import Image\nimport glob\nimport concurrent.futures\nimport multiprocessing\nimport tqdm\ntqdm.tqdm = tqdm.tqdm_notebook = tqdm.tqdm\nimport scipy.ndimage\nimport random\nimport gc\n    \nall_bird_data = {}\nerrors = []\n\n# Path configuration\nBASE_PATH = '/kaggle/input/birdclef-2025'\nTRAIN_AUDIO_PATH = os.path.join(BASE_PATH, 'train_audio')\nTRAIN_SOUNDSCAPES_PATH = os.path.join(BASE_PATH, 'train_soundscapes')\n\n# random seed\nrandom.seed(42)\n\n# fabio.csv\nFABIO_CSV_PATH = '/kaggle/input/fabio-csv1/fabio.csv'\nfabio_df = pd.read_csv(FABIO_CSV_PATH)\nfabio_segments = {\n    row['filename']: (float(row['start']), float(row['stop']))\n    for _, row in fabio_df.iterrows()\n}\n\n# Number of workers for parallel processing\n# Using 80% of available CPUs is usually a good balance\nNUM_WORKERS = max(1, int(multiprocessing.cpu_count() * 0.8))\nprint(f\"Using {NUM_WORKERS} workers for parallel processing\")\n\n# Create destination directories\ndef create_output_dirs():\n    # Only for spectrograms\n    os.makedirs('specs_5sec', exist_ok=True)\n    \n    print(\"Output directory created\")\n\ndef extend_audio_to_target_length(audio, sr, target_duration=5.0):\n    \"\"\"\n    오디오를 목표 길이로 확장 (반복 패딩 사용)\n    \"\"\"\n    target_length = int(sr * target_duration)\n    \n    if len(audio) >= target_length:\n        return audio\n    \n    # 반복 패딩으로 목표 길이 채우기\n    repeat_count = int(np.ceil(target_length / len(audio)))\n    extended_audio = np.tile(audio, repeat_count)[:target_length]\n    \n    return extended_audio\n\ndef get_random_20sec_segment(audio, sr, max_total_sec=20):\n    \"\"\"\n    오디오에서 랜덤한 위치에서 시작하는 20초 구간을 추출\n    \"\"\"\n    max_samples = int(max_total_sec * sr)\n    total_samples = len(audio)\n    if total_samples <= max_samples:\n        # 오디오가 20초 이하라면 전체 반환\n        return audio\n    else:\n        # 랜덤 시작점 선택\n        max_start = total_samples - max_samples\n        start_sample = random.randint(0, max_start)\n        end_sample = start_sample + max_samples\n        return audio[start_sample:end_sample]\n\ndef segment_audio_interval(audio, sr, start_sec, stop_sec, duration, max_total_sec=20):\n    \"\"\"\n    start_sec~stop_sec 구간만 5초씩 분할, 20초 이상이면 랜덤 20초만 사용\n    \"\"\"\n    start_sample = int(start_sec * sr)\n    stop_sample = int(stop_sec * sr)\n    audio_interval = audio[start_sample:stop_sample]\n    \n    # 5초 미만이면 반복 패딩으로 확장\n    audio_interval = extend_audio_to_target_length(audio_interval, sr, duration)\n    \n    audio_interval = get_random_20sec_segment(audio_interval, sr, max_total_sec)\n    samples_per_segment = int(sr * duration)\n    num_segments = len(audio_interval) // samples_per_segment\n    segments = []\n    for i in range(num_segments):\n        seg_start = i * samples_per_segment\n        seg_end = seg_start + samples_per_segment\n        segment = audio_interval[seg_start:seg_end]\n        segments.append(segment)\n    return segments\n\n# Function to slice audio into segments\ndef segment_audio(audio, sr, duration, max_total_sec=20):\n    \"\"\"\n    오디오 전체를 5초씩 분할, 20초 이상이면 랜덤 20초만 사용\n    \"\"\"\n    # 5초 미만이면 반복 패딩으로 확장\n    audio = extend_audio_to_target_length(audio, sr, duration)\n    \n    audio = get_random_20sec_segment(audio, sr, max_total_sec)\n    samples_per_segment = int(sr * duration)\n    num_segments = len(audio) // samples_per_segment\n    segments = []\n    for i in range(num_segments):\n        start = i * samples_per_segment\n        end = start + samples_per_segment\n        segment = audio[start:end]\n        segments.append(segment)\n    return segments\n\n# Function to create mel-spectrogram (uint16 버전)\ndef create_melspectrogram(audio, sr, size=(256, 256), db_min=-80, db_max=0):\n    melspec = librosa.feature.melspectrogram(\n        y=audio, \n        sr=sr, \n        n_mels=size[0],  \n        fmax=sr/2\n    )\n    melspec_db = librosa.power_to_db(melspec, ref=np.max)\n    if melspec_db.shape[1] != size[1]:\n        melspec_db = scipy.ndimage.zoom(melspec_db, (1, size[1] / melspec_db.shape[1]), order=1)\n    melspec_db = np.clip(melspec_db, db_min, db_max)\n    melspec_norm = (melspec_db - db_min) / (db_max - db_min)\n    \n    # uint16으로 변환 (0-65535 범위)\n    melspec_uint16 = (melspec_norm * 65535).astype(np.uint16)\n    return melspec_uint16\n\n# Function to process a single audio file\ndef process_audio_file(args):\n    bird_folder, audio_file, bird_folder_path = args\n    stats = {'specs_5sec': 0, 'errors': 0, 'extended': 0}\n    audio_path = os.path.join(bird_folder_path, audio_file)\n    \n    try:\n        audio, sr = librosa.load(audio_path, sr=None)\n        base_filename = os.path.splitext(audio_file)[0]\n        rel_path = os.path.join(bird_folder, audio_file)\n        \n        # 5초 미만 체크 및 확장\n        original_duration = len(audio) / sr\n        if original_duration < 5.0:\n            stats['extended'] += 1\n        \n        if rel_path in fabio_segments:\n            start_sec, stop_sec = fabio_segments[rel_path]\n            segments_5sec = segment_audio_interval(audio, sr, start_sec, stop_sec, 5, max_total_sec=20)\n        else:\n            segments_5sec = segment_audio(audio, sr, 5, max_total_sec=20)\n            \n        for i, segment in enumerate(segments_5sec):\n            segment_filename = f\"{bird_folder}-{base_filename}_{i+1:02d}\"\n            spec_img = create_melspectrogram(segment, sr)\n            all_bird_data[segment_filename] = spec_img\n            stats['specs_5sec'] += 1\n            \n    except Exception as e:\n        print(f\"Error processing {audio_file}: {e}\")\n        stats['errors'] += 1\n        errors.append((audio_file, str(e)))\n        \n    return stats\n\n# 새로 추가: soundscape 파일 처리 함수 (개선됨)\ndef process_soundscape_file(audio_file):\n    \"\"\"\n    soundscape 파일에서 맨 앞 5초만 추출하여 negative 샘플로 변환\n    5초 미만이면 반복 패딩으로 확장\n    \"\"\"\n    stats = {'specs_5sec': 0, 'errors': 0, 'extended': 0}\n    audio_path = os.path.join(TRAIN_SOUNDSCAPES_PATH, audio_file)\n    \n    try:\n        audio, sr = librosa.load(audio_path, sr=None)\n        base_filename = os.path.splitext(audio_file)[0]\n        \n        # 5초 길이 설정\n        target_length = int(sr * 5)\n        \n        if len(audio) >= target_length:\n            # 5초 이상이면 맨 앞 5초만 사용\n            segment = audio[:target_length]\n        else:\n            # 5초 미만이면 반복 패딩으로 5초 채우기\n            segment = extend_audio_to_target_length(audio, sr, 5.0)\n            stats['extended'] += 1\n        \n        segment_filename = f\"negative-{base_filename}\"\n        spec_img = create_melspectrogram(segment, sr)\n        all_bird_data[segment_filename] = spec_img\n        stats['specs_5sec'] += 1\n            \n    except Exception as e:\n        print(f\"Error processing soundscape {audio_file}: {e}\")\n        stats['errors'] += 1\n        errors.append((audio_file, str(e)))\n        \n    return stats\n\ndef process_soundscapes():\n    \"\"\"\n    모든 soundscape 파일을 처리하여 negative 샘플 생성\n    \"\"\"\n    if not os.path.exists(TRAIN_SOUNDSCAPES_PATH):\n        print(f\"Warning: {TRAIN_SOUNDSCAPES_PATH} does not exist, skipping soundscape processing\")\n        return {'specs_5sec': 0, 'errors': 0, 'extended': 0}\n    \n    soundscape_files = [f for f in os.listdir(TRAIN_SOUNDSCAPES_PATH) if f.endswith('.ogg')]\n    print(f\"Processing {len(soundscape_files)} soundscape files for negative samples...\")\n    \n    total_stats = {'specs_5sec': 0, 'errors': 0, 'extended': 0}\n    \n    for audio_file in soundscape_files:\n        stats = process_soundscape_file(audio_file)\n        for key in total_stats:\n            total_stats[key] += stats[key]\n    \n    return total_stats\n\n# Main processing function with parallel execution\ndef process_bird_audio_parallel():\n    # Create output directories\n    create_output_dirs()\n    \n    # Get list of audio folders (bird species IDs)\n    bird_folders = [f for f in os.listdir(TRAIN_AUDIO_PATH) if os.path.isdir(os.path.join(TRAIN_AUDIO_PATH, f))]\n    \n    # Initialize overall statistics\n    total_stats = {\n        'specs_5sec': 0,\n        'errors': 0,\n        'extended': 0\n    }\n\n    # Process bird audio files (positive samples)\n    print(f\"Processing {len(bird_folders)} bird species folders...\")\n    for bird_folder in bird_folders:\n        bird_folder_path = os.path.join(TRAIN_AUDIO_PATH, bird_folder)\n        audio_files = [f for f in os.listdir(bird_folder_path) if f.endswith(('.ogg', '.mp3', '.wav'))]\n        for audio_file in audio_files:\n            stats = process_audio_file((bird_folder, audio_file, bird_folder_path))\n            for key in total_stats:\n                total_stats[key] += stats[key]\n\n    # Process soundscape files (negative samples)\n    soundscape_stats = process_soundscapes()\n    for key in total_stats:\n        total_stats[key] += soundscape_stats[key]\n\n    return total_stats\n\n# 배치 저장 함수 추가\ndef save_spectrograms_in_batches(all_bird_data, batch_size=5000):\n    \"\"\"배치 단위로 스펙트로그램 저장 (메모리 효율적)\"\"\"\n    keys = list(all_bird_data.keys())\n    num_batches = len(keys) // batch_size + (1 if len(keys) % batch_size else 0)\n    \n    print(f\"Saving {len(keys)} spectrograms in {num_batches} batches...\")\n    \n    for i in range(num_batches):\n        start_idx = i * batch_size\n        end_idx = min((i + 1) * batch_size, len(keys))\n        batch_keys = keys[start_idx:end_idx]\n        \n        batch_data = {key: all_bird_data[key] for key in batch_keys}\n        \n        filename = f'melspecs_uint16_batch_{i:03d}.npy'\n        np.save(filename, batch_data)\n        print(f\"Saved batch {i+1}/{num_batches}: {filename} ({len(batch_keys)} samples)\")\n        \n        # 메모리 정리\n        del batch_data\n        gc.collect()\n    \n    print(\"All spectrograms saved successfully!\")\n\n# Function to display examples of created mel-spectrograms\ndef visualize_examples_from_dict(all_bird_data):\n    import matplotlib.pyplot as plt\n    keys = list(all_bird_data.keys())[:4]\n    for i, k in enumerate(keys):\n        plt.subplot(1, 4, i+1)\n        # uint16을 float로 변환하여 시각화\n        spec_float = all_bird_data[k].astype(np.float32) / 65535.0\n        plt.imshow(spec_float, aspect='auto', origin='lower', cmap='viridis')\n        plt.title(k)\n        plt.axis('off')\n    plt.tight_layout()\n    plt.show()\n\n# Get statistics about processed files\ndef get_processing_stats():\n    # Count number of created files\n    specs_5sec_count = sum(len(files) for _, _, files in os.walk('specs_5sec'))\n    \n    print(f\"Processing statistics:\")\n    print(f\"- Created {specs_5sec_count} mel-spectrograms for 5-second segments\")\n\n# Run the main process\nif __name__ == \"__main__\":\n    print(\"Starting parallel audio processing...\")\n    print(\"Using uint16 format for memory efficiency...\")\n    print(\"Short audio files (<5s) will be extended using repeat padding...\")\n    \n    # Important: matplotlib needs to be configured for non-interactive backend in parallel processing\n    plt.switch_backend('agg')\n    \n    # Process files in parallel\n    stats = process_bird_audio_parallel()\n    \n    print(\"Processing completed!\")\n    print(f\"Processing statistics from parallel processing:\")\n    print(f\"- Created {stats['specs_5sec']} mel-spectrograms for 5-second segments\")\n    print(f\"- Extended {stats['extended']} short audio files (<5s) using repeat padding\")\n    print(f\"- Encountered {stats['errors']} errors during processing\")\n    \n    # 추가: positive/negative 샘플 수 확인\n    positive_samples = len([k for k in all_bird_data.keys() if not k.startswith('negative-')])\n    negative_samples = len([k for k in all_bird_data.keys() if k.startswith('negative-')])\n    print(f\"- Positive samples: {positive_samples}\")\n    print(f\"- Negative samples: {negative_samples}\")\n    print(f\"- Negative ratio: {negative_samples/(positive_samples+negative_samples)*100:.1f}%\")\n\n    # 메모리 효율적인 배치 저장\n    print(\"\\nSaving spectrograms in batches to avoid memory issues...\")\n    save_spectrograms_in_batches(all_bird_data, batch_size=5000)\n    \n    # 메모리 사용량 추정\n    total_samples = len(all_bird_data)\n    memory_mb = total_samples * 256 * 256 * 2 / (1024 * 1024)  # uint16 = 2 bytes\n    print(f\"\\nMemory usage estimate: {memory_mb:.1f} MB ({memory_mb/1024:.1f} GB)\")\n    \n    # Double-check with filesystem statistics\n    print(\"\\nVerifying with filesystem statistics:\")\n    get_processing_stats()\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-05T11:18:34.043010Z","iopub.execute_input":"2025-06-05T11:18:34.043283Z","iopub.status.idle":"2025-06-05T11:18:52.580957Z","shell.execute_reply.started":"2025-06-05T11:18:34.043259Z","shell.execute_reply":"2025-06-05T11:18:52.579642Z"}},"outputs":[],"execution_count":null}]}