Défi énigme du CST – Énigme 6 - Solution

Télécharger la vidéo (.Mkv File)

 

Solution

Pour résoudre cette énigme, le participant doit utiliser une vidéo contenant un message secret. Comme la couleur du message secret est similaire à celle de l’arrière-plan, on ne peut le voir à l’œil nu. Le message peut être extrait de plusieurs façons, mais voici comment l’extraction est effectuée dans le script de notre solution :

  • La vidéo est d’abord analysée image par image.
  • Sur chaque image, on compte le nombre de pixels d’une couleur donnée.

Après avoir répété cette étape sur différentes images, on constate une constance parmi les couleurs apparaissant plus de 2 000 fois, signe que ces couleurs doivent appartenir à l’arrière-plan. À la lumière de cette information, il suffit de changer les couleurs d’arrière-plan à blanc, puis d’enregistrer l’image pour trouver le message secret.

Il pourrait s’agir de la solution finale, mais tentons maintenant de supprimer les images en double.

Pour ce faire, il suffit d’omettre les images qui sont identiques à l’image précédente. Il importe de mentionner que cette méthode ne peut être appliquée à la première et la dernière image en raison de l’effet de fondu. Pour remédier à la situation, il suffit de supprimer ces images en utilisant l’information de base (le compte des secondes et les images par seconde de la vidéo).

Il sera alors possible de lire le message secret dans la vidéo créée à partir des images de la solution.

Voici le script de la solution :

 

from cv2 import VideoCapture, imwrite, CAP_PROP_FPS, CAP_PROP_FRAME_COUNT
import os
import shutil
import numpy as np


ANSWER_TEMP_PATH = 'answer-frames'
CHALLENGE_VIDEO_FILE = 'movie.mkv'
ANSWER_VIDEO_FILE = 'answer.mkv'
INTRO_MESSAGE_LENGTH_SECONDS = 8
ENDING_MESSAGE_LENGTH_SECONDS = 4


def main():
    print('finding the ghost message...')
    create_answer_temp_path()
    extract_ghost_message()
    create_answer_video()
    remove_answer_temp_path()
    print(f'watch the {ANSWER_VIDEO_FILE} to read the ghost message!')


def create_answer_temp_path():
    if not os.path.exists(ANSWER_TEMP_PATH):
        os.makedirs(ANSWER_TEMP_PATH)


def extract_ghost_message():
    video = VideoCapture(CHALLENGE_VIDEO_FILE)
    fps = video.get(CAP_PROP_FPS)
    video_frame_length = video.get(CAP_PROP_FRAME_COUNT)
    success, next_frame = True, None
    frame_count = 0
    unique_frame_count = 0

    while success:
        frame = next_frame
        success, next_frame = video.read()
        if frame_is_interesting(frame, next_frame, frame_count, video_frame_length, fps):
            print(f'decoding frame {frame_count}...')
            ghost_frame = decode_frame(frame)
            save_ghost_frame(ghost_frame, unique_frame_count)
            unique_frame_count += 1
        frame_count += 1


def frame_is_interesting(frame, next_frame, frame_count, video_frame_length, fps):
    return not (is_frame_part_of_intro_message(frame_count, fps)
                or is_frame_part_of_ending_message(frame_count, video_frame_length, fps)
                or is_frame_duplicate(frame, next_frame))


def is_frame_part_of_intro_message(frame_count, fps):
    return frame_count < fps * INTRO_MESSAGE_LENGTH_SECONDS + 15  # 15 was added on the second run to remove excess intro message frames.


def is_frame_part_of_ending_message(frame_count, video_frame_length, fps):
    return frame_count >= video_frame_length - fps * ENDING_MESSAGE_LENGTH_SECONDS - 7  # 7 was added on the second run to remove excess ending message frames.


def is_frame_duplicate(frame, next_frame):
    return np.array_equal(frame, next_frame)


def decode_frame(frame):
    color_frequency = find_color_frequency(frame)
    background_colors = identify_background_colors(color_frequency)  # Turns out the background colors do not change from frame to frame so this could be done only once if we wanted to.

    return remove_background_colors_from_frame(frame, background_colors)


def find_color_frequency(frame):
    colors = {}
    for i in range(frame.shape[0]):
        for j in range(frame.shape[1]):
            bgr = bgr_to_str(frame[i, j])

            if bgr in colors:
                colors[bgr] += 1
            else:
                colors[bgr] = 0

    return colors


def bgr_to_str(bgr):
    return ''.join(str(i) for i in bgr)


def identify_background_colors(color_frequency):
    background_colors = []
    for key, value in color_frequency.items():
        if value > 2000:  # 2000 was an estimate found after inspecting the color_frequency variable for a couple of frames.
            background_colors.append(key)

    return background_colors


def remove_background_colors_from_frame(frame, background_colors):
    for i in range(frame.shape[0]):
        for j in range(frame.shape[1]):
            bgr = bgr_to_str(frame[i, j])

            if bgr in background_colors:
                for k in range(3):
                    frame[i, j, k] = 255

    return frame


def save_ghost_frame(ghost_frame, name):
    imwrite(f'{ANSWER_TEMP_PATH}/{name}.png', ghost_frame)


def create_answer_video():
    print('making a video out of the ghost frames...')
    os.system(f'ffmpeg -framerate 1 -i {ANSWER_TEMP_PATH}/%01d.png -codec copy -y {ANSWER_VIDEO_FILE} 2> /dev/null')


def remove_answer_temp_path():
    shutil.rmtree(ANSWER_TEMP_PATH)


if __name__ == "__main__":
    main()
 

 

Vous aimez résoudre des énigmes ? Faites-en une carrière