Fundamentos da Manipulação de Imagens com a Biblioteca Pillow em Python

A biblioteca Pillow, uma ramificação do clássico Python Imaging Library (PIL), é uma ferramenta essencila para processamento e manipulação de imagens em Python. Seu componente mais fundamental é a classe Image, residente no módulo de mesmo nome. Um objeto Image pode ser instanciado de várias maneiras: carregando uma imagem de um arquivo existente, criando uma nova imagem do zero ou gerando uma imagem a partir de operações sobre outras imagens.

Carregando e Inspecionando Imagens

Para carregar uma imagem existente de um sistema de arquivos, utiliza-se a função Image.open(). Esta função retorna uma instância da classe Image se a operação for bem-sucedida.

from PIL import Image
from pathlib import Path

def carregar_e_detalhar_imagem(caminho_arquivo: Path):
    """
    Carrega uma imagem de um caminho especificado e imprime suas propriedades básicas.
    Retorna o objeto Image ou None em caso de erro.
    """
    try:
        with Image.open(caminho_arquivo) as img:
            print(f"Caminho do arquivo: {caminho_arquivo}")
            print(f"Formato: {img.format}")
            print(f"Dimensões (Largura, Altura): {img.size}")
            print(f"Modo de cor: {img.mode}")
            print(f"Informações adicionais (dicionário): {img.info}")
            return img
    except IOError as e:
        print(f"Erro ao carregar a imagem '{caminho_arquivo}': {e}")
        return None

# Exemplo de uso:
# imagem_exemplo_path = Path('caminho/para/sua_imagem.png')
# minha_imagem = carregar_e_detalhar_imagem(imagem_exemplo_path)
# if minha_imagem:
#     # minha_imagem.show() # Descomente para exibir a imagem graficamente
#     pass

Após carregar a imagem, várias propriedades do objeto Image fornecem metadados importantes:

  • format: Uma string indicando o formato do arquivo (ex: 'PNG', 'JPEG'). Será None se a imagem foi criada e não lida de um arquivo.
  • size: Uma tupla (largura, altura) representando as dimensões da imagem em pixels.
  • mode: Uma string que descreve o tipo e a profundidade dos pixels. Modos comuns incluem 'L' (tons de cinza), 'RGB' (cores verdadeiras), 'CMYK' (imagens para pré-impressão). A documentação oficial da Pillow detalha a lista completa de modos.
  • palette: Disponível apenas para imagens no modo 'P' (paleta), retorna uma instância de ImagePalette.
  • info: Um dicionário que pode conter informações adicionais específicas do arquivo, como metadados EXIF, DPI, etc.

Caso o arquivo especificado não possa ser aberto ou não seja um formato de imagem reconhecido, uma exceção IOError será lançada.

Leitura e Escrita de Imagens

A Pillow possui um suporte extensivo a diversos formatos de imagem. A função Image.open() é inteligente o suficiente para detectar automaticamente o formato do arquivo, tornando desnecessário especificar o tipo ao carregar. Para salvar um objeto Image modificado ou criado, utiliza-se o método save(). O formato de saída é inferido pela extensão do nome do arquivo, mas pode ser explicitamente definido como um argumento do método.

Exemplo: Conversão de Formato de Imagem em Lote

O script a seguir demonstra como converter todas as imagens em um diretório para o formato PNG.

import os
from PIL import Image
from pathlib import Path

def converter_diretorio_para_png(diretorio_entrada: Path, diretorio_saida: Path):
    """
    Converte todas as imagens de um diretório de entrada para PNG no diretório de saída.
    """
    diretorio_saida.mkdir(parents=True, exist_ok=True) # Garante que o diretório de saída exista

    for caminho_arquivo_origem in diretorio_entrada.iterdir():
        if caminho_arquivo_origem.is_file():
            # Cria o nome do arquivo de saída com a extensão .png
            nome_base = caminho_arquivo_origem.stem
            caminho_arquivo_destino = diretorio_saida / f"{nome_base}.png"

            # Evita converter se o arquivo já é PNG
            if caminho_arquivo_origem.suffix.lower() == ".png":
                print(f"'{caminho_arquivo_origem.name}' já é PNG, ignorando.")
                continue

            try:
                with Image.open(caminho_arquivo_origem) as img_obj:
                    img_obj.save(caminho_arquivo_destino, "PNG")
                print(f"Convertido: {caminho_arquivo_origem.name} -> {caminho_arquivo_destino.name}")
            except IOError as e:
                print(f"Não foi possível converter '{caminho_arquivo_origem.name}': {e}")
            except Exception as e:
                print(f"Um erro inesperado ocorreu ao converter '{caminho_arquivo_origem.name}': {e}")

# Exemplo de uso:
# pasta_origem = Path('minhas_imagens_originais')
# pasta_destino = Path('imagens_convertidas_png')
# # Crie alguns arquivos de imagem de teste na pasta_origem antes de executar
# converter_diretorio_para_png(pasta_origem, pasta_destino)

Criação de Miniaturas

A geração de miniaturas (thumbnails) é uma técnica fundamental para aplicações web, galerias de fotos e softwares de visualização de imagens. A Pillow simplifica essa tarefa com o método thumbnail() da classe Image. Este método recebe uma tupla (largura_max, altura_max) e redimensiona a imagem 'in place' (modifica o objeto atual) para que ela se ajuste dentro das dimensões fornecidas, mantendo sua proporção original.

Exemplo: Geração de Miniaturas para um Diretório

from PIL import Image
from pathlib import Path

def gerar_miniaturas_em_lote(diretorio_base: Path, tamanho_miniatura: tuple = (128, 128), subpasta_miniaturas: str = "miniaturas"):
    """
    Gera miniaturas de todas as imagens em um diretório e as salva em uma subpasta.
    """
    diretorio_saida = diretorio_base / subpasta_miniaturas
    diretorio_saida.mkdir(exist_ok=True) # Garante que a pasta de saída existe

    for caminho_img_original in diretorio_base.iterdir():
        if caminho_img_original.is_file() and caminho_img_original.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif']:
            nome_base_arquivo = caminho_img_original.stem
            caminho_miniatura_destino = diretorio_saida / f"{nome_base_arquivo}_thumb.jpg" # Salva como JPEG por padrão

            try:
                with Image.open(caminho_img_original) as img:
                    img.thumbnail(tamanho_miniatura) # Redimensiona a imagem no lugar
                    img.save(caminho_miniatura_destino, "JPEG")
                    print(f"Miniatura criada para '{caminho_img_original.name}' em '{caminho_miniatura_destino.name}'")
            except Exception as e:
                print(f"Não foi possível criar miniatura para '{caminho_img_original.name}': {e}")

# Exemplo de uso:
# pasta_com_fotos = Path('imagens_para_galeria')
# # Crie alguns arquivos de imagem de teste em 'imagens_para_galeria'
# gerar_miniaturas_em_lote(pasta_com_fotos, tamanho_miniatura=(200, 150))

Importante: A Pillow emprega uma estratégia de carregamento "preguiçoso" (lazy loading). Ao abrir um arquivo com Image.open(), apenas os metadados do cabeçalho (formato, modo, dimensões) são lidos. Os dados de pixel da imagem não são carregados na memória até que uma operação que realmente precise deles seja executada (como manipulação de pixels, salvamento, ou uma chamada explícita ao método load()). Isso torna a operação de abertura de arquivos extremamente rápida, independentemente do tamanho da imagem.

Recorte, Colagem e Fusão de Imagens

A classe Image oferece métodos poderosos para manipular regiões específicas de uma imagem, incluindo corte, cópia e colagem de sub-regiões.

Recortando uma Sub-região

O método crop() permite extrair uma porção retangular de uma imagem.

from PIL import Image
from pathlib import Path

def recortar_area_especifica(caminho_imagem_origem: Path, coordenadas_caixa: tuple):
    """
    Carrega uma imagem e recorta uma área definida por uma tupla de coordenadas.
    Retorna a imagem recortada.
    """
    try:
        with Image.open(caminho_imagem_origem) as img_base:
            # As coordenadas da caixa são (esquerda, topo, direita, base)
            # A origem (0,0) está no canto superior esquerdo.
            # Este exemplo recorta um quadrado de 120x120 pixels a partir de (50, 50)
            regiao_extraida = img_base.crop(coordenadas_caixa)
            print(f"Região recortada com dimensões: {regiao_extraida.size}")
            # regiao_extraida.show()
            return regiao_extraida
    except IOError as e:
        print(f"Erro ao carregar ou recortar a imagem '{caminho_imagem_origem}': {e}")
        return None

# Exemplo de uso:
# img_para_recorte_path = Path('caminho/para/minha_foto.jpg')
# caixa_recorte_exemplo = (50, 50, 170, 170) # (left, top, right, bottom)
# minha_regiao = recortar_area_especifica(img_para_recorte_path, caixa_recorte_exemplo)
# if minha_regiao:
#     # Salva a região recortada para verificação
#     minha_regiao.save(img_para_recorte_path.parent / "regiao_recortada.png")

Processando e Colando Sub-imagens

Uma vez que uma região é recortada, ela pode ser manipulada (por exemplo, girada ou espelhada) e então colada de volta na imagem original ou em outra imagem usando o método paste().

from PIL import Image
from pathlib import Path

def deslocar_img_horizontal(img_obj: Image.Image, deslocamento_pixels: int):
    """
    Desloca o conteúdo de uma imagem horizontalmente (efeito de rolagem).
    A modificação é feita 'in-place' no objeto Image fornecido.
    """
    largura_img, altura_img = img_obj.size
    
    # Ajusta o deslocamento para estar dentro dos limites da largura da imagem
    deslocamento_efetivo = deslocamento_pixels % largura_img
    if deslocamento_efetivo == 0:
        return img_obj # Nenhum deslocamento, retorna a imagem original

    # Divide a imagem em duas partes com base no deslocamento
    parte_esquerda = img_obj.crop((0, 0, deslocamento_efetivo, altura_img))
    parte_direita = img_obj.crop((deslocamento_efetivo, 0, largura_img, altura_img))

    # Força o carregamento dos dados para evitar problemas com 'lazy loading'
    # se a imagem_obj original for modificada antes do paste.
    parte_esquerda.load() 
    parte_direita.load()

    # Cola a parte da direita na posição inicial (0,0)
    img_obj.paste(parte_direita, (0, 0))
    # Cola a parte da esquerda na posição final (largura - deslocamento_efetivo, 0)
    img_obj.paste(parte_esquerda, (largura_img - deslocamento_efetivo, 0))

    return img_obj

# Exemplo de uso:
# caminho_imagem_teste = Path('caminho/para/sua_foto_grande.jpg')
# try:
#     imagem_original = Image.open(caminho_imagem_teste)
#     print(f"Dimensões originais: {imagem_original.size}")
#     
#     imagem_deslocada = deslocar_img_horizontal(imagem_original, 150) # Desloca 150 pixels
#     caminho_salvar_deslocada = caminho_imagem_teste.parent / f"{caminho_imagem_teste.stem}_deslocada.png"
#     imagem_deslocada.save(caminho_salvar_deslocada, "PNG")
#     print(f"Imagem deslocada salva em: {caminho_salvar_deslocada}")
#     # imagem_deslocada.show()
# except IOError as e:
#     print(f"Erro ao carregar ou processar imagem para deslocamento: {e}")

Separação e Fusão de Canais de Cores

Para imagens multicanais (por exemplo, RGB), pode ser necessário processar cada canal de cor individualmente. A Pillow oferece os métodos split() e merge() para essa finalidade:

  • split(): Retorna uma tupla de objetos Image, onde cada objeto representa um canal separado da imagem original. Para imagens de canal único, retorna a própria imagem.
  • merge(mode, bands): Cria uma nova imagem a partir de um mode especificado e uma tupla de objetos Image que atuam como canais. Todos os canais devem ter as mesmas dimensões.
from PIL import Image
from pathlib import Path

def manipular_canais_de_cor(caminho_imagem_rgb: Path):
    """
    Demonstra a separação e fusão de canais de cor em uma imagem RGB.
    """
    try:
        with Image.open(caminho_imagem_rgb) as img_colorida:
            if img_colorida.mode != 'RGB':
                print(f"A imagem '{caminho_imagem_rgb.name}' não está no modo RGB, convertendo...")
                img_colorida = img_colorida.convert('RGB')

            # Separa a imagem nos seus canais R, G e B
            canal_r, canal_g, canal_b = img_colorida.split()
            print("Canais R, G, B separados.")
            
            # Exemplo de manipulação: inverter o canal vermelho
            # N.B.: Para manipulações complexas, usar ImageOps ou aplicar funções a pixels é mais comum.
            # Aqui, apenas demonstração de reordenação/combinação.

            # Recombina os canais na ordem original (ou em uma nova ordem, ex: (g, r, b))
            img_recombinada = Image.merge('RGB', (canal_r, canal_g, canal_b))
            
            caminho_salvar_recombinada = caminho_imagem_rgb.parent / f"{caminho_imagem_rgb.stem}_recombinada.png"
            img_recombinada.save(caminho_salvar_recombinada, "PNG")
            print(f"Imagem com canais recombinados salva em: {caminho_salvar_recombinada}")
            # img_recombinada.show()

    except IOError as e:
        print(f"Erro ao processar imagem para canais de cor: {e}")

# Exemplo de uso:
# imagem_cores_path = Path('caminho/para/minha_foto_colorida.jpg')
# manipular_canais_de_cor(imagem_cores_path)

Transformações Geométricas

A classe Image inclui métodos para executar transformações geométricas básicas, como redimensionamento e rotação.

Redimensionamento e Rotação Simples

O método resize() aceita uma tupla (largura, altura) para definir as novas dimensões da imagem, retornando uma nova imagem redimensionada. O método rotate() gira a imagem por um ângulo especificado (em graus, sentido horário).

from PIL import Image
from pathlib import Path

def aplicar_redimensionar_e_rotacionar(caminho_imagem: Path):
    """
    Demonstra redimensionamento e rotação de uma imagem.
    """
    try:
        with Image.open(caminho_imagem) as img_original:
            print(f"Dimensões originais: {img_original.size}")

            # Redimensiona a imagem para 200x200 pixels
            img_redimensionada = img_original.resize((200, 200))
            print(f"Dimensões após redimensionamento: {img_redimensionada.size}")
            # img_redimensionada.show()

            # Rotaciona a imagem redimensionada em 45 graus.
            # 'expand=True' ajusta o tamanho da nova imagem para conter todo o conteúdo rotacionado.
            img_rotacionada = img_redimensionada.rotate(45, expand=True)
            print(f"Dimensões após rotação (expand=True): {img_rotacionada.size}")
            # img_rotacionada.show()
            
            # Salva as imagens transformadas
            img_redimensionada.save(caminho_imagem.parent / f"{caminho_imagem.stem}_redimensionada.png")
            img_rotacionada.save(caminho_imagem.parent / f"{caminho_imagem.stem}_rotacionada.png")

    except IOError as e:
        print(f"Erro ao processar imagem para transformações: {e}")

# Exemplo de uso:
# imagem_para_transf_path = Path('caminho/para/minha_imagem.jpg')
# aplicar_redimensionar_e_rotacionar(imagem_para_transf_path)

Operações de Transposição Eficientes

O método transpose() oferece um conjunto de transformações de espelhamento e rotação que são muito eficientes:

  • Image.FLIP_LEFT_RIGHT: Espelha a imagem horizontalmente.
  • Image.FLIP_TOP_BOTTOM: Espelha a imagem verticalmente.
  • Image.ROTATE_90: Gira a imagem 90 graus no sentido horário.
  • Image.ROTATE_180: Gira a imagem 180 graus no sentido horário.
  • Image.ROTATE_270: Gira a imagem 270 graus no sentido horário.

Para transformações mais complexas ou personalizadas, o método transform() pode ser utilizado.

Módulo ImageDraw: Desenho 2D em Imagens

O módulo ImageDraw estende as capacidades da Pillow, permitindo que se desenhe gráficos 2D diretamente sobre objetos Image. Com ele, é possível tanto criar novas imagens com conteúdo gráfico quanto adicionar elementos a imagens existentes.

Instanciando a Classe Draw

Para começar a deesnhar, primeiro é necessário criar uma instância da classe Draw, passando o objeto Image no qual se deseja desenhar:

from PIL import Image, ImageDraw

# Exemplo: Abrir uma imagem existente ou criar uma nova
# img_para_desenho = Image.open('caminho/para/imagem.png')
# ou
# img_para_desenho = Image.new('RGB', (600, 400), color = 'white') # Cria uma imagem branca 600x400

# contexto_desenho = ImageDraw.Draw(img_para_desenho)

Funções Básicas de Desenho

A classe Draw oferece uma variedade de métodos para desenhar formas geométricas e texto:

  • point(xy, fill): Desenha pontos.
  • line(xy, fill, width): Desenha uma ou múltiplas linhas. xy pode ser uma lista de tuplas [(x1, y1), (x2, y2), ...] ou uma lista de coordenadas planas [x1, y1, x2, y2, ...].
  • rectangle(xy, fill, outline, width): Desenha um retângulo. xy é uma tupla (x1, y1, x2, y2).
  • ellipse(xy, fill, outline, width): Desenha uma elipse (ou um círculo). xy é a caixa delimitadora (x1, y1, x2, y2).
  • polygon(xy, fill, outline): Desenha um polígono.
  • text(xy, text, fill, font, ...): Desenha texto.
  • textbbox(xy, text, font, ...): Calcula a caixa delimitadora do texto.

As opções como fill (cor de preenchimento) e outline (cor da borda) aceitam tuplas RGB (R, G, B) ou nomes de cores (ex: "red"). width define a espessura da linha em pixels.

Exemplo: Desenho de Linhas em uma Imagem

from PIL import Image, ImageDraw
from pathlib import Path

def desenhar_elementos_graficos(caminho_imagem_base: Path, caminho_salvar_resultado: Path):
    """
    Demonstra o desenho de múltiplas linhas sobre uma imagem existente.
    """
    try:
        with Image.open(caminho_imagem_base) as img_fundo:
            contexto_canvas = ImageDraw.Draw(img_fundo)

            largura_img, altura_img = img_fundo.size

            # Desenha uma linha horizontal no terço superior da imagem
            y_terco_superior = int(altura_img / 3)
            contexto_canvas.line([(0, y_terco_superior), (largura_img, y_terco_superior)],
                                 fill=(255, 165, 0), width=4) # Laranja, espessura 4px

            # Desenha uma forma em 'V' invertido na parte inferior da imagem
            # Pontos: canto inferior esquerdo -> centro inferior -> canto inferior direito
            ponto_inf_esq = (0, altura_img)
            ponto_central_baixo = (largura_img // 2, altura_img - 50) # 50px acima da base
            ponto_inf_dir = (largura_img, altura_img)

            contexto_canvas.line([ponto_inf_esq, ponto_central_baixo, ponto_inf_dir],
                                 fill=(0, 0, 255), width=5) # Azul, espessura 5px

            # Salva a imagem com os desenhos
            img_fundo.save(caminho_salvar_resultado, "JPEG")
            print(f"Imagem com gráficos desenhados salva em: {caminho_salvar_resultado}")
            # img_fundo.show()

    except IOError as e:
        print(f"Erro ao processar imagem para desenho: {e}")

# Exemplo de uso:
# pasta_ativos = Path('assets_de_imagem')
# imagem_original_path = pasta_ativos / 'imagem_base.jpg'
# imagem_desenhada_path = pasta_ativos / 'imagem_com_elementos.jpg'
# # Certifique-se de que 'imagem_base.jpg' existe na pasta 'assets_de_imagem'
# desenhar_elementos_graficos(imagem_original_path, imagem_desenhada_path)

Tags: Pillow Python ProcessamentoDeImagens ManipulacaoDeImagens Image

Publicado em 10-16 13:57