← Projects

ShelfCount

I built a phone web app that counts shoe boxes by finding the colored sticker on each box in a photo of the shelf.

Year
2026
Built with
TypeScript, Next.js, computer vision, Supabase

Overview

I built ShelfCount to count shoe boxes from a photo instead of by hand. Each box gets a small neon green or hot pink sticker. I take a picture of a shelf section with my phone, and the app finds the stickers and counts them.

The detection runs in the browser, so the photo doesn't have to go to a server to be counted. The app puts a marker on every sticker it found. I can zoom in, tap a marker to remove a false hit, or tap an empty spot to add a sticker it missed, and the count changes as I tap.

Counts are grouped into sessions. A session has a name, a sticker color and a sensitivity level. Each photo in it becomes an entry with a section label, notes, the automatic count and the corrected count. A whole session can be exported as a CSV file for a spreadsheet.

The app is built with Next.js and TypeScript, and it stores sessions, entries and photos in Supabase.

How it works

Before detection, the app shrinks the photo to at most 1600 pixels wide and saves it as a JPEG at 75% quality. It then runs five steps on the pixels, in plain TypeScript with no image library.

  • It converts each pixel from RGB to HSV, which separates the color itself (hue) from how strong and how bright it is.
  • It keeps the pixels whose hue, saturation and brightness fall inside the range for the session's sticker color. This gives a black and white mask.
  • It erodes and then dilates the mask with a 3 by 3 window. This removes specks of a few pixels and keeps the stickers.
  • It groups touching pixels into blobs with a two pass labeling that uses union find.
  • It drops blobs that are too small or too large, and turns the center of each remaining blob into one dot.

The size limits grow with the photo's resolution. At medium sensitivity, a blob must cover between 25 and 5,000 pixels for each megapixel in the photo. The editor can also show the mask over the photo, which makes it easy to see why a sticker was missed.

Design decisions

  • Detection in the browser. Running it on the phone means there is no image server to host, and the count appears right after the photo is taken. The cost is that I wrote the image steps by hand instead of calling OpenCV.
  • A fixed width before detecting. The detector reads every pixel several times, so capping the width at 1600 pixels keeps the work for each photo bounded. The same smaller JPEG is the one that gets uploaded.
  • The automatic count is a first draft. The app saves both the detected dots and the corrected ones, so the final number is always one a person has checked, and the CSV lists both counts side by side. Dots I add are drawn in green and detected ones in blue, so I can see what I changed.
  • Settings per session. The sticker color and sensitivity are chosen when a session starts, so every photo in one count uses the same thresholds.
  • No accounts. A session's ID is the only thing that protects it, and the Supabase public key ships in the page. That kept the app small, and the README says it is only meant for internal use.

Problems I ran into

  • Pink wraps around the color wheel. Hue is an angle from 0 to 360 degrees, and hot pink sits on both sides of 0. A plain minimum and maximum would miss half of it, so a range whose minimum is larger than its maximum is read as wrapping past 360. At medium sensitivity, pink is any hue from 290 to 360 or from 0 to 10.
  • Taps and pans on the same image. On a phone, a drag to pan the photo ends with a touch that looks like a tap, and that would add a stray dot. The editor ignores taps while a pan is in progress and for 100 ms after it ends.
  • Small targets under a finger. Each marker is drawn with a radius of 12 image pixels but answers to taps within 20, so it can be removed without zooming in every time.
  • Stickers that touch. Two stickers side by side merge into one blob, which is often too large and gets dropped. The app can't split them yet. Instead it lowers its confidence score when many blobs are rejected, and the entry shows a Low or Med badge so I know to look closer.

Results

I don't have a measured accuracy for ShelfCount. The repository has no test photos and no tests, so I can't say how often the automatic count is right before correction.

What the code does give is a confidence score from 0 to 1 for each photo. It starts at 1 and goes down when fewer than 70% of the blobs pass the size filter, when more than half are rejected, when nothing is found, or when more than 50 dots are found. The entry list shows it as High at 0.8 or more, Med from 0.5, and Low below that.

The app covers the whole flow for a count:

  • creating a session and choosing its sticker color and sensitivity,
  • taking a photo with the back camera or uploading one,
  • detection, then correction with zoom, undo and reset,
  • editing a saved entry later,
  • exporting the session as a CSV with the automatic count, the final count and the confidence for every section.

What I'd do next

  • Measure it. A small set of shelf photos with hand counts would show how often the automatic count is right, and which sensitivity level should be the default.
  • Split touching stickers. The typical sticker size in a photo could be used to estimate how many stickers a large blob holds, instead of dropping it.
  • Pick one version. The repository also holds an unfinished second version with a FastAPI and OpenCV backend, user accounts and a Vite frontend. Keeping one of the two would make the project easier to maintain.
  • Add accounts. Today anyone with a session link can read and change it, which is fine for a private tool but not for a shared one.

← Projets

ShelfCount

J'ai construit une application web pour téléphone qui compte des boîtes à chaussures en repérant la pastille de couleur de chaque boîte sur une photo de l'étagère.

Année
2026
Outils
TypeScript, Next.js, vision par ordinateur, Supabase

Aperçu

J'ai construit ShelfCount pour compter des boîtes à chaussures à partir d'une photo plutôt qu'à la main. Chaque boîte reçoit une petite pastille vert fluo ou rose vif. Je photographie une section d'étagère avec mon téléphone, et l'application trouve les pastilles et les compte.

La détection se fait dans le navigateur, donc la photo n'a pas besoin de passer par un serveur pour être comptée. L'application place un repère sur chaque pastille trouvée. Je peux zoomer, toucher un repère pour retirer une fausse détection, ou toucher un endroit vide pour ajouter une pastille oubliée, et le total change à chaque toucher.

Les comptages sont regroupés en sessions. Une session a un nom, une couleur de pastille et un niveau de sensibilité. Chaque photo devient une entrée avec une étiquette de section, des notes, le total automatique et le total corrigé. Une session entière peut être exportée en fichier CSV pour un tableur.

L'application est écrite avec Next.js et TypeScript, et elle enregistre les sessions, les entrées et les photos dans Supabase.

Fonctionnement

Avant la détection, l'application réduit la photo à 1600 pixels de large au plus et l'enregistre en JPEG à 75 % de qualité. Elle applique ensuite cinq étapes aux pixels, en TypeScript pur, sans bibliothèque d'images.

  • Elle convertit chaque pixel de RVB en TSV, ce qui sépare la couleur elle-même (la teinte) de son intensité et de sa luminosité.
  • Elle garde les pixels dont la teinte, la saturation et la luminosité tombent dans la plage de la couleur de pastille choisie pour la session. On obtient un masque en noir et blanc.
  • Elle érode puis dilate le masque avec une fenêtre de 3 sur 3. Cela supprime les points isolés de quelques pixels et garde les pastilles.
  • Elle regroupe les pixels voisins en taches par un étiquetage en deux passes qui utilise une structure union-find.
  • Elle écarte les taches trop petites ou trop grandes, et le centre de chaque tache restante devient un point.

Les limites de taille suivent la résolution de la photo. En sensibilité moyenne, une tache doit couvrir entre 25 et 5 000 pixels par mégapixel de la photo. L'éditeur peut aussi afficher le masque sur la photo, ce qui aide à voir pourquoi une pastille a été manquée.

Choix de conception

  • La détection dans le navigateur. La faire sur le téléphone évite d'héberger un serveur d'images, et le total s'affiche juste après la photo. En contrepartie, j'ai écrit les étapes de traitement d'image à la main au lieu d'utiliser OpenCV.
  • Une largeur fixe avant la détection. Le détecteur lit chaque pixel plusieurs fois, donc limiter la largeur à 1600 pixels borne le travail pour chaque photo. C'est ce même JPEG réduit qui est envoyé au stockage.
  • Le total automatique est un premier jet. L'application garde les points détectés et les points corrigés, donc le total final est toujours vérifié par une personne, et le CSV donne les deux totaux côte à côte. Les points que j'ajoute sont verts et les points détectés sont bleus, ce qui montre ce que j'ai changé.
  • Des réglages par session. La couleur de pastille et la sensibilité sont choisies au début d'une session, donc toutes les photos d'un même comptage utilisent les mêmes seuils.
  • Pas de comptes. L'identifiant d'une session est la seule chose qui la protège, et la clé publique Supabase est livrée dans la page. Cela a gardé l'application simple, et le README précise qu'elle ne sert qu'en interne.

Problèmes rencontrés

  • Le rose fait le tour du cercle des couleurs. La teinte est un angle de 0 à 360 degrés, et le rose vif se trouve des deux côtés de 0. Un simple minimum et maximum en manquerait la moitié, donc une plage dont le minimum dépasse le maximum est lue comme passant par 360. En sensibilité moyenne, le rose correspond à toute teinte de 290 à 360 ou de 0 à 10.
  • Toucher et déplacer sur la même image. Sur un téléphone, un glissement pour déplacer la photo se termine par un contact qui ressemble à un toucher, ce qui ajouterait un point en trop. L'éditeur ignore les touchers pendant un déplacement et pendant 100 ms après sa fin.
  • De petites cibles sous le doigt. Chaque repère est dessiné avec un rayon de 12 pixels d'image mais réagit aux touchers jusqu'à 20, ce qui permet de le retirer sans zoomer à chaque fois.
  • Des pastilles qui se touchent. Deux pastilles côte à côte se fondent en une seule tache, souvent trop grande, qui est écartée. L'application ne sait pas encore les séparer. Elle baisse plutôt son score de confiance quand beaucoup de taches sont rejetées, et l'entrée affiche un badge Low ou Med pour que je regarde de plus près.

Résultats

Je n'ai pas de mesure de précision pour ShelfCount. Le dépôt ne contient ni photos de test ni tests, donc je ne peux pas dire à quelle fréquence le total automatique est juste avant correction.

Le code fournit par contre un score de confiance de 0 à 1 pour chaque photo. Il part de 1 et baisse quand moins de 70 % des taches passent le filtre de taille, quand plus de la moitié sont rejetées, quand rien n'est trouvé ou quand plus de 50 points sont trouvés. La liste des entrées l'affiche en High à partir de 0,8, en Med à partir de 0,5, et en Low en dessous.

L'application couvre tout le déroulement d'un comptage :

  • créer une session et choisir sa couleur de pastille et sa sensibilité,
  • prendre une photo avec la caméra arrière ou en importer une,
  • la détection, puis la correction avec zoom, annulation et réinitialisation,
  • modifier plus tard une entrée enregistrée,
  • exporter la session en CSV avec le total automatique, le total final et la confiance de chaque section.

Pour la suite

  • La mesurer. Un petit ensemble de photos d'étagères comptées à la main montrerait à quelle fréquence le total automatique est juste, et quel niveau de sensibilité choisir par défaut.
  • Séparer les pastilles qui se touchent. La taille habituelle d'une pastille dans la photo pourrait servir à estimer combien de pastilles contient une grande tache, au lieu de l'écarter.
  • Choisir une seule version. Le dépôt contient aussi une seconde version inachevée, avec un serveur FastAPI et OpenCV, des comptes utilisateurs et une interface Vite. N'en garder qu'une rendrait le projet plus simple à entretenir.
  • Ajouter des comptes. Aujourd'hui, toute personne qui a le lien d'une session peut la lire et la modifier, ce qui convient à un outil privé mais pas à un outil partagé.