4
\$\begingroup\$

I am trying to demodulate an analog video signal of PAL format and encountered a problem in demodulating the color part. So my input data is as follows: I have 2500 spectra, spectrum width 200 MHz, my sample rate 245.76 MHz. The time of each spectrum is 33 μs. The signal is at a frequency of 5820 MHz and has the following form: enter image description here

I successfully manage to demodulate the luma component and get a picture in black and white background enter image description here

But when I try to get a picture in color, nothing happens. The signal I get after demodulating the chroma looks terrible and I don't know what to do. enter image description here

The photo should be like this, I modulate the signal through the hacktv program using hackrf: enter image description here

The entire code that I use for chroma demodulation is given below. Please, if anyone knows how to properly perform chroma demodulation, tell me. I've been trying to do this for over 3 months and nothing works.

class MyApp(QMainWindow):
    def __init__(self, full_signal):
        super().__init__()

        self.UV = None
        self.real_env = None
        self.imag_env = None
        self.signal_freq = None
        self.full_signal = full_signal

        uic.loadUi("main.ui", self)
        self.figure_time = Figure()
        self.canvas_time = FigureCanvas(self.figure_time)
        self.plot_signal_time_real = self.figure_time.add_subplot(111)
        self.figure_time.tight_layout()
        self.line_time_real = None
        self.line_time_real_env = None
        self.line_time_imag = None
        self.line_time_imag_env = None

        toolbar_time = NavigationToolbar(self.canvas_time, self)
        layout_time = QVBoxLayout(self.findChild(QWidget, "widget_plot_time"))
        layout_time.setContentsMargins(0, 0, 0, 0)
        layout_time.addWidget(toolbar_time)
        layout_time.addWidget(self.canvas_time)

        self.figure_freq = Figure()
        self.canvas_freq = FigureCanvas(self.figure_freq)
        self.plot_signal_freq = self.figure_freq.add_subplot(111)
        self.figure_freq.tight_layout()
        self.line_freq = None

        toolbar_freq = NavigationToolbar(self.canvas_freq, self)
        layout_freq = QVBoxLayout(self.findChild(QWidget, "plot_freq"))
        layout_freq.setContentsMargins(0, 0, 0, 0)
        layout_freq.addWidget(toolbar_freq)
        layout_freq.addWidget(self.canvas_freq)

        self.figure_imag = Figure()
        self.canvas_imag = FigureCanvas(self.figure_imag)
        self.plot_signal_imag = self.figure_imag.add_subplot(111)
        self.figure_imag.tight_layout()
        self.line_imag = None

        toolbar_imag = NavigationToolbar(self.canvas_imag, self)
        layout_imag = QVBoxLayout(self.findChild(QWidget, "plot_imag"))
        layout_imag.setContentsMargins(0, 0, 0, 0)
        layout_imag.addWidget(toolbar_imag)
        layout_imag.addWidget(self.canvas_imag)
        #############################

        self.stepSlider = 1
        self.sliderShift.valueChanged.connect(self.on_slider_value_changed)
        self.sliderShift.setMaximum(LENGTH_SPECTRUM_CUT)
        self.sliderShift.setMinimum(0)

        self.buttonUpdate.clicked.connect(self.plot_example)

        self.tabWidget.currentChanged.connect(self.on_tab_changed)

    ###############################################################################
    def on_slider_value_changed(self, value):
        rounded_value = (value // self.stepSlider) * self.stepSlider
        if rounded_value != value:
            self.sliderShift.blockSignals(True)
            self.sliderShift.setValue(rounded_value)
            self.sliderShift.blockSignals(False)

        self.textSlider.setText(str(rounded_value))

        self.plot_example(rounded_value)

    ##########################################################################################################################
    def plot_example(self, shiftSignal=0):
        self.UV = []
        self.real_env = []
        self.imag_env = []
        fs = 4.43e6
        band_with = 1e6
        low = (fs - band_with) / (sample_rate / 2)
        high = (fs + band_with) / (sample_rate / 2)

        for i in range(0, NSpec):
            spectr = self.full_signal[i]

            spectr = np.roll(spectr, signalShift)

            right_spectr = spectr[int(8192 / 2 - (LENGTH_SPECTRUM_CUT // 2)):int(8192 / 2 + (LENGTH_SPECTRUM_CUT // 2))]
            spectr_512 = np.roll(right_spectr, shiftSignal)

            if i == 0:
                self.signal_freq = np.abs(spectr_512)
            signal_time = np.fft.ifft(np.fft.ifftshift(spectr_512))

            b, a = signal.butter(10, [low, high], btype='bandpass')
            chroma_signal = signal.filtfilt(b, a, signal_time)

            chroma_signal = np.roll(np.fft.fftshift(np.fft.fft(chroma_signal)), -int(fs + (band_with // 2) // 30))

            chroma_signal = np.fft.ifft(np.fft.fftshift(chroma_signal))
            b_lp, a_lp = signal.butter(5, 1e6 / (sample_rate / 2))
            signal_time = signal.filtfilt(b_lp, a_lp, chroma_signal)

            self.UV.append(signal_time)

        self.UV = np.concatenate(self.UV)

        self.plot_time()
        self.plot_frequency()
        self.plot_image()

    ##########################################################################################################################
    def plot_time(self):
        if not self.UV is None:
            if self.line_time_real is None:
                self.plot_signal_time_real.clear()
                self.line_time_real, = self.plot_signal_time_real.plot(np.real(self.UV), label="Real")

                self.plot_signal_time_real.legend("real")
                self.plot_signal_time_real.grid(True)
            else:
                self.line_time_real.set_ydata(np.real(self.UV))
                self.plot_signal_time_real.relim()
                self.plot_signal_time_real.autoscale_view()
            self.canvas_time.draw()

    ##########################################################################################################################
    def plot_frequency(self):
        if not self.signal_freq is None:
            if self.line_freq is None:
                self.plot_signal_freq.clear()
                self.line_freq, = self.plot_signal_freq.plot(self.signal_freq)
                self.plot_signal_freq.set_title("Синус")
                self.plot_signal_freq.grid(True)
            else:
                self.line_freq.set_ydata(self.signal_freq)
                self.line_freq.set_xdata(np.arange(len(self.signal_freq)))
                self.plot_signal_freq.relim()
                self.plot_signal_freq.autoscale_view()
            self.canvas_freq.draw()

    ##########################################################################################################################
    def plot_image(self):
        U_lines = np.real(self.UV)
        V_lines = np.imag(self.UV)

        U_lines = U_lines / np.max(np.abs(U_lines))
        V_lines = V_lines / np.max(np.abs(V_lines))

        image = []
        picture = []
        for i in range(0, testFrame.indexLine):
            Y = IQ[testFrame.index_start_line[i]:testFrame.index_start_line[i] + SAMPLE_IN_FULL_NOT_SYNC]
            U = U_lines[testFrame.index_start_line[i]:testFrame.index_start_line[i] + SAMPLE_IN_FULL_NOT_SYNC]
            V = V_lines[testFrame.index_start_line[i]:testFrame.index_start_line[i] + SAMPLE_IN_FULL_NOT_SYNC]

            if i % 2 == 0:
                V = V * -1

            R = Y + 1.13983 * V
            G = Y - 0.39465 * U - 0.58060 * V
            B = Y + 2.03211 * U

            R = np.clip(R * 255, 0, 255).astype(np.uint8)
            G = np.clip(G * 255, 0, 255).astype(np.uint8)
            B = np.clip(B * 255, 0, 255).astype(np.uint8)

            image.append(np.stack([R, G, B], axis=-1))

        picture.append(image)

        for im in picture:
            if self.line_imag is None:
                self.line_imag = self.plot_signal_imag.imshow(im, aspect='auto')
                self.plot_signal_imag.axis("off")
            else:
                self.line_imag.set_data(im)
            self.canvas_imag.draw()
    def on_tab_changed(self, index):
        if index == 0:
            self.plot_frequency()
        elif index == 1:
            self.plot_image()
\$\endgroup\$
1
  • \$\begingroup\$ What kind of PAL signals are these ? \$\endgroup\$ Commented Jun 10 at 17:25

1 Answer 1

3
\$\begingroup\$

Reading your code, you seem to be trying to decode the chrominance subcarrier at 4.43 MHz while, according to Wikipedia, it should be 4.43361875 MHz. However, the error of 3.61875 kHz would explain a slow color drift, not what you get.

There are variants PAL M and PAL N with significantly different subcarrier frequencies (see Wikipedia again), of 3.58205625 MHz and 3.575611 MHz. The frequency error looks somewhat compatible with what you get.

Note that the “Phase Alternating Line” modulation was intended to be decoded by adding/subtracting two consecutive lines (using an analog 64 µs delay line) and get rid of the colour shift that was common with NTSC. Your code implement what was called “poor man’s PAL” demodulation.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.