Inspiration

The fashion industry is one of the most wasteful on the planet. The Ellen MacArthur Foundation's "A New Textiles Economy" report put it starkly: every second, the equivalent of one garbage truck of clothing is landfilled or burned, and clothing utilisation has fallen 36% over the past fifteen years. That's not a supply problem. It's an attention problem: people own more than ever and use less of it than ever.

I felt that exact problem in my own closet. My biggest recurring frustration isn't having nothing to wear, it's forgetting what I already own. Things get pushed to the back of a rail and functionally disappear, even though they're perfectly good, sometimes barely worn. Multiply that by a closet, then by every closet, and "forgetting what you own" stops being a personal quirk and becomes the mechanism behind a chunk of that garbage-truck statistic.

ClosetIQ is my answer to my own problem: an app that actively surfaces what I'm not seeing, tells me why it works today specifically, and makes it effortless enough that "I forgot I had this" stops being the reason it never gets worn again. Essentially ending the "I don't have anything to wear"-everyday-problem.

What it does

ClosetIQ reads your skin from one selfie, then picks something from your own wardrobe that suits you today and that you haven't worn in months — and renders it onto your own photo before you commit to wearing it.

Three screens:

  • Mirror — today's skin reading (undertone, Fitzpatrick, redness/dullness/dark circles), one forgotten garment with a sentence explaining why, and virtual try-on. "Wore it" logs the wear and resets that garment's dormancy clock.
  • Closet — everything you own, headlined by the percentage worn this season and the dormancy score
  • Worth it? — photograph something you're about to buy and see how many similar things you already own, entirely on-device.

Onboarding asks for a selfie plus optional full/upper/lower-body photos, because those are genuinely different photographs: Skin Analysis needs a face, virtual try-on needs the body region it's about to replace. The app resolves the right photo per garment category so a top uses the upper-body shot, trousers use the lower-body shot, and if neither exists it reports it as an error.

How we built it

This is an Android app made with Kotlin and Jetpack Compose. It uses MVVM with StateFlow.

The app does not talk to YouCam directly. A Node and Express backend holds the API key. It uses one process for both tasks: create a task, then check it. The poller waits 1s, 2s, 4s, then 8s, and stops after 90 seconds. Images are resized for YouCam before sending. The long side is max 1600, the short side is at least 480, and JPEG quality is 88. This ensures uploads are not rejected.

Scoring is paletteFit + skinDayFit + 2×dormancy − recentRepeat. Dormancy is weighted double on purpose: too low and the app just recommends your five favourites forever, which defeats the entire premise.

   ┌─────────────┐        ┌──────────────────┐
   │   Selfie    │───────▶│  Node backend     │───────▶  YouCam API
   │ + body pics │        │ (holds the key)   │◀───────  (Skin + Try-On)
   └─────────────┘        └────────┬─────────┘
                                    │
                                    ▼
                          ┌───────────────────┐
                          │  Skin reading       │
                          │  undertone,         │
                          │  Fitzpatrick,        │
                          │  redness/dullness    │
                          └────────┬────────────┘
                                    │
                                    ▼
   ┌─────────────┐        ┌───────────────────┐
   │  Your closet │───────▶│  Local scoring      │
   │  (on-device) │        │  engine (no API)    │
   └─────────────┘        │  dormancy + colour   │
                          └────────┬────────────┘
                                    │
                                    ▼
                          ┌───────────────────┐
                          │  Today's pick        │
                          │  + why, in one line   │
                          └────────┬────────────┘
                                    │
                                    ▼
                          ┌───────────────────┐
                          │  Virtual try-on       │
                          │  rendered on you      │
                          └───────────────────┘

Challenges we ran into

  1. cloth (virtual try-on) reports a request it couldn't fulfil as SUCCESS with an empty results object - no error, no image, credit spent. We hit this directly: aiming a top's try-on at a head-and-shoulders selfie returned error_src_face_too_small from a real device. The fix was architectural — store the selfie and body photos in separate slots, resolve the correct one per garment category, and refuse before spending a credit when nothing on file could plausibly contain the target region.

  2. Extracting a garment's colour from its photo was its own trap. Averaging pixels is wrong on patterns (a red-and-white stripe averages to pink), so we bucket into coarse Lab cells and take the largest region. Filtering out near-black to drop shadow initially deleted most of a black coat and returned the colour of the wall behind it instead — background is now only dropped when a region is bright AND colourless together.

  3. We also chained multiple try-on calls to render a whole outfit (top, then outerwear over it, then trousers, then shoes) rather than one garment floating over the user's own clothes — each pass feeds the previous render back in as the next person image, which meant downloading YouCam's result URLs locally, which also fixed their 2-hour expiry as a side effect.

What's next for ClosetIQ

  • Downloading and caching try-on renders permanently instead of relying on short-lived URLs.
  • Letting outfits be saved and reused, not just generated fresh each time.
  • And eventually, nudging users proactively — "you haven't worn this in 90 days" — instead of waiting for them to open the app.

Built With

  • android
  • coil
  • datastore
  • express.js
  • jetpack-compose
  • kotlin
  • kotlinx.serialization
  • material-3
  • node.js
  • okhttp
  • perfectcorp-youcam-api
  • retrofit
  • room
Share this project:

Updates

posted an update —

local colour matching :

    /** Golden, earthy, sunlit. Positive b throughout. */
    private val WARM_ANCHORS = listOf(
        LabColor(45f, 30f, 35f),   // rust
        LabColor(70f, 8f, 55f),    // mustard
        LabColor(45f, -8f, 25f),   // olive
        LabColor(88f, 2f, 14f),    // cream
        LabColor(32f, 12f, 20f),   // chocolate
        LabColor(66f, 32f, 24f),   // coral
        LabColor(40f, -20f, 22f),  // moss
        LabColor(58f, 18f, 30f)    // caramel
    )

    /** Blue-based, cool, jewel-toned. Mostly negative b. */
    private val COOL_ANCHORS = listOf(
        LabColor(28f, 3f, -24f),   // navy
        LabColor(46f, -32f, 12f),  // emerald
        LabColor(32f, 30f, 0f),    // berry
        LabColor(94f, 0f, -2f),    // optic white
        LabColor(35f, 0f, -3f),    // charcoal
        LabColor(62f, 12f, -18f),  // lavender
        LabColor(52f, -22f, -6f),  // teal
        LabColor(45f, 2f, -35f)    // true blue
    )

    /** Muted and low-chroma — the tones that read as neither warm nor cool. */
    private val NEUTRAL_ANCHORS = listOf(
        LabColor(34f, 2f, -16f),   // soft navy
        LabColor(60f, 4f, 10f),    // taupe
        LabColor(64f, -12f, 12f),  // sage
        LabColor(92f, 0f, 4f),     // soft white
        LabColor(45f, -2f, -6f),   // slate
        LabColor(60f, 18f, 8f),    // dusty rose
        LabColor(30f, 4f, 6f),     // espresso
        LabColor(72f, -6f, 2f)     // stone
    )

    /**
     * The set of anchor colours that suit this person.
     *
     * Undertone picks the family; Fitzpatrick tunes it. These are hand-picked constants
     * rather than anything generated — colour analysis is a matter of taste, and taste
     * is easier to argue with when it is written down as eight literal values.
     */
    fun buildPalette(reading: SkinReading): Palette {
        val base = when (reading.undertone) {
            Undertone.WARM -> WARM_ANCHORS
            Undertone.COOL -> COOL_ANCHORS
            Undertone.NEUTRAL -> NEUTRAL_ANCHORS
        }

        return Palette(
            anchors = base.map { forFitzpatrick(it, reading.fitzpatrick) },
            undertone = reading.undertone
        )
    }

Log in or sign up for Devpost to join the conversation.

Submission history