2025, Oct 05 07:19

Как распаковать упакованные 12‑битовые выборки в float в Python и MATLAB

Распаковка упакованных 12‑битовых выборок: векторизованный Python/NumPy и эквивалент MATLAB; порядок little‑endian, сдвиги и масштабирование к float32 точно.

Распаковка упакованных 12‑битовых выборок в float в Python и MATLAB

Распространённый формат сбора данных упаковывает две 12‑битовые выборки в 3 байта. Это экономит место, но усложняет последующую обработку — особенно когда нужна векторизованная производительность в средах вроде NumPy и MATLAB. Ниже — краткое пошаговое объяснение: стартуем с рабочей реализации на Python и показываем, как воспроизвести точное поведение в MATLAB без изменения математики и масштабирования.

Схема упаковки и практическая особенность

Каждый 3‑байтовый блок кодирует две выборки. Важно не столько рисовать битовые диаграммы, сколько понимать фактический порядок байтов в памяти. Рабочая реализация рассматривает данные как little‑endian: первый байт в каждой группе из 3 байт содержит наименее значимые биты 24‑битового фрагмента. Два 12‑битовых значения вырезаются из этих 24 бит.

Эталонная реализация на Python

Ниже приведена функция на Python, которая конвертирует буфер байтов с упакованными 12‑битовыми значениями в float32. Она использует приёмы со stride, чтобы сформировать двухколоночное представление поверх 3‑байтовых групп, применяет маски и сдвиги для извлечения двух 12‑битовых целых, затем выполняет сдвиг влево, чтобы корректно разместить знак, и после этого масштабирует.

def decode_s12_packed_to_f32(raw):
    import numpy as np
    import numpy.lib.stride_tricks as st
    i32 = np.frombuffer(raw, dtype=np.int32)
    view2 = np.copy(np.transpose(
        st.as_strided(i32,
            shape=(2, int((i32.size*4)/3)),
            strides=(0, 3), writeable=False)))
    mask12 = (1 << 12) - 1
    view2[:, 0] &= mask12
    view2[:, 0] <<= 20
    view2[:, 1] >>= 12
    view2[:, 1] &= mask12
    view2[:, 1] <<= 20
    return view2.reshape(-1).astype(np.float32) * (2.**-31)

Что на самом деле происходит с битами

Суть проста, как только понятен порядок байтов. Каждые 3 байта дают два 12‑битовых целых. Первая выборка берёт младшие 8 бит из первого байта и младшие 4 бита из второго. Вторая выборка — старшие 4 бита из второго байта и все 8 бит из третьего. После извлечения 12‑битовых целых сдвиг влево на 20 бит переносит знаковый бит в 31‑й разряд 32‑битового знакового целого. Умножение на 2^-31 даёт нужное масштабирование в float.

Именно поэтому реализация на Python использует int32 для побитовых операций, маски для выделения 12 бит, сдвиги для разделения и выравнивания полей, затем финальный сдвиг влево, приводение к float32 и масштабирование.

Эквивалент на MATLAB один к одному

Следующий код MATLAB воспроизводит те же побитовые операции и масштабирование. Предполагается, что на вход подаётся плоский массив байтов, длина которого кратна 3. Для наглядности массив преобразуется к размеру 3-на-N, затем из каждого 3‑байтового столбца собираются два 12‑битовых значения. Приведение к int32 перед сдвигами соответствует поведению кода на Python.

% raw8 — это вектор байтовых значений (0..255), длина которого кратна 3
blk = int32(reshape(raw8, 3, []));
out_i32 = zeros(2, size(blk, 2), 'int32');
out_i32(2, :) = bitshift(blk(3, :), 4) + bitshift(blk(2, :), -4);
out_i32(1, :) = bitshift(bitand(blk(2, :), 15), 8) + blk(1, :);
out_i32 = bitshift(out_i32, 20);
out = single(out_i32) * 2^-31;
out = reshape(out, 1, []);

Это полностью отражает логику Python: собрать два 12‑битовых значения в порядке little‑endian, сдвинуть влево на 20, чтобы выставить знак, привести к single и умножить на 2^-31. Необязательный bitand с 15 лишь подчёркивает намерение; на результат он не влияет, потому что последующий сдвиг влево всё равно отбрасывает старшие биты.

Почему важно сделать это правильно

Форматы с битовой упаковкой усиливают даже небольшие ошибки. Перепутанный порядок байтов или сдвиг на четыре бита не туда незаметно портит каждую выборку. Точные, идентичные операции в Python и MATLAB обеспечивают согласованность при валидации наборов данных и кросс‑проверке конвейеров анализа. В сценариях, где измерения хранятся вместе с параметрами и метаданными, например как элементы внутри ZIP‑контейнера, обычно сначала получают сырой байтовый полезный блок, а затем детерминированно распаковывают его, как показано выше.

В некоторых рабочих процессах, когда данные лежат в обычном бинарном файле, бывает удобно читать их напрямую как 12‑битовые поля. Например, MATLAB умеет читать 12‑битовые значения через fread с точностью ubit12 — пригодность зависит от того, как байты расположены в файле.

Ключевые выводы

Данные идут в little‑endian: две 12‑битовые выборки на каждые 3 байта. Функция на Python демонстрирует векторизованный подход с использованием stride‑приёмов, масок и сдвигов, затем постановку знака через сдвиг влево на 20 бит и масштабирование 2^-31 до float32. Приведённый код MATLAB реализует ту же последовательность операций и даёт те же численные результаты. Перенося решение между средами, приводите к int32 перед сдвигами, точно воссоздавайте оба 12‑битовых значения согласно порядку байтов и сохраняйте финальный шаг нормализации.

Статья основана на вопросе на StackOverflow от datenwolf и ответе Cris Luengo.