Olivier Couthaud

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.

Photo Title Numbers for the photo Numbers for the title Learnertrained on real prices Low, middle, highstretched so the real pricelands inside 4 times in 5
Both lists of numbers come from models I did not train. Swipe sideways to see all of it.

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.

How close the middle guess gets

Out of 100 products: 13 land within 10% of the real price, 31 within 25% and 57 within 50%. The typical miss is 42%.

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.

price, dollars (log scale: equal distances are equal ratios) $1 $2 $5 $10 $20 Text only: $1.20 Photo: $13.93 Actual: about $15.47
One rare skin I picked, AK-47 Crane Flight (Field-Tested). An example, not a measurement. Swipe sideways to see all of it.

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.

Product photo Brand + title +leaf category Category (one-hot) CLIP ViT-B/32frozen, 512-d all-MiniLM-L6-v2frozen, 384-d Concatenateone vector LightGBMquantile x3 p10p50p90target:log1p(price) Conformalcalibration(CQR) output:low / mid / high~80% interval
  1. The product photo. For the Amazon sets it is the catalog image, and I also tested on photos customers uploaded in reviews.
  2. CLIP ViT-B/32, frozen. It turns the photo into 512 numbers. I never train it, and each image is embedded only once.
  3. Brand, title and leaf category joined into one string.
  4. all-MiniLM-L6-v2, also frozen. It turns that string into 384 numbers.
  5. 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.
  6. The image numbers, text numbers and category slots are stuck end to end into one long vector.
  7. 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.
  8. 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.
Diagram. The pipeline for the Amazon models. Both encoders are frozen and only the LightGBM heads are trained. The CS2 models differ: the text model uses name n-grams plus float and wear features, and the image model uses the CLIP vector plus float, StatTrak and wear features, with no text encoder. Swipe sideways to see all of it.
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.

The same question at four scales

Coverage is how often the real price fell inside the stated 80% range. 8,821 listings, 3,658 distinct skins. The target is CSFloat's reference market value, not the seller's ask, which was too noisy.

ModelRMSLEMedian errorCoverage
Guess the median3.04n/an/a
Name text + tabular features0.78833.1%83%
Image only0.91329.9%81%
Image + float, StatTrak0.82819.0%83%

The photo model has the lower typical error and the worse RMSLE. I put that down to a few expensive skins.

5,000 Amazon grocery products, 4,296 of them usable. Price runs from $0.39 to $495, median $18.75.

ModelRMSLEMedian errorCoverage
Guess the median0.726n/an/a
Text only0.67142.1%78.8%
Image only0.67743.8%80.5%
Image + text0.65941.6%81.9%

Fusing gave 1.8% better RMSLE than text alone. Small, but the photo on its own nearly matches the title.

12,000 products, 1,500 in each of 8 categories. Price runs from $0.01 to $2,000, median $19.99.

ModelRMSLEMedian errorCoverage
Guess the median1.023n/an/a
Text only0.85548.5%79.2%
Image only0.82347.9%80.7%
Image + text0.79146.7%80.2%
Image + text + category0.78847.1%80.7%

Image beats text here. One model for all eight categories did as well as eight separate ones: average RMSLE 0.784 against 0.782.

72,000 products, 4,000 in each of 18 categories, tested on held-out catalog rows.

ModelRMSLEMedian errorCoverage
Image only0.77446%79%
Image + text0.72642%80%
Image + text + category0.72042%79%

Six times the data moved the typical error from 47% to 42%. More on that under what didn't work.

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.

price, dollars (log scale) $1 $2 $5 $10 $20 Text model: $1.20 Image-only model: $13.93 Actual: about $15.47
Diagram. AK-47 Crane Flight (Field-Tested), drawn from the three prices above on a log axis. Swipe sideways to see all of it.
13within 10% 18more, within 25% 26more, within 50% 43further off
The same table as a picture: out of every 100 products in the held-out test set (a fifth of the 72,000), how many the middle guess landed within 10%, 25% and 50% of the real price.
72,000 Amazon products, 18 categories. Held-out catalog test, and 2,979 real user review photos. The first three columns (≤10%, ≤25%, ≤50%) are the share of products whose middle guess was within that distance of the listed price. Median error is the median absolute percentage error. Coverage is the share of true prices inside the stated 80% range. Fused means image + text + category.
Model, test set≤10%≤25%≤50%Median errorCoverage
Fused, catalog images13%31%57%42%79%
Fused, user photos13%31%57%43%82%
Image only, user photos11%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.

median error, % (lower is better) Cell Phones & Accessories25% Pet Supplies34% Baby Products35% Beauty38% All categories42% Video Games49% Tools51% also worse: Automotive 53%, Musical Instruments 55%
Median error by category for the 72,000-product model. Only the four best and four worst categories are shown. Swipe sideways to see all of it.

All eight categories, from the 12,000-product run. Pick what to rank them by.

Eight categories, one model

12,000-product model, image + text + category, held-out test. About 300 test products per category. Coverage is the share of true prices inside the stated 80% range.
CategoryTest nRMSLEMedian errorCoverage
Grocery3020.64339.0%84.1%
Toys & Games3070.69940.6%86.6%
Amazon Fashion2700.72741.0%83.3%
Home & Kitchen3040.78042.9%79.9%
Sports & Outdoors2970.76947.9%78.5%
Office Products3060.78754.0%79.7%
Electronics3170.85559.6%78.2%
Musical Instruments2970.99161.5%75.4%

Sorted by median error, best first. Grocery, toys and fashion are the easy ones. Electronics and instruments are the hard ones.

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).

RDD2022, United States split, held-out 20%. Average precision is the area under the precision-recall curve.
Damage typeShare of imagesROC-AUCAverage precision
Alligator crack (D20)15%0.7640.43
Transverse crack (D10)49%0.6790.68
Longitudinal crack (D00)81%0.6550.88
Pothole (D40)2.4%0.6540.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

← All work