Envio e recebimento de mensagens de texto dentro de imagens com Python
msc2020
Posted on April 29, 2024
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
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.
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)
'''
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:
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
R, G, B
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
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
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.'
'''
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!
Posted on April 29, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.