Engee documentation
Notebook

Slow-scan television

In this example, we will consider the image transmission protocol based on Slow Scan Television (SSTV), which is slow—scan television or low-frame television. It is a unique and flexible image transmission protocol that is ideally suited for narrow-band communication channels. The variability of SSTV allows you to adapt the transmission to specific conditions, and the simplicity of its implementation makes it popular among radio amateurs. However, due to the low transfer rate, SSTV is not suitable for tasks requiring high speed or high image quality.

This model is not tied to any communication standard implemented in practice, and primarily describes the technologies and methods that we can apply in Engee for such systems.

The general principle of SSTV is to transmit static images over narrow-band communication channels such as radio channels. The process can be divided into several stages: image encoding, modulation, transmission, reception, demodulation and decoding.

image_2.png

Next, let's move on to the implemented model.

Model analysis

The following logic is implemented at the input of the model: we read the image into the model using the Images library and normalize the image.

image_2.png
In [ ]:
using Images

path_img = "$(@__DIR__)/img.jpg"
inp_img = imrotate(Gray.(load(path_img)), deg2rad(-90))

S = size(inp_img)
println("Input image size: $(S))")
Размер входного изображения: (160, 160))

In this example, all work with the image will be carried out in the format of gray shadows of the representation. If we consider Julia representations, this means that pixels are encoded in the range from 0 to 1, where 0 is black and 1 is white.

In [ ]:
Gray.([1,0])
Out[0]:
No description has been provided for this image

Now let's move on to the next block, the interleaving block. Interleaving is a technique used in data transmission systems to increase error tolerance. The basic idea is to change the order of the data before transmission, and then neighboring bits or characters will be separated in the stream.

image_2.png

This block is implemented using the Engee Function. At the input, it has the number of interleaving groups and the input signal itself.

The interleaving formula:

index = mod((i - 1) * n, N) + div(i - 1, div(N, n)) + 1

The depersonalization formula:

index = mod((i - 1), n) * div(N, n) + div(i - 1, n) + 1

These formulas ensure that the index does not exceed the limits of the array, since the mod operation is used to limit the index within N.

Let's compare the formulas themselves.

  1. There is a difference in calculating the position within the group.
    In the first formula, the position within the group is calculated as follows: mod((i−1),n).
    In the second formula, the position within the group is calculated as follows: mod((i−1)×n,N).

  2. There are also differences in the calculation of the group number.
    In the first formula, the group number is calculated as follows: div(i−1,n).
    In the second formula, the group number is calculated in this way: div(i−1,div(N,n)).

That is, as we can see, they are mutually reversible. Next, let's look at a simple example of the interleaving we implemented.

In [ ]:
L = 4
data_in = collect(1:L)
n = 2 # parts of the array
println("Initial data: ", data_in)

# The interleaving function
N = length(data_in)
interleaved_data = similar(data_in)

for i in 1:N
    index = mod((i - 1) * n, N) + div(i - 1, div(N, n)) + 1
    interleaved_data[i] = data_in[index]
end

println("Interleaved data: ", interleaved_data)

# The interleaving function
N = length(interleaved_data)
deinterleaved_data = similar(interleaved_data)

for i in 1:N
    index = mod((i - 1), n) * div(N, n) + div(i - 1, n) + 1
    deinterleaved_data[i] = interleaved_data[index]
end

println("Deperjected data: ", deinterleaved_data)
Исходные данные: [1, 2, 3, 4]
Перемеженные данные: [1, 3, 2, 4]
Деперемеженные данные: [1, 2, 3, 4]

As we can see, the algorithm is working correctly. Now let's set the number of groups for our model.

In [ ]:
num = 400 # Number of groups
println("Number of values in the group: $((S[1]*S[2])/num)")
Количество значений в группе: 64.0

The next block we use is XOR. This operation is reversible and allows you to implement a simple version of the scrambler.

image_2.png

A scrambler (from the English scramble — to encrypt, to mix) is a software or hardware device that performs scrambling, that is, the reversible transformation of a digital stream without changing the transmission rate in order to obtain the properties of a random sequence. After scrambling, the occurrence of "1" and "0" in the output sequence is equally likely.

Next, we will set the bitmask for our scrambler.

In [ ]:
bit_mask = 0b01010101 # max 8 bit
println("bit_mask: $bit_mask")
bit_mask: 85

The next blocks that we will consider are a bundle of 16—FSK modulation and automatic gain control (AGC).

image.png

FSK is a modulation method in which digital data (bits) are transmitted by changing the frequency of the carrier signal. Each bit value (0 or 1) or group of bits corresponds to a certain frequency. 16-FSK is an extension of FSK, which uses 16 different frequencies to transmit 4 bits of data simultaneously.
Bit and number matching table for 16-FSK:

Bit combination Number
[0, 0, 0, 0] 1
[0, 0, 0, 1] 3
[0, 0, 1, 0] 5
[0, 0, 1, 1] 7
[0, 1, 0, 0] 9
[0, 1, 0, 1] 11
[0, 1, 1, 0] 13
[0, 1, 1, 1] 15
[1, 0, 0, 0] -1
[1, 0, 0, 1] -3
[1, 0, 1, 0] -5
[1, 0, 1, 1] -7
[1, 1, 0, 0] -9
[1, 1, 0, 1] -11
[1, 1, 1, 0] -13
[1, 1, 1, 1] -15

AGC is a system that automatically adjusts the signal gain level on the receiving side to maintain a constant output signal level regardless of changes in the input signal level. In our case, we have implemented logic that calculates the output power relative to the data before the signal is amplified, but you can easily replace this approach with a preset value.

image_4.png

Based on the test below, we can clearly conclude that at 16-FSK, the desired signal power is 85 watts.

In [ ]:
norm_img = ((channelview(inp_img))[:])
power_img = sum(norm_img.^2) / length(norm_img)
println("Input signal power: ", power_img, " Tue")

values = [1:2:15; -1:-2:-15] 
v = [values[rand(1:length(values))] for _ in 1:length(norm_img)]
power_desired = sum(v .^ 2) / length(v)
println("Desired power: ", power_desired, " Tue")

K = sqrt(power_desired / power_img) # We use the square root, since the power is proportional to the square of the amplitude.
println("Gain factor: ", K)

amplified = norm_img .* K  # Enhanced signal

power_out = sum(amplified .^ 2) / length(amplified) 
println("Output signal power: ", round(power_out, digits=2), " Tue")
Мощность входного сигнала: 0.3573653492647059 Вт
Желаемая мощность: 84.9459375 Вт
Коэффициент усиления: 15.417540102273989
Мощность выходного сигнала: 84.97 Вт

The following blocks that we will consider are FM Modulator Baseband and FM Demodulator Baseband. This is a modulation that converts the base signal into a frequency-modulated (FM) signal.

Two key parameters are used in FM radio broadcasting.

  1. The carrier frequency is the main frequency of the signal, which is modulated to transmit information. In the FM broadcasting range, it is between 87.5 MHz and 108 MHz.

  2. The frequency of deviation is the maximum frequency deviation from the carrier frequency during signal transmission. For standard FM broadcasting, the maximum allowable deviation is ± 75 kHz.

Thus, if the carrier frequency is, say, 100 MHz, then the actual frequency of the transmitted signal will range from 99.925 to 100.075 MHz.

In [ ]:
fs = 100e3 # Hz
println("Carrier frequency: $fs Hz")

deviation_fs = fs * 0.05 # 5% of fs
println("Frequency of deviation: $deviation_fs Hz")
Несущая частота: 100000.0 Гц
Частота девиации: 5000.0 Гц

The last block that we will consider is the communication channel block. To apply noise, it uses the Signal-to-Noise Ratio (SNR) metric, the signal-to-noise ratio. This metric shows how much stronger the useful signal is than the noise.
image.png

The higher the SNR, the higher the signal quality, as noise has less effect on the data. Conversely, the lower the SNR, the lower the signal quality, because noise begins to dominate the useful signal.

For example:

  1. If SNR = 20 dB, it means that the signal is 100 times more powerful than the noise (on a linear scale);
  2. If SNR = 0 dB, it means that the signal and noise power are equal;
  3. If SNR = -10 dB, it means that the noise is 10 times stronger than the signal.
In [ ]:
snr = 10;
println("SNR: $snr dB")
snr_linear = 10^(snr / 10)
signal_power = 1
noise_power = 1 / snr_linear
println("Noise power: $noise_power")
SNR: 10 дБ
Мощность шума: 0.1

Launching the model and analyzing the results

Let's set the simulation parameters and run the model.

In [ ]:
st = 1/fs*S[1]*S[2] # Sample time
println("Time of one countdown: $st")
solver_step = st/S[1]/S[2]
println("Solver step: $solver_step")
time_stop = st*2.1
println("Simulation step: $st")
Время одного отсчёта: 0.256
Шаг решателя: 1.0e-5
Шаг моделирования: 0.256
In [ ]:
name_model = "SSTV"
Path = (@__DIR__) * "/" * name_model * ".engee"

if name_model in [m.name for m in engee.get_all_models()] # Checking the condition for loading a model into the kernel
    model = engee.open( name_model ) # Open the model
    model_output = engee.run( model, verbose=true ); # Launch the model
else
    model = engee.load( Path, force=true ) # Upload a model
    model_output = engee.run( model, verbose=true ); # Launch the model
    engee.close( name_model, force=true ); # Close the model
end
sleep(1)
Building...
Progress 0%
Progress 5%
Progress 11%
Progress 16%
Progress 22%
Progress 28%
Progress 34%
Progress 39%
Progress 45%
Progress 50%
Progress 60%
Progress 67%
Progress 72%
Progress 81%
Progress 87%
Progress 93%
Progress 99%
Progress 100%
Progress 100%

Let's visualize the results of our model and summarize the results.

In [ ]:
println("SNR: $snr dB")
println()
ber = ((collect(BER)).value)[end] 
println("Total bits: $(Int(ber[3]))")
println("Number of errors: $(Int(ber[2]))")
println("BER: $(round(ber[1], digits=2))")

inp = plot(framestyle=:none, grid=false, title = "Entrance")
heatmap!( 1:S[2], 1:S[1], permutedims(inp_img, (2, 1)), colorbar=false)

sim_img = imrotate(colorview(Gray, ((collect(img_FM))[3,:].value)), deg2rad(-90))
sim = plot(framestyle=:none, grid=false, title = "Exit")
heatmap!( 1:size(sim_img, 2), 1:size(sim_img, 1), permutedims(sim_img, (2, 1)), colorbar=false)

sim_error = (collect(error_sim)).value 
Average_error = round(sum(abs.(sim_error))/length(sim_error)*100, digits=2)
err = plot(sim_error, title = "The average error is $Average_error % ")

plot(plot(inp, sim), err, layout = grid(2, 1, heights=[0.7, 0.3]), legend=false) 
SNR: 10 дБ

Всего бит: 430088
Кол-во ошибок: 39938
BER: 0.09
Out[0]:

As can be seen from the simulation results, we get good BER values at the output. If we take a closer look at the interleaving and scrambling settings, we can get an even better picture quality. For these purposes, you are provided with a second script SSTV_test. There is no superfluous description in it, but only the code of this project is given.

Conclusion

In this example, we examined the modeling capabilities of the slow-scan television-based image transmission protocol in Engee, and also explored many tools that allow us to perform such projects in Engee. As you can see from our example, Engee has all the necessary functions and tools for these models, and they work correctly.