# Bitplan encoding and decoding¶

`SeparatedBitPlanEncoder`

and `MixingBitPlanDecoder`

implement a
simple but powerful encoding scheme (especially useful for RGB images).

## Sample data¶

```
import numpy as np
```

```
sample = np.array([[204, 180, 73], [133, 11, 39]], dtype='uint8')
sample
```

```
array([[204, 180, 73],
[133, 11, 39]], dtype=uint8)
```

## Encoding¶

Let’s take an RGB colored image: it is composed of 3 channels (red,
green and blue) of the same width and height of `uint8`

. The
`SeparatedBitPlanEncoder`

flattens and concatenates each channel and builds the binary representation of them.

We start by explaining what happens with the default setting
`n_bits=8`

and `starting_bit=0`

and an array of `uint8`

in input.

```
import lightonml.encoding.base as base
```

```
encoder = base.SeparatedBitPlanEncoder()
encoder
```

```
SeparatedBitPlanEncoder(n_bits=8, starting_bit=0)
```

```
encoded_sample = encoder.transform(sample)
print('The encoded sample has shape {}.'.format(encoded_sample.shape))
print('The shape is (n_samples*n_bits, n_features), in this case (2*8, 3)')
print(encoded_sample)
```

```
The encoded sample has shape (16, 3).
The shape is (n_samples*n_bits, n_features), in this case (2*8, 3)
[[0 0 1]
[0 0 0]
[1 1 0]
[1 0 1]
[0 1 0]
[0 1 0]
[1 0 1]
[1 1 0]
[1 1 1]
[0 1 1]
[1 0 1]
[0 1 0]
[0 0 0]
[0 0 1]
[0 0 0]
[1 0 0]]
```

Let’s see what happens inside the `transform`

method:

we add an auxiliary axis to go 3D

we unpack the bit representation on the auxiliary axis

```
# record the original dimensions
n_samples, n_features = sample.shape
print('Original shape: ({}, {})'.format(n_samples, n_features))
# add an auxiliary axis: [n_samples, n_features] -> [n_samples, n_features, 1]
sample_uint8 = np.expand_dims(sample, axis=2).view(np.uint8)
print('Expanded shape: {}'.format(sample_uint8.shape))
# Unpacks the bits along the auxiliary axis: [n_samples, n_features, 1] -> [n_samples, n_features, 8]
sample_uint8_unpacked = np.unpackbits(sample_uint8, axis=2)
print('Unpacked shape: {}'.format(sample_uint8_unpacked.shape))
print('Unpacked sample')
print(sample_uint8_unpacked)
```

```
Original shape: (2, 3)
Expanded shape: (2, 3, 1)
Unpacked shape: (2, 3, 8)
Unpacked sample
[[[1 1 0 0 1 1 0 0]
[1 0 1 1 0 1 0 0]
[0 1 0 0 1 0 0 1]]
[[1 0 0 0 0 1 0 1]
[0 0 0 0 1 0 1 1]
[0 0 1 0 0 1 1 1]]]
```

In `uint8`

we can represent the interval \([0, 255]\). Let’s take
the first row of `sample`

:

powers of 2 |
\(2^7 (128)\) |
\(2^6 (64)\) |
\(2^5 (32)\) |
\(2^4 (16)\) |
\(2^3 (8)\) |
\(2^2 (4)\) |
\(2^1 (2)\) |
\(2^0 (1)\) |
---|---|---|---|---|---|---|---|---|

binary rep of 204 |
1 |
1 |
0 |
0 |
1 |
1 |
0 |
0 |

binary rep of 180 |
1 |
0 |
1 |
1 |
0 |
1 |
0 |
0 |

binary rep of 73 |
0 |
1 |
0 |
0 |
1 |
0 |
0 |
1 |

This is the unpacked bit representation for each element, with bits going from the most significant (MSB) to the least significant (LSB).

we reverse the order of the bit to go from the least significant to the most significant bit

```
# Reverse the order of bits: MSB to LSB becomes LSB to MSB
# LSB = Least Significant Bit
# MSB = Most Significant Bit
sample_uint8_reversed = np.flip(sample_uint8_unpacked, axis=2)
print('Reversed sample')
print(sample_uint8_reversed)
print('You can see that we just reversed the order of the representation.')
```

```
Reversed sample
[[[0 0 1 1 0 0 1 1]
[0 0 1 0 1 1 0 1]
[1 0 0 1 0 0 1 0]]
[[1 0 1 0 0 0 0 1]
[1 1 0 1 0 0 0 0]
[1 1 1 0 0 1 0 0]]]
You can see that we just reversed the order of the representation.
```

we switch the auxiliary axis with the features axis

```
# switch axis 2 with axis 1
encoded_sample = np.transpose(sample_uint8_reversed, [0, 2, 1])
print('Encoded sample')
print(encoded_sample)
print('We have switched axis 1 and 2, the representation is now on columns.')
```

```
Encoded sample
[[[0 0 1]
[0 0 0]
[1 1 0]
[1 0 1]
[0 1 0]
[0 1 0]
[1 0 1]
[1 1 0]]
[[1 1 1]
[0 1 1]
[1 0 1]
[0 1 0]
[0 0 0]
[0 0 1]
[0 0 0]
[1 0 0]]]
We have switched axis 1 and 2, the representation is now on columns.
```

we select the bit representation or a part of it by slicing

```
# slicing does nothing if self.starting_bit=0 and n_bits=bitwidth of input - like in this case
encoded_sample = encoded_sample[:, encoder.starting_bit:encoder.n_bits + encoder.starting_bit, :]
print(encoded_sample)
```

```
[[[0 0 1]
[0 0 0]
[1 1 0]
[1 0 1]
[0 1 0]
[0 1 0]
[1 0 1]
[1 1 0]]
[[1 1 1]
[0 1 1]
[1 0 1]
[0 1 0]
[0 0 0]
[0 0 1]
[0 0 0]
[1 0 0]]]
```

we reshape the encoded sample to [n_samples * n_bits, n_features]

In the end we get a representation were the columns are concatenated
`n_bits`

representation of each feature of the samples.

```
# the encoded sample is then reshaped to [n_samples * n_bits, n_features]
reshaped_encoded_sample = encoded_sample.reshape((n_samples * encoder.n_bits, n_features))
print('Reshaped encoded shape: {}'.format(reshaped_encoded_sample.shape))
print('Encoded sample:')
print(reshaped_encoded_sample)
print('Each column is the concatenation of separate columns of the previous cell')
```

```
Reshaped encoded shape: (16, 3)
Encoded sample:
[[0 0 1]
[0 0 0]
[1 1 0]
[1 0 1]
[0 1 0]
[0 1 0]
[1 0 1]
[1 1 0]
[1 1 1]
[0 1 1]
[1 0 1]
[0 1 0]
[0 0 0]
[0 0 1]
[0 0 0]
[1 0 0]]
Each column is the concatenation of separate columns of the previous cell
```

## Decoding¶

```
decoder = base.MixingBitPlanDecoder(decoding_decay=2)
decoder
```

```
MixingBitPlanDecoder(decoding_decay=2, n_bits=8)
```

Note that here we set `decoding_decay`

\(=2\), but when using the
OPU, where the random features are in \([0, 255]\), you need to use
\(0.5\).

```
decoded_sample = decoder.transform(reshaped_encoded_sample)
print('The decoded sample returns to the original shape {} [n_samples, n_features].'.format(decoded_sample.shape))
print(decoded_sample)
```

```
The decoded sample returns to the original shape (2, 3) [n_samples, n_features].
[[ 204. 180. 73.]
[ 133. 11. 39.]]
```

This is what happens in the `transform`

method:

we compute what was the original shape of the data

Note

`n_bits`

must be set to the same value used for the encoder, otherwise an error is raised.

```
# compute the original shape of the data
n_out, n_features = reshaped_encoded_sample.shape
n_dim_0 = n_out // decoder.n_bits
```

we reshape the array to 3D [n_samples, n_bits, n_features]

```
# the data are reshaped in 3D [n_samples, n_bits, n_features]
reshaped_encoded_sample = np.reshape(reshaped_encoded_sample, (n_dim_0, decoder.n_bits, n_features))
```

we build an array with decaying factors using:

we multiply the encoded sample with the decay factors along the bit dimension

```
# a decay_factors array is built, that weights the importance of every bit in the
# product on the second line
decay_factors = np.reshape(decoder.decoding_decay ** np.arange(decoder.n_bits), (1, decoder.n_bits, 1))
decayed_sample = reshaped_encoded_sample * decay_factors
```

we sum over the bit axis

```
decoded_sample = np.sum(decayed_sample, axis=1)
```

```
decoded_sample
```

```
array([[204, 180, 73],
[133, 11, 39]])
```

## Optional arguments¶

The parameters `n_bits`

and `starting_bits`

defaults can be changed.
This is useful if you notice that certain bitplanes are just noise and
you want to throw them away.