Se você gosta de tutoriais sobre Streamlit, recomendo que você confira o meu tutorial anterior
📸 Criando Filtros para Webcam com Streamlit e OpenCV.


Esta semana decidi criar um app que usa o GPT-4 Vision para gerar descrições de imagens com base em um prompt do usuário.

O app permite que o usuário faça upload de várias imagens e gere descrições para todas elas de uma vez. Como o usuário pode escrever o prompt, ele pode adaptar a saída do modelo às suas necessidades. Por exemplo, o usuário pode pedir ao modelo para criar:

  • descrições de produtos
  • legendas para postagens em redes sociais
  • gerar texto para tags alt de imagens
  • extrair o texto das imagens

Uma vez que as descrições são geradas, o usuário pode baixá-las juntamente com os nomes das imagens em um arquivo CSV.

Você pode encontrar o código completo deste app no final ⬇️ deste post ou no meu GitHub.

Bora construir o app!

Para criar este app, usei o seguinte arquivo requirements.txt.

pandas==2.2.0
streamlit==1.31.0
openai==1.12.0
watchdog==4.0.0

Para instalar os módulos em seu ambiente, você pode usar:

pip install -r requirements.txt

Agora, vamos para a implementação. Ela está dividida em três etapas:

Configurar o uploader de imagens

O primeiro passo é criar um app Streamlit que permita ao usuário fazer upload de imagens. Para fazer isso, podemos usar o método st.file_uploader.

Para tornar o app um pouco mais bonito, também defini um título para a página, um ícone, o layout e uma barra lateral usando o método st.set_page_config.

Para colocar o uploader de arquivos dentro da barra lateral, nós o colocamos dentro do bloco with st.sidebar.

import streamlit as st


st.set_page_config(
    page_title="Image to Text",
    page_icon="📝",
    layout="wide",
    initial_sidebar_state="expanded",
)

st.title("AI Image Description Generator 🤖✍️")

with st.sidebar:
    st.title("Upload Your Images")
    st.session_state.images = st.file_uploader(label=" ", accept_multiple_files=True)

A imagem abaixo mostra o layout do app com a barra lateral e o uploader de arquivos. As imagens enviadas pelo usuário são armazenadas na variável st.session_state.images.

App Streamlit para fazer upload de imagens, com um arquivo 'duck.png' listado. Layout do app com a barra lateral e o uploader de arquivos.

Renderizando as imagens em uma tabela

Agora que temos as imagens no lado do servidor, o próximo passo é exibir as imagens em uma tabela, juntamente com seus nomes e descrições que serão preenchidas pelo modelo.

Vamos fazer isso em 3 passos:

Passo 1: Converter as imagens em strings base64

Quando um usuário faz upload de uma imagem, o Streamlit armazena a imagem na memória como um objeto BytesIO. Para exibir as imagens na tabela, precisamos converter os objetos BytesIO em strings base64 (fonte). Para essa conversão, usaremos a função abaixo.

import base64

def to_base64(uploaded_file):
    file_buffer = uploaded_file.read()
    b64 = base64.b64encode(file_buffer).decode()
    return f"data:image/png;base64,{b64}"

Passo 2: Armazenar as imagens em um DataFrame

Antes de renderizar as imagens em uma tabela, precisamos armazenar as imagens em um DataFrame. Para isso, criaremos uma função para gerar um DataFrame contendo as colunas image_id, image (a string codificada em base64), name e description.

import pandas as pd

def generate_df():
    st.session_state.df = pd.DataFrame(
        {
            "image_id": [img.file_id for img in st.session_state.images],
            "image": [to_base64(img) for img in st.session_state.images],
            "name": [img.name for img in st.session_state.images],
            "description": [""] * len(st.session_state.images),
        }
    )

O DataFrame gerado será armazenado na variável st.session_state.df. Assim, poderemos acessá-lo em outras partes do app posteriormente.

Passo 3: Renderizar o DataFrame como uma tabela

O último passo é renderizar as imagens em uma tabela usando o widget st.data_editor. Para tornar mais simples a geração da tabela em diferentes situações, criaremos uma função chamada render_df.

def render_df():
    st.data_editor(
        st.session_state.df,
        column_config={
            "image": st.column_config.ImageColumn(
                "Preview Image", help="Image preview", width=100
            ),
            "name": st.column_config.Column("Name", help="Image name", width=200),
            "description": st.column_config.Column(
                "Description", help="Image description", width=800
            ),
        },
        hide_index=True,
        height=500,
        column_order=["image", "name", "description"],
        use_container_width=True,
    )

if st.session_state.images:
    generate_df()
    render_df()

O widget st.data_editor espera alguns parâmetros. A maioria deles são autoexplicativos, mas gostaria de destacar o parâmetro column_config.

O parâmetro column_config nos permite personalizar as colunas da tabela. Por exemplo, estamos usando o st.column_config.ImageColumn para exibir as imagens. Você pode encontrar mais informações sobre os diferentes tipos de colunas aqui.

O parâmetro column_order nos permite não apenas definir a ordem das colunas, mas também ocultar aquelas colunas que não queremos. Neste caso, estamos ocultando a coluna image_id.

A última parte do trecho de código é responsável por verificar se já foi feito o upload de alguma image, se sim, gerar o DataFrame e renderizá-lo como uma tabela.

Agora, vamos fazer upload de algumas imagens e ver o resultado.

Um app Streamlit para fazer upload de imagens e exibi-las em uma tabela. Exibindo em uma tabela as imagens enviadas pelo usuário.

Gerar as descrições das imagens usando o GPT-4 Vision

Para a o próximo passo, usaremos o modelo OpenAI GPT-4 Vision para gerar as descrições das imagens.

Uma API do OpenAI é necessária para usar o modelo. Você pode obter uma depois de fazer um cadastro no site da OpenAI. Para criar a chave da API, vá para este link. Depois de obter a chave, defina-a como uma variável de ambiente chamada OPENAI_API_KEY.

Para consumir a API do OpenAI e gerar as descrições, criaremos duas funções: generate_description e update_df.

generate_description()

Esta função recebe uma imagem como parâmetro e retorna a descrição gerada pelo modelo. O prompt é passado para a chamada da API via a variável st.session_state.text_prompt (veremos mais adiante). Aqui, definimos o max_tokens como 50, mas você pode alterá-lo de acordo com suas necessidades.

def generate_description(image_base64):
    response = client.chat.completions.create(
        model="gpt-4-vision-preview",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": st.session_state.text_prompt},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": image_base64,
                        },
                    },
                ],
            }
        ],
        max_tokens=50,
    )
    return response.choices[0].message.content

update_df()

A função update_df usa o método apply do pandas para aplicar a função generate_description a cada linha do DataFrame.

O método apply passa a coluna image como parâmetro para a função generate_description e armazena o resultado na coluna description.

def update_df():
    st.session_state.df["description"] = st.session_state.df["image"].apply(generate_description)

Com essas duas funções, já é possível gerar as descrições das imagens. O último passo é criar um botão para acionar a função update_df, e outro botão para baixar as descrições em um arquivo CSV.

Adicionando os botões de ação e o campo de prompt

O último passo é adicionar os botões de ação e o campo de prompt ao app. Faremos isso modificando a seção que verifica se as imagens foram enviadas (a que vem depois da seção render_df 😉).

if st.session_state.images:
    generate_df()

    st.text_input("Prompt", value="What's in this image?", key="text_prompt")

    col1, col2 = st.columns([1, 1])

    with col1:
        if st.button("Generate Image Descriptions", use_container_width=True):
            update_df()
    
    with col2:    
        st.download_button(
                "Download descriptions as CSV",
                st.session_state.df.drop(['image', "image_id"], 
                                         axis=1).to_csv(index=False),
                "descriptions.csv",
                "text/csv",
                use_container_width=True
        )

    render_df()

Para o campo de prompt, usaremos o widget st.text_input. O prompt será armazenado na variável st.session_state.text_prompt.

Para fazer os botões aparecerem lado a lado, usaremos st.columns.

Na primeira coluna, colocaremos o botão para gerar as descrições. Quando o botão é clicado, a função update_df será chamada.

Na segunda coluna, colocaremos o botão para baixar as descrições em um arquivo CSV. O botão usará o widget st.download_button. Para tornar o arquivo CSV mais amigável, excluímos as colunas image e image_id do DataFrame antes de baixá-lo.

Finalmente, chamaremos a função render_df para renderizar o DataFrame na tabela. O DataFrame sempre será renderizado se houver imagens enviadas.

Neste ponto, o app está completo. Mas podemos fazer melhor! Vamos ajustar o código para fazer menos chamadas à API e economizar alguns dinheiros.

A versão final do app Streamlit para gerar descrições de imagens usando o GPT-4 Vision.

Seção Bônus: Economizando Custos 💰

A implementação atual chama a API do OpenAI para cada imagem, mesmo que sua descrição já tenha sido gerada. Para melhorar isso, precisamos alterar dois lugares no código.

Primeiro, precisamos modificar a função generate_df para que, quando o usuário fizer upload de mais imagens, as descrições das imagens que já foram geradas não sejam perdidas. Fazemos isso com um join entre o DataFrame atual, contendo todas as imagens, e o DataFrame anterior. Se a imagem já estiver no DataFrame, mantemos a descrição. Se for uma nova imagem, a adicionamos ao DataFrame.

def generate_df():
    current_df = pd.DataFrame(
        {
            "image_id": [img.file_id for img in st.session_state.images],
            "image": [to_base64(img) for img in st.session_state.images],
            "name": [img.name for img in st.session_state.images],
            "description": [""] * len(st.session_state.images),
        }
    )

    if "df" not in st.session_state:
        st.session_state.df = current_df
        return

    new_df = pd.merge(current_df, st.session_state.df, on=["image_id"], how="outer", indicator=True)
    new_df = new_df[new_df["_merge"] != "right_only"].drop(columns=["_merge", "name_y", "image_y", "description_x"])
    new_df = new_df.rename(columns={"name_x": "name", "image_x": "image", "description_y": "description"})
    new_df["description"] = new_df["description"].fillna("")

    st.session_state.df = new_df

Em seguida, precisamos modificar a função update_df para chamar a função generate_description apenas para as imagens que ainda não possuem descrição.

def update_df():
    indexes = st.session_state.df[st.session_state.df["description"] == ""].index
    for idx in indexes:
        description = generate_description(st.session_state.df.loc[idx, "image"])
        st.session_state.df.loc[idx, "description"] = description

Muito melhor! Agora o app só chamará a API do OpenAI para as imagens que ainda não têm descrição. Isso economizará alguns custos e tornará o app mais eficiente 🏎️.

Código Completo

Expanda para ver a implementação completa...


Possíveis Melhorias

  • Adicionar um spinner de carregamento enquanto as descrições estão sendo geradas.
  • Permitir a geração de descrições para imagens a partir de uma URL.
  • Permitir que as descrições sejam regeradas quando o prompt é alterado.
  • Permitir que o usuário selecione o número máximo de tokens a serem gerados.
  • Redimensionar as imagens para um tamanho menor para tornar as chamadas à API mais rápidas.

Conclusão

Neste post, aprendemos como criar um app Streamlit para gerar descrições de imagens usando o modelo OpenAI GPT-4 Vision. Também aprendemos como otimizar o app para economizar custos e torná-lo mais eficiente.

Espero que você tenha gostado deste post e aprendido algo novo. Se tiver alguma dúvida ou sugestão, deixe um comentário abaixo.

Até a próxima! 👋