Une image cliquable sous Streamlit

Implémenter un composant sous Streamlit pour une demo plus originale, ici l'exemple d'une image cliquable.

Pour qui veut mettre en place des démo de ses modèles en action, Streamlit est un bon choix. Simple et rapide à mettre en place, ses fonctionnalités de base peuvent satisfaire la plupart des projets. S’il pouvait subsister une certaine frustration pour ceux qui désiraient développer des interfaces plus originales, désormais, avec la possibilité offerte d'implémenter ses propres composants, Streamlit comble aujourd’hui ce vide.

L’objet de cet article est d’illustrer la marche à suivre pour développer un composant de type image cliquable. L’objectif n’est pas de produire un composant complexe mais au contraire de garder les choses simples pour mettre l’accent sur les différentes étapes à respecter.

Import du template

Streamlit met à notre disposition un template de composant qui prend la forme d’un simple slider. Nous allons récupérer celui-ci et l’adapter à notre convenance.

Rendez-vous dans le repo github : component-template
Téléchargez le code, vous devriez récupérer un zip, décompresser celui-ci en local sur votre disque.
Nous allons renommer les répertoires et autres mentions spécifiées dans le template afin de l’adapter à notre composant, que nous appellerons “clickImage” :

Renommer tout d’abord le répertoire “my_component” situé sous “template” en “clickImage”.
Dans le répertoire template, au niveau supérieur, se trouve un fichier setup.py, ouvrez ce fichier et remplacer le nom du package “streamlit-custom-component” par “streamlit-clickImage”.

Implémentation de la partie frontend

Nous allons maintenant attaquer la partie frontend de notre composant, et nous charger notamment du script React.

Rendez-vous tout d’abord dans le répertoire template / frontend / src puis ouvrez le fichier “index.tsx”. Il y a dans ce fichier 3 references a MyComponent qui va nous falloir remplacer par ClickImage, comme ceci :


import React from "react"
import ReactDOM from "react-dom"
import ClickImage from "./ClickImage"

ReactDOM.render(
   <React.StrictMode>
     <ClickImage/>
   </React.StrictMode>,
  document.getElementById("root")
)

Dans ce même répertoire, renommer le fichier “MyComponent.tsx” en “ClickImage.tsx”. Ouvrez à présent ce fichier, c’est dans celui-ci que se trouve notre classe principale.

Renommer la classe en ClickImage :


class ClickImage extends StreamlitComponentBase<State>

Nous allons gérer 2 variables d'état sur notre composant, soient x et y pour les coordonnées de la souris. Au-dessus de la définition de la classe déclarez ces 2 variables dans l’interface State :


interface State {
  x: number,
  y: number
}

De même initialiser ces variables en début de classe :


class ClickImage extends StreamlitComponentBase<State> {
  public state = { x: 0, y: 0 }
...

Nous n'aurons besoin dans notre classe ClickImage que de deux méthodes. La premiere est la méthode render bien entendu, qui constitue la méthode par laquelle le composant est restitué à l'écran. Dans notre cas nous allons tout remplacer par ceci :


public render = (): ReactNode => {
return (
 <div>
   <img src={require('./34068.jpg')} alt="mon image" 
     style={{ width: "100%" }}
     onClick={this.getMousePosition.bind(this)}/>
 </div>
)
}

Comme vous le voyez, c’est relativement simple, nous affichons dans un div notre image en précisant une largeur de 100% et en spécifiant une méthode à exécuter en cas de clic.
Pour information nous avons pour l’exemple déposer une image (ici 34068.jpg) dans le répertoire frontend / src.
Vous l’aurez compris, notre seconde méthode sera donc getMousePosition :


private getMousePosition(e: any) {
let currentTargetRect = e.currentTarget.getBoundingClientRect();
const event_offsetX = e.pageX - currentTargetRect.left,
      event_offsetY = e.pageY - currentTargetRect.top;

this.setState({ x: event_offsetX, y: event_offsetY },
 () => Streamlit.setComponentValue([this.state.x, this.state.y]));
}

Ici nous calculons la position relative du curseur de la souris par rapport à l'image et non par rapport à l'écran, c’est pourquoi nous récupérons dans une variable currentTargetRect la position de notre image. En fin de méthode, nous mettons à jour nos variables d'état via la méthode setComponentValue.

Voici le code complet de notre composant (fichier ClickImage.tsx) :


import {
  Streamlit,
  StreamlitComponentBase,
  withStreamlitConnection,
} from "streamlit-component-lib"
import React, { ReactNode } from "react"

interface State {
  x: number,
  y: number
}

/**
 * This is a React-based component template. The `render()` function is called
 * automatically when your component should be re-rendered.
 */
class ClickImage extends StreamlitComponentBase<State> {
  public state = { x: 0, y: 0}

  public render = (): ReactNode => {
    return (
     <div>
       <img src={require('./34068.jpg')} alt="clickImage" 
         style={{ width: "100%" }}
         onClick={this.getMousePosition.bind(this)}/>
     </div>
    )
  }

  private getMousePosition(e: any) {
    let currentTargetRect = e.currentTarget.getBoundingClientRect();
    const event_offsetX = e.pageX - currentTargetRect.left,
          event_offsetY = e.pageY - currentTargetRect.top;

    this.setState({ x: event_offsetX, y: event_offsetY },
     () => Streamlit.setComponentValue([this.state.x, this.state.y]));
  }

}

// "withStreamlitConnection" is a wrapper function. It bootstraps the
// connection between your component and the Streamlit app, and handles
// passing arguments from Python -> Component.
//
// You don't need to edit withStreamlitConnection (but you're welcome to!).
export default withStreamlitConnection(ClickImage)

Lancement du composant

Dans les instructions qui suivent vous allez avoir besoin de npm, le gestionnaire de paquet Node.js, veillez par conséquent à ce qu’il soit installé sur votre machine.
Notre future application Streamlit va communiquer avec ce composant via une relation client/serveur. Nous allons donc exécuter notre composant clickImage sur un serveur local. Pour ce faire, ouvrez un terminal puis rendez-vous dans le répertoire frontend. Ensuite lancer les instructions suivantes :


npm install

puis


npm run start

Si tout se passe bien, vous devriez avoir d’une part un message vous informant que le composant a été compilé correctement, et d’autre part, l’url à laquelle le composant est accessible. Il s’agit d’une adresse locale, sur le port 3001.

Que faire si je développe mon application Streamlit sous Google Colab ?

En effet, le fait que, en phase de développement, notre composant soit déployé sur un serveur local peut sembler contraignant pour ceux qui travaillent sous Colab. Il faudra créer un tunnel de connexion afin d’exposer l’url du composant.

Comme vous le savez peut-être déjà, il est courant d’utiliser Ngrok pour déployer Streamlit depuis Colab, or il vaut mieux éviter de créer un second tunnel ngrok pour déployer le composant, nous allons donc utiliser une alternative : localtunnel

Voici le lien github pour plus de details : repo localtunnel

Dans un autre terminal, lancer les instructions suivantes :


npm install -g localtunnel

puis


lt --port 3001

Vous allez normalement obtenir une adresse en loca.lt qui va venir exposer l’adresse locale de votre composant.

Si vous vous apprêtez à développer votre application Streamlit sous Colab, notez cette adresse. Si vous travaillez en local l’adresse précédente suffit.
Passons maintenant au développement de notre démo Streamlit.

Implémentation de l’application Streamlit

L’application Streamlit minimale, consistant à éditer notre composant et retranscrire à l'écran les coordonnées du clic de la souris, va ressembler à cela :


import streamlit as st 
import streamlit.components.v1 as components

_clickImage = components.declare_component(
    "clickImage",
    url="https://xxxxxxxx.loca.lt",  #ou l’url locale
)

def clickImage(key=None):
    clickImage_value = _clickImage(key=key, default=0)
    return clickImage_value


st.write("Mon image cliquable :")
coord = clickImage()
if not isinstance(coord, int):
    st.write('x : {}, y : {}'.format(coord[0], coord[1]))

Comme vous le voyez, nous déclarons notre composant en spécifiant l’adresse à laquelle le trouver. Je précise que nous ne sommes encore qu’en phase de développement car nous allons compiler a terme le composant pour qu’il soit intégré à l’application Streamlit et qu’il puisse être déployé en production.

Lorsque vous lancez votre application Streamlit, vous obtenez normalement un message de sécurité en lieu et place du composant. Il vous faut alors cliquer sur l’adresse puis valider la communication en cliquant sur le bouton “Continue” avant de rafraîchir votre page. Ce point disparaîtra lorsque le composant sera compilé.

A ce stade, si vous exécutez votre application Streamlit, vous devriez voir votre composant en action. Ainsi chaque clic de souris sur l’image devrait se traduire par un affichage des coordonnées du curseur. Il nous reste, pour être complet, à compiler notre composant afin de ne plus avoir à lancer de serveur local (ni de tunnel).

Compilation et finalisation du composant

Stopez le tunnel si vous en avez lancé un, ainsi que le serveur local sur lequel est déployé le composant.
Ouvrez un nouveau terminal et rendez-vous dans le répertoire template / clickImage / frontend. S’il existe un répertoire build, je vous conseille de le supprimer, nous allons en effet en recréer un automatiquement.
Lancez ensuite les commandes suivantes :


npm install

puis


npm run build

Si tout se passe normalement, un message, vous indiquant que le répertoire build est désormais prêt à être déployé, s’affiche à l'écran. Il ne vous reste donc plus qu'à copier ce répertoire build et le placer, après l’avoir renommé, dans le projet associé à votre application Streamlit. Pour ceux qui travaillent sous Colab, il faudra par conséquent l’importer dans le système de fichiers associé au notebook.

Pour l'exemple, nous copions donc le répertoire build, l’importons dans notre projet puis le renommons clickImage. Il nous reste à adapter légèrement notre application Streamllit afin que celle-ci n’aille plus requêter le composant auprès d’un serveur mais via un chemin relatif, comme ceci :


_clickImage = components.declare_component(
    "clickImage",
    path='./clickImage'
)

Comme vous le voyez nous avons enlevé le paramètre “url” et l’avons remplacé par un paramètre “path” qui indique à l'application ou se trouve le répertoire de notre composant. (pour rappel le répertoire se nomme ici clickImage comme notre composant).

Voilà, nous connaissons les bases et n’avons plus aucune excuse désormais pour ne pas tenter de développer des interfaces originales qui mettent en valeur tout le travail réalisé en amont sur nos modèles.