Если вам нужно написать код самостоятельно, вот уловка для простой схемы «дельта» кодирования, которая позволяет избежать ненужной потери точности.
Идея состоит в том, что компрессор должен вычислять дельты НЕ между текущими данными точка и последний, но между текущей точкой данных и тем, что декомпрессор вычислит для последнего. Это означает, что ошибки квантования не складываются.
В качестве простого примера, который вы можете просмотреть / поэкспериментировать с приведенным здесь кодом c, который сжимает двойные числа в (start double) и последовательность чисел с плавающей запятой:
typedef struct
{ double start;
int n;
float* delta;
} compT;
compT compress( int n, const double* data)
{
compT c = (compT){ data[0], n-1, malloc( (n-1)*sizeof *c.delta)};
double r = c.start;
for( int i=1; i<n; ++i)
{
float d = (float)(data[i]-r);
c.delta[i-1] = d;
r += d;
}
return c;
}
static double* uncompress( compT c)
{
double* d = malloc( (c.n+1)*sizeof *d);
double r = c.start;
d[0] = r;
for( int i=1; i<=c.n; ++i)
{ r += c.delta[i-1];
d[i] = r;
}
return d;
}
compT bad_compress( int n, const double* data)
{
compT c = (compT){ data[0], n-1, malloc( (n-1)*sizeof *c.delta)};
for( int i=1; i<n; ++i)
{
float d = (float)(data[i]-data[i-1]);
c.delta[i-1] = d;
}
return c;
}
При использовании чисел с плавающей запятой нарастание ошибок квантования действительно заметно только на длинных (миллионах) последовательностях данных.
Однако, когда вы надеетесь сжать больше, эффекты более заметны. В приведенном ниже коде для дельт используется int_16t. Я использовал это в случаях, когда значения данных гарантированно составляли около 1 км, поэтому я использовал шкалу (в приведенном ниже коде) 16, чтобы можно было справиться с различиями в 2 км.
typedef struct
{ float scale;
double start;
int n;
int16_t* delta;
} compiT;
compiT compressi( int n, const double* data, float scale)
{
compiT c = (compiT){ scale, data[0], n-1, malloc( (n-1)*sizeof *c.delta)};
double r = c.start;
for( int i=1; i<n; ++i)
{
int16_t d = (int16_t)round(c.scale*(data[i]-r));
c.delta[i-1] = d;
r += ((double)d)/c.scale;
}
return c;
}
compiT bad_compressi( int n, const double* data, float scale)
{
compiT c = (compiT){ scale, data[0], n-1, malloc( (n-1)*sizeof *c.delta)};
for( int i=1; i<n; ++i)
{
int16_t d = (int16_t)round(c.scale*(data[i]-data[i-1]));
c.delta[i-1] = d;
}
return c;
}
static double* uncompressi( compiT c)
{
double* d = malloc( (c.n+1)*sizeof *d);
double r = c.start;
d[0] = r;
for( int i=1; i<=c.n; ++i)
{
double delta = ((double)c.delta[i-1])/c.scale;
r += delta;
d[i] = r;
}
return d;
}
В прогонов произвольной длины максимальная ошибка (ie разница между исходными данными и распакованными сжатыми данными) составляла, как и должно быть, около 3 см, тогда как при использовании bad_compressor ошибки составляли около 0,5 м на прогонах 1000, 2,5 м при пробегах 10000
Конечно, если у вас нет гарантий ограниченности различий, вам нужно будет включить какой-то перезапуск.