Envio e recebimento de mensagens de texto dentro de imagens com Python
Tags: python, tutorial, braziliandevs
Published: 2024-04-29 22:51:07

O processo de enviar e receber mensagens de texto dentro de imagens faz parte da área de [Esteganografia](https://pt.wikipedia.org/wiki/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 <u>[`Pillow` (`PIL`)](https://pillow.readthedocs.io/en/stable/)</u> do Python 3:

 ```shell
 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.

![imagem de camaleão](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i8tc0ogxflclcnovhisy.jpeg)

<center><small><small><u>Fonte:</u> <a href="https://it.wikifur.com/wiki/Camaleonte">https://it.wikifur.com/wiki/Camaleonte</a></small></small></center>

---

## Exemplo

Vejamos um exemplo com a biblioteca `Pillow`.

```Python
# 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](https://www.rapidtables.com/web/color/RGB_Color.html) para facilitar na conversão, checamos que a cor do pixel escolhido:

![imagem da cor (184, 215, 95)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fcagw2fl5ghe48ycrpmz.png)

---

## 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{% katex inline %}\le{% endkatex %} R, G, B {% katex inline %}\le{% endkatex %}255 usamos o `putpixel()`.

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

```Python
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`](https://en.wikipedia.org/wiki/233_(number)) tem um lado cabalístico, pois é ao mesmo tempo um [número primo](https://en.wikipedia.org/wiki/Prime_number), [primo de Shopie Germain](https://en.wikipedia.org/wiki/Safe_and_Sophie_Germain_primes), [primo de Srinivasa Ramujan](https://en.wikipedia.org/wiki/Ramanujan_prime) e também de [Fibonacci](https://en.wikipedia.org/wiki/Fibonacci_prime). Números com essas características são de extrema importância para área de criptografia. Por exemplo, o projeto [PrimeGrid](https://www.primegrid.com/) 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:

```Python
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 *`.

```Python
#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.'
'''
```

![img original vs img com mensagem](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tm5sx6mrjesglo73eubb.png)
<center><small><small><u>Sobre a figura:</u> 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.</a></small></small></center>

---

## 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)](https://en.wikipedia.org/wiki/Bit_numbering). 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](https://www.vivaolinux.com.br/artigo/Esteganografia-e-Esteganalise-transmissao-e-deteccao-de-informacoes-ocultas-em-imagens-digitais/?pagina=1) ou [aqui](https://dev.to/vapourisation/steganograhy-part-1-2j73).

<center>👾 👻 🦎 🐉 🧌 🖼️ 🕴️</center>

<br/>
***Esperamos que tenham gostado e agradecemos a leitura!***