I lost a full day of batch output to a single grayscale pixel.
The batch was 40 AI-generated line art pieces, all headed for Adobe Stock as vector-adjacent raster submissions. The CSV was built, the tracker said nothing was pending, the files looked clean at 100% zoom. Adobe's review flagged a chunk of them for artifacts in the line work. When I opened one at 400% I saw it: faint gray anti-aliasing halos around every stroke, and in a few corners, near-invisible noise that read as "dirty scan" to a reviewer. The vector trace I ran afterward to check editability produced hundreds of stray anchor points from those gray pixels.
That was the moment I stopped trusting my eyes and started binarising everything before it touched a CSV.
Why gray pixels kill a trace
AI line art almost never comes out as pure black on white. Diffusion models output soft edges, so a stroke that looks crisp is actually a gradient from, say, RGB 20 down to RGB 180 across 2 or 3 pixels. Two problems follow from that.
First, any automatic vectorizer (Illustrator's Image Trace, potrace, vtracer) has to decide where the edge is. On a soft edge it invents curves to follow the gradient, so you get wobbly paths and anchor point counts that balloon from a few hundred to several thousand per icon. On my test icon, raw trace: 1,847 nodes. After binarising at the right threshold: 312.
Second, Adobe's review checks are human but fast. Faint gray noise reads as a low-quality scan or an upsampled image. Rejections cost you the submission slot and, if they pile up, reviewer attention on your whole account. I was not willing to gamble a 40-asset batch on that.
The threshold is not 128
The obvious move is pixel > 128 ? white : black with a global threshold. It failed on about a third of my pieces, because some generations came out with an overall light gray cast. A stroke at RGB 110 on a background of RGB 90 inverts the whole image. I burned a whole render cycle learning that.
What worked, in order:
- Convert to grayscale, then compute the histogram.
- If the histogram is strongly bimodal, use Otsu's method to pick the threshold automatically. This handled the cast cases.
- If it is not bimodal, fall back to a fixed 160. In practice nearly all clean line art is bimodal, so the fallback rarely fires.
One more detail that mattered: binarise before any resizing. Resizing a grayscale image and then thresholding smears the decision across interpolated pixels. Threshold first, then resize with nearest-neighbor if you must, and the stroke edges stay hard.
Memory on a 1 vCPU box
The whole pipeline runs on 1 vCPU and 2 GB RAM, no GPU. Binarising is cheap, but the render steps around it are not, and I learned the hard habits there.
One process at a time, never fan out image jobs in parallel on a single core. gc.collect() after every asset. Measure peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss, not average usage, because the ceiling is what gets you OOM-killed. For the heavy render steps I run one file per subprocess so the OS reclaims the memory instead of trusting Python to. And inspect at 60 dpi, not 300, when you only need to confirm the binarisation worked. Rendering a 300 dpi preview of every asset just to eyeball it was the fastest way to eat the RAM budget.
The CSV is where good art still dies
Clean binaries mean nothing if the metadata file is wrong. Adobe wants a 5-column header, Vecteezy wants 4, Dreamstime wants 15, and all three reject a wrong header instantly, before anyone looks at your art. I have had a batch bounce in under a minute because of a header mismatch.
So make_metadata.py writes exactly the header each platform demands and nothing else, built from the folder of finished files. Filenames become title-cased titles by default, which is fine for a first pass but I replace them with real titles from a JSON file before anything gets submitted. And upload_tracker.py keeps a ledger of what is already posted, because the fastest way to get an account flagged is running your batch twice after a partial failure. That happened to me once with 12 duplicates. Never again.
The order that finally worked end to end: binarise and verify the trace node count, run the metadata build, mark each file done in the tracker as it goes out, package the survivors. The 39 finished products in this catalogue, 856 MB of output, all went through that order.
What survived
Three things actually held up under review and trace testing. Otsu thresholding instead of a fixed cutoff. Binarising before resizing, always. And checking trace node counts as a quality gate, because a node count in the low hundreds per icon is a reliable proxy for clean line art, while thousands means gray noise survived somewhere.
Everything else I tried, morphological cleanup, manual despeckle passes, per-image hand-tuned thresholds, either broke on edge cases or cost more time than it saved on a solo schedule.
I packaged the working parts of this into the vector prompt pack. It is the prompt engine plus the pipeline habits from this post, the same ones that produced those 39 products, written up as a practical playbook rather than a theory doc. It is the tool I built from this work, take it or leave it.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)