The problem in brief
Photos of cats and dogs were converted into coloured ASCII art: every image is a grid of 128 × 128 characters, each drawn in its own colour inside a 10 × 10 pixel cell, giving a 1280 × 1280 picture. The characters come from a fixed palette: ten "fill" characters from light to dense (space, ., ;, c, o, P, O, ?, @, ■) and four "edge" characters (|, -, \, /) placed on contours. You must segment the pet: output a binary mask of the 1280 × 1280 image. You get 3 680 training images with masks and 500 test images, and two rules: no pretrained models, no external data. Everything has to be learned from scratch on the given images.
The data comes from the Kaggle competition archive. The test masks are private and we never submitted anything, so all scores below are measured on 400 held-out training images, with the exact pixel-level metric.
How you are scored
The Dice coefficient between the predicted mask and the true mask , at full resolution, averaged over images:
What to aim for. The organisers publish no threshold. On the public leaderboard the best entry scores 0.921, the next ones 0.909, 0.906 and 0.903.
First look at the data
The organisers' baseline shrinks each image to 128 × 128 (one pixel per character), trains a small CNN, and enlarges the predicted mask back to 1280 × 1280. Before training anything, it is worth asking what the best possible mask of that kind would score. We took the true character-level labels, filled each pet character's whole 10 × 10 cell, and compared with the real mask: Dice 0.23. Even a perfect classifier would fail with this output.
The reason is visible once you zoom in:

The true mask is not the silhouette of the pet. It is the set of ink pixels of the characters that belong to the pet: the strokes of the /, the ring of the o, the bar of the -. The black background pixels between strokes are 0 even in the middle of the dog. Only 7 % of the pixels are 1 in a typical mask, while the silhouette covers about half the image.
Two checks confirm it on a training image: every mask pixel is an ink pixel (non-black in the render), and the rule
rebuilds the mask with Dice 0.994 from the character labels. The 0.6 % missing comes from a few glyphs (like \) that slightly overflow into the next cell.
A first baseline
So the task reduces to a 128 × 128 classification problem: which characters belong to the pet? The pixel mask then follows from the ink of the image itself, which we can read exactly.
A U-Net trained for a single epoch on character-level features already gives 0.79 with the ink rule. The same predictions turned into full blocks give 0.17: the output format is worth more than the model.
The key idea
Work at the level of the generating process: one decision per character, then let the image provide the exact pixels. Two consequences guide the design.
What the model sees. Instead of a blurred 128 × 128 RGB image, we describe each character by 13 numbers: the ink density in a 3 × 3 grid of sub-blocks (3, 4 and 3 pixels wide), the mean colour of its ink (3 values) and its total amount of ink. The sub-block densities tell the glyphs apart: | lights up the middle column, - the middle row, \ and / the two diagonals, and the fill characters differ by their total density. This matters because the converter put the edge characters on the contours of the photo, so the glyph shape is a direct hint of where the pet's outline is. The colour carries the content (fur versus grass versus a sofa).
What the model optimises. A character's weight in the Dice score is its amount of ink: a missed @ costs many pixels, a missed . almost none, a missed space nothing. So the loss counts only characters with ink, and the soft Dice part is weighted by ink:
where is the ink of character , the predicted probability and the fraction of its ink that is in the mask.
Augmenting correctly. A horizontal mirror is a natural augmentation for pets, but a mirrored image would contain mirrored glyphs, and a / becomes a \. Mirroring our features therefore means flipping the 128 × 128 grid and swapping the left and right sub-block columns of each character, so the augmented example is still a valid ASCII rendering.
The full solution
- Features (about 6 minutes for all images, read directly from the zip): a 128 × 128 × 13 array per image, plus the target per character.
- U-Net from scratch: 5 levels, widths 32 to 256, batch normalisation, 5.1 M parameters, trained 30 epochs (batches of 8, AdamW, one-cycle learning rate up to ) with the loss above and the glyph-aware mirror. About 30 seconds per epoch on the Apple GPU.
- Prediction: average the probabilities of the image and of its mirror (test-time augmentation), threshold at 0.5.
- Back to pixels with the ink rule:
def to_pixels(cells, im):
return (im.max(2) > 0) & np.kron(cells, np.ones((10, 10), bool)) # ink AND pet character
- Encode each mask in column-major run-length encoding, as in the organisers' baseline.
Results
Mean pixel Dice at 1280 × 1280 on 400 held-out training images (the test masks are private).
| approach | Dice |
|---|---|
| perfect character labels, full 10 × 10 blocks (best possible "baseline-style" output) | 0.233 |
| U-Net 1 epoch, width 16, full blocks | 0.171 |
| U-Net 30 epochs, width 32, full blocks | 0.197 |
| U-Net 1 epoch, width 16, ink rule | 0.792 |
| U-Net 30 epochs, width 32, ink rule (final) | 0.903 |
| perfect character labels, ink rule (upper bound) | 0.994 |
The final model reaches 0.903 on held-out images, the level of the fourth public-leaderboard entry and 2 points below the best one (0.921). The gap to the upper bound (0.994) is now purely about deciding which characters are pet, mostly near the outline and in dark regions where fur and background look alike in ASCII.
What to remember
- Understand the target before choosing the model. Ten minutes spent looking at one mask at full zoom changed the best achievable score from 0.23 to 0.99. When data is synthetic, find the rule that generated it.
- Predict at the scale of the decision. Each character is either pet or not; predicting 1.6 million pixels independently would waste capacity, and predicting blocks loses the exact shape. One decision per character plus an exact, deterministic expansion gets the best of both.
- Weight the loss like the metric. The Dice score counts pixels, so characters with more ink matter more; an ink-weighted soft Dice aligns training with evaluation.
- Augmentations must keep the data valid. Mirroring ASCII art turns
/into\; the feature transform has to follow. - From scratch is fine when inputs are simple. With a compact, meaningful encoding (13 numbers per character), a U-Net without pretraining reaches the top of the leaderboard in 15 minutes.