PriceEstimator
Guesses what an item costs from a photo and a title, and gives a range instead of a single number.
I wanted to point my phone at something and get a price. Not an exact one, just a fair guess that also tells me how wrong it might be.
The trouble is that a photo leaves out most of what sets a price. You can see a pair of headphones. You can't see whether they are the cheap ones or the good ones. So the program never gives a single number. It gives a low, a middle and a high, and the range is stretched until the real price falls inside it about four times out of five. When it is unsure, the range gets wider.
To build it I took two existing models that turn pictures and text into lists of numbers. They only describe things, they don't write or draw anything. I fed them the photos and titles of tens of thousands of products from a public set of Amazon listings, and trained a small learner to connect those numbers with the real prices. I started with Counter-Strike skins because that was cheap, then groceries, then eighteen categories.
It works as a ballpark and nothing better. Turn the dial to see how often the middle guess was close, out of 100 products it had never seen.
It behaves like a friend who glances at something in a shop and guesses. They know a phone case from a laptop, but not whether it is the 128 GB model or the 256. It does best on standard things like phone accessories, where the typical miss is about a quarter of the price, and worst on musical instruments, where it is often off by half.
I hoped more data would tighten it up. Six times as much moved the typical miss only from 47% to 42%. I think that is a limit of the approach: brand, size and condition mostly aren't in the pixels. It does cope with real photos, though. In a test with about 3,000 photos customers uploaded in reviews, it did nearly as well as on clean catalog pictures. That test covered only some of the product types, so it is a rough check.
The Counter-Strike skins taught me something different. A version that worked from the skin's name and its wear numbers priced a rare skin at $1.20 because its name looks like thousands of cheap ones. The photo version said $13.93, and it is worth about $15.
What's next
There is no app. The one thing you can run is a local web page for the skin version, where you upload a skin picture and get an estimate and its range. Next I want to feed it brand and specs, and let it look up similar listings. I also tried the same approach on road condition, from street photos. It isn't useful yet.
What I wanted
I wanted to point a phone at an object and get a price. Marketplace listings are the obvious training data, but a photo leaves out brand, size and condition, so I settled on something narrower: a ballpark price plus a range that says how far to trust it. I also decided up front to use no generative model. Everything here is an encoder, which turns an image or a sentence into a list of numbers, feeding gradient-boosted trees.
How it works
The text input is the brand, title and leaf category joined into one string. Both encoders are frozen, so every product's image and text are embedded once and cached, and everything after that runs on a CPU. The target is log1p(price), which makes a 20% miss cost the same at $5 and at $500.
- The product photo. For the Amazon sets it is the catalog image, and I also tested on photos customers uploaded in reviews.
- CLIP ViT-B/32, frozen. It turns the photo into 512 numbers. I never train it, and each image is embedded only once.
- Brand, title and leaf category joined into one string.
- all-MiniLM-L6-v2, also frozen. It turns that string into 384 numbers.
- Optional. One slot per category, 8 or 18 of them, set to 1 for the item's own category. It barely helped: RMSLE went from 0.791 to 0.788 on the 12,000-product set.
- The image numbers, text numbers and category slots are stuck end to end into one long vector.
- Three LightGBM models, one each for the 10th, 50th and 90th percentile of log1p(price). The middle one is the guess, the outer two are the raw low and high.
- A conformal step checks the low and high against a held-out fifth of the data and widens them by exactly as much as needed. On the 72,000 run, 79% of test prices fell inside the range.
What is CLIP?
CLIP is a model trained on pairs of images and captions, so that a photo and a description of it end up close together as lists of numbers. I use it frozen: ViT-B/32 turns each photo into 512 numbers and I never retrain it. It describes images, it doesn't generate them.
Three LightGBM models predict the 10th, 50th and 90th percentile of that target. A conformal step then widens or tightens the band using a held-out calibration split, so the stated 80% range holds about 80% of the time. I wrote that step by hand instead of pulling in a library. The split is 60% train, 20% calibration, 20% test.
What is a quantile model?
An ordinary regression predicts one number, the average. A quantile model predicts a chosen percentile instead: the 10th percentile is a price that 10% of similar items fall below. With the 10th, 50th and 90th together you get a low, a middle and a high, and the width tells you how spread out the prices are for items that look like this one.
What is conformal calibration?
The raw 10th-to-90th band is only as honest as the model, and a model can be too confident. So I score it on data it was not trained on and measure by how much the true prices miss the band. The conformal step then picks the width that brings the miss rate to 20%, which is what makes the 80% mean something. This version is called CQR.
I built it in four stages, each a bigger version of the last. CS2 skins from CSFloat listings came first, because it was a cheap way to check the pipeline. Then 5,000 Amazon grocery products, to ask whether a photo adds anything a title doesn't. Then 12,000 products across 8 categories, and finally 72,000 across 18. For the last one I rented an RTX 3090 on Vast.ai to do the embedding, which cost $2.09 in all.
Results
The photo does carry price signal on general products, and fusing it with the title works best. Switch between the stages to see the numbers.
What is RMSLE?
It is the typical size of the error measured in log-price, so being off by a factor of two counts the same whether the item costs $5 or $500. Lower is better. Where a table has a first row that guesses the median price for everything, that row shows what no skill looks like.
CS2 is a weak test of the photo idea, because a skin image is just a render of its name and wear, so the model mostly recognises which skin it is. Even so, the image-only model got right a skin the text model badly missed. The text model priced AK-47 Crane Flight (Field-Tested) at $1.20, because its name pieces are shared with thousands of cheap skins. The image-only model said $13.93 against about $15.47. That is one skin I picked, an example and not a measurement.
| Model, test set | ≤10% | ≤25% | ≤50% | Median error | Coverage |
|---|---|---|---|---|---|
| Fused, catalog images | 13% | 31% | 57% | 42% | 79% |
| Fused, user photos | 13% | 31% | 57% | 43% | 82% |
| Image only, user photos | 11% | 27% | 52% | 47% | 82% |
The user photos come from 8 of the 18 categories, and the fused rows also read each product's title. Those products may also appear in the training catalog, so this shows how well it copes with phone photos, not a clean exam. The cost of the switch from catalog to real photos is small: RMSLE 0.720 to 0.770.
All eight categories, from the 12,000-product run. Pick what to rank them by.
A photo gets a ballpark, not a price. Only 13% of items land within 10%, and the median error is about 42%.
What didn't work
The accuracy goal didn't survive scaling. Going from 12,000 to 72,000 products moved the median error only from 47% to 42% (the category sets differ, 8 then 18, and so do the test rows, so the two are only roughly comparable), and the share within 10% stayed at 13%. My read is that this is a ceiling for frozen CLIP plus trees, not a lack of data, since brand, size, specs and condition mostly aren't in the pixels. Nothing I ran tests that directly.
Smaller failures. On the grocery set, fusing image and title improved RMSLE by only 1.8%, and image-only was slightly worse than text-only. I suspect pack size and weight, which a photo rarely shows, but I didn't test it. CSFloat's rate limit capped how much CS2 data I could pull (8,821 listings across 3,658 skins, about 2.4 per skin), so rare skins are priced poorly, though the model flags them as low confidence. Embedding at scale was limited by image downloads, about 13 images per second, not by the GPU.
What's unfinished
There is no inference API and no abstain gate for photos that can't be priced. Structured attributes such as specs and brand, retrieval of comparable listings for recognisable products, and end-to-end fine-tuning are all untried. The only demo is a local web page for the CS2 skin model: upload a skin image and it shows the estimate, the 80% range and a confidence meter.
I also began applying the same stack to road condition. On 4,805 US dashcam-style images from RDD2022 (CC-BY-SA), CLIP embeddings plus LightGBM pick up a weak to moderate signal for each of the four damage types, with ROC-AUC between 0.65 and 0.76 (0.5 is chance).
| Damage type | Share of images | ROC-AUC | Average precision |
|---|---|---|---|
| Alligator crack (D20) | 15% | 0.764 | 0.43 |
| Transverse crack (D10) | 49% | 0.679 | 0.68 |
| Longitudinal crack (D00) | 81% | 0.655 | 0.88 |
| Pothole (D40) | 2.4% | 0.654 | 0.06 |
Potholes are the weakest, since they are small and rare and a whole-image embedding mostly misses them. Every image in that set contains damage, so a damaged-or-not test isn't possible on it.
The second step matched Mapillary street imagery to San Francisco's open pavement-condition scores by GPS, with no manual labelling. 656,951 image points against 19,425 road segments gave 30,479 matches within 25 m, and I sampled 10,479 labelled images from those. A condition model trained on them is barely better than guessing the median: mean error 16.5 points against 18.0, correlation 0.34. It isn't useful yet. A street photo is mostly sky, buildings and cars, so the road is a small part of the pixels, and cropping to the pavement is the first thing I would try.
The training data is Amazon Reviews 2023 (McAuley Lab) and CSFloat's listings API, and the road work uses RDD2022, Mapillary imagery and San Francisco's open pavement data. I haven't published the data or the trained models, and I haven't checked whether those terms allow commercial use, so this stays a research project.
- Stack
- Python, CLIP ViT-B/32 (via sentence-transformers clip-ViT-B-32), all-MiniLM-L6-v2, LightGBM, hand-written conformal prediction (CQR), Flask demo
- Data
- Amazon Reviews 2023 (up to 72,000 products), CSFloat listings (8,821), RDD2022 (4,805 US images), Mapillary x San Francisco pavement data (10,479). Not redistributed.
- Status
- Prototype. Evaluated offline; local demo for CS2 only
- Code
- Not public