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.