Envio e recebimento de mensagens de texto dentro de imagens com Python

msc2020

msc2020

Posted on April 29, 2024

Envio e recebimento de mensagens de texto dentro de imagens com Python

O processo de enviar e receber mensagens de texto dentro de imagens faz parte da área de Esteganografia. No post de hoje, mostramos uma forma simples de como fazer isso utilizando a linguagem Python. ☕


Pré-requisitos

Para fazer este tutorial é necessário instalar a biblioteca Pillow (PIL) do Python 3:

 pip install pillow
Enter fullscreen mode Exit fullscreen mode

Representação de um pixel RGB

Uma imagem digital é formada por pixels, sendo essas sua menor unidade. Por sua vez, cada pixel de imagens usuais como .JPG, .PNG, .JPEG, está associado a três valores inteiros (int) que representam a quantidade de cores R (red, vermelho), G (green, verde) e B (blue, azul). Os valores das cores R, G e B variam entre 0 e 255. A combinação desses valores irá formar uma ampla gama de cores do sistema RGB.

Algumas imagens contam com uma componente extra, além dos canais de cores R, G e B, chamada A (alpha) que controla a transparência da imagem. Esse valor varia de 0 a 1 (ou de 0 a 255, como no Pillow). Quanto mais próximo de 0, mais transparente a imagem ficará.

Neste post usaremos a imagem abaixo para realizar os testes.

imagem de camaleão

Fonte: https://it.wikifur.com/wiki/Camaleonte

Exemplo

Vejamos um exemplo com a biblioteca Pillow.

# exemplo.py

from PIL import Image

# path para imagem de entrada
filename_path = './images/img_camaleao.jpeg' 
# download da imagem: https://it.wikifur.com/wiki/Camaleonte

# carrega imagem como um objeto Image
img = Image.open(filename_path)

# print do formato e sistema de cores
print(f'Formato: {img.format}')
print(f'Cores: {img.getbands()}')
print(f'Tamanho: {img.size}')
x, y = 400, 100
print(f'Valor das cores do pixel na posição \
(x, y) = ({x}, {y}): {img.getpixel( (x, y))}')

# exibe imagem
img.show()

'''
saída esperada no terminal:

Formato: JPEG
Cores: ('R', 'G', 'B')
Tamanho: (800, 500)
Valor das cores do pixel na posição (x, y) = (400, 100): (184, 215, 95)
'''
Enter fullscreen mode Exit fullscreen mode

No exemplo considerado, o pixel selecionado faz parte do camaleão verde, pois a componente G tem o maior valor na tripla ordenada (R, G, B) = (184, 215, 95). Usando um site para facilitar na conversão, checamos que a cor do pixel escolhido:

imagem da cor (184, 215, 95)


Inserindo uma mensagem de texto em uma imagem RGB

Assim como o Pillow possui um método para coletar as informações sobre um determinado pixel de uma imagem, getpixel(), também há um para inserir um pixel. Para inserir um pixel com cores (R, G, B), onde 0 \le R, G, B \le 255 usamos o putpixel().

O código a seguir insere uma mensagem numa imagem com cores RGB:

def insert_msg(img_original, msg):
    '''
    Input:
      . img_original: imagem com cores em RGB
      . msg: uma mensagem em forma de string
    Output:
      . img_with_msg: imagem com a mensagem introduzida em alguns pixels
    '''

    img_with_msg = img_original.copy()

    x_cte = img_with_msg.size[0] - 1

    # codifica mensagem de string para números inteiros
    msg_encoded_bytes = msg.encode(encoding='utf-8', errors='strict')
    msg_encoded = str(int.from_bytes(bytes=msg_encoded_bytes, byteorder='little'))

    # insere mensagem no canal de cor R da imagem
    for j, n in enumerate(msg_encoded):
        rgb_pixel = list(img.getpixel((x_cte, j)))
        red_color = rgb_pixel[0] # (r, g, b)[0]
        try:
            if red_color < 255-9:
                r = red_color % 10
                rgb_pixel[0] =  red_color - r + int(n)
            else:                
                rgb_pixel[0] =  int(n)

            img_with_msg.putpixel((x_cte, j), tuple(rgb_pixel))
        except Exception as e:
            print(e)            
            img_with_msg.putpixel((x_cte, j), tuple(rgb_pixel))

    # adiciona pixels no fim como flag
    flag_pixel = (233, 233, 233)
    for j in range(5):
        k = len(msg_encoded) + j
        rgb_pixel = img.getpixel((x_cte, k))
        try: # tenta retorna um flag_pixel
            img_with_msg.putpixel((x_cte, k), flag_pixel)
        except: # se der exceção, retorna o pixel original
            img_with_msg.putpixel((x_cte, j), rgb_pixel)

    return img_with_msg, msg_encoded
Enter fullscreen mode Exit fullscreen mode

Estratégia usada: Para inserir a mensagem na imagem, primeiramente codificamos ela como uma lista de inteiros e depois inserimos cada valor inteiro no canal de cor vermelho R, mantendo fixo um valor para coordenada x (x = x_cte).

As inserções alteram os pixels da imagem original. Por exemplo, suponha que o valor do pixel original escolhido for, digamos, (23, 127, 53) e vamos inserir o número 7 na lista de inteiros que codificam a mensagem. Então, o pixel alterado é (27, 127, 53).

Essa estratégia de inserir, foi definida na etapa r = red_color % 10. Caso haja valores maiores do que 255, que é o valor máximo permitido, tratamos com o try/except.

Nota: Essa forma de inserir pode não ser a mais eficiente, nem tem essa intenção. O objetivo do post é apresentar uma, dentre tantas, maneira de fazer a tarefa desejada.

Após inserir a mensagem codificada na imagem, finalizamos a lista de pixels alterados com uma flag. A flag corresponde em alterar os 5 próximos pixels, após o último que foi necessário, para o valor de (233, 233, 233). Isso nos ajudará no momento de recuperar a mensagem enviada.

Curiosidade: O número 233 tem um lado cabalístico, pois é ao mesmo tempo um número primo, primo de Shopie Germain, primo de Srinivasa Ramujan e também de Fibonacci. Números com essas características são de extrema importância para área de criptografia. Por exemplo, o projeto PrimeGrid investiga números primos como esses desde 2005.


Extraindo a mensagem de texto da imagem

Para extrair a mensagem presente na imagem, usamos essa função:

def exctract_msg(img_with_msg):
    '''
    Input:
      . img_with_msg: imagem com uma mensagem inserir pela função `insert_msg()`
    Output:
     . msg_decoded_str: mensagem na forma de string
    '''

    img_input = img_with_msg.copy()
    x_cte = img_input.size[0] - 1

    # pega posição da flag
    is_flag = 0
    flag_pixel = (233, 233, 233)
    list_pixels = []
    for j in range(img_input.size[1] - 1):
        rgb_pixel = img_input.getpixel((x_cte, j))
        if rgb_pixel == flag_pixel:
            is_flag += 1
            if is_flag == 5:
                j_end = j

    # cria lista com os números inteiros correspondentes a mensagem codificada como inteiros
    msg_encoded_int = []
    for j in range(j_end-4):
        rgb_pixel = list(img_input.getpixel((x_cte, j)))
        red_color = rgb_pixel[0] # (r, g, b)
        r = str(red_color % 10)
        msg_encoded_int.append(r)
        list_pixels.append(rgb_pixel)

    # decodifica, passando a lista de inteiros para uma string com a mensagem escolhida
    msg_encoded_int = ''.join(msg_encoded_int)
    msg_encoded_int = int(msg_encoded_int)
    msg_decoded_bytes = msg_encoded_int.to_bytes((msg_encoded_int.bit_length() + 7) // 8, 'little')
    msg_decoded_str = msg_decoded_bytes.decode('utf-8')

    return msg_decoded_str
Enter fullscreen mode Exit fullscreen mode

Resumidamente, para extrair o texto da imagem, basta seguir o processo contrário do que foi feito em insert_msg(). Ou seja, capturar a lista de números inteiros que foram inseridos nos pixels alterados e decodificar essa lista para uma string que será a mensagem original.


Testes

Para testar as funções criadas, iremos considerar a imagem do camaleão já mostrada.

O seguinte código usa as duas funções, para inserção e posterior extração da mensagem. Para evitar repetir o código dessas funções, inserimos ambas, insert_msg() e exctract_msg (), no utils.py e importamos seu conteúdo com from utils import *.

#insere_extrai_msg_em_imagem.py

from PIL import Image
from utils import *

# caminho para imagem JPEG
img_path = './images/img_camaleao.jpeg' # https://it.wikifur.com/wiki/Camaleonte

# mensagem escolhida
msg_input = 'Há mais força no perdão do que na ofensa, há mais força no reparo do que no erro. Raduan Nassar.'

# carrega imagem como um objeto Image
img = Image.open(img_path)

# descomentar a linha abaixo para exibir imagem original
# img.show(title='Imagem original')

# insere mensagem na imagem
img_encoded = insert_msg(img, msg_input)

# descomentar para exibir imagem com a mensagem inserida
# img_encoded.show(title='Imagem com mensagem')

# extrai mensagem de imagem
msg_decoded = exctract_msg(img_encoded)
print(msg_decoded)

'''
saída esperada:

'Há mais força no perdão do que na ofensa, há mais força no reparo do que no erro. Raduan Nassar.'
'''
Enter fullscreen mode Exit fullscreen mode

img original vs img com mensagem

Sobre a figura: A imagem a esquerda é a original e a direita possui a mensagem inserida. Se dermos uma 'zoom in' na imagem a direita notaremos uma sequência de pixels brancos em um pedaço da borda.

Outras formas de inserir e extrair mensagens em imagem

Há maneiras mais eficientes de se inserir e extrair mensagens em imagens digitais. Uma forma muito usada é considerar a codificação binária da mensagem e dos pixels da imagem e alterar os bits menos significativos da imagem (LSB). Dessa maneira, as mensagens transmitidas podem ser um tanto longas e a chance das mudanças na imagem serem perceptíveis a olho nu é baixa. Um bom material sobre o assunto pode ser encontrado neste link ou aqui.

👾 👻 🦎 🐉 🧌 🖼️ 🕴️



Esperamos que tenham gostado e agradecemos a leitura!

💖 💪 🙅 🚩
msc2020
msc2020

Posted on April 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related