<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Brenner Cruvinel - compressão</title>
    <subtitle>AI researcher and product designer building at the edge of computer science and mental health.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://brennercruvinel.blog/en/tags/compressao/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://brennercruvinel.blog"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-09-02T00:00:00+00:00</updated>
    <id>https://brennercruvinel.blog/en/tags/compressao/atom.xml</id>
    <entry xml:lang="en">
        <title>The encoder had ignored tune=still since the first build</title>
        <published>2026-09-02T00:00:00+00:00</published>
        <updated>2026-09-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/encoder-ignorou-o-tune-still-desde-o-primeiro-build/"/>
        <id>https://brennercruvinel.blog/en/blog/encoder-ignorou-o-tune-still-desde-o-primeiro-build/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/encoder-ignorou-o-tune-still-desde-o-primeiro-build/">&lt;p&gt;The symptom was a number that didn&#39;t add up. I had a battery of encoders running in parallel: nineteen configurations across AV1, AVIF, x264, x265, WebP and lossy JPEG XL. Even VVC got in, more out of curiosity than hope. Each one calibrated to the same visual quality, to find out whether Nest&#39;s AV1 was the most compact option or just the one closest to home. One of the agents reproduced the Nest build by hand, with the same parameters, and the file came out a different size. Same card, same CRF, same preset, different bytes. I read that in the morning and my first reaction was that the agent had gotten the command wrong.&lt;/p&gt;
&lt;h2 id=&quot;the-probe-failed-and-nobody-said-a-word&quot;&gt;The probe failed and nobody said a word&lt;/h2&gt;
&lt;p&gt;It hadn&#39;t. The &lt;code&gt;forge&lt;/code&gt; asks the encoder for &lt;code&gt;tune=still&lt;/code&gt;, the still-picture mode, which is the right one for a stream where every frame is an independent photo. Before asking, it runs a probe to check whether the encoder accepts that mode. The probe ran without a parameter the mode requires, failed, and the code fell back to a generic mode without telling anyone. The manifest recorded the spec, which said &quot;still&quot;, not what the encoder had actually done. Since the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/&quot;&gt;first build&lt;/a&gt; I had been reading a manifest that didn&#39;t match the binary, and the only place that recorded the mode was the wrong place.&lt;/p&gt;
&lt;p&gt;The difference measured eleven points of visual quality for ten percent more bytes, at the same CRF. The old build was the worst point on the curve: smaller, but ugly in a way that isn&#39;t worth it. And nobody would have caught it by watching the video, because what a frame looks like in the player doesn&#39;t tell you which tune was used. Only bytes against the bytes of an independent encode.&lt;/p&gt;
&lt;p&gt;And it was by chance. The encoder battery existed to answer a different question, and the manual baseline encode was just its reference point. If I had calibrated the battery against the manifest instead of against an encode done from scratch, the bug would have stayed invisible, and every number in the series would have come out ten percent optimistic on bytes and eleven points pessimistic on quality. Now the manifest records the mode the encoder actually used, and the probe passes the right parameter.&lt;/p&gt;
&lt;p&gt;Even after the fix there was a second layer. SVT-AV1 has &lt;a rel=&quot;external&quot; href=&quot;https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Parameters.md&quot;&gt;two modes&lt;/a&gt; you could call &quot;still&quot;: &lt;code&gt;tune=2&lt;/code&gt;, optimized for a structural similarity metric, which is what the fixed probe started picking, and &lt;code&gt;tune=3&lt;/code&gt;, called image quality, which the documentation itself marks as still images only. The second one gives one more point of quality for one percent more bytes. Not much, but it&#39;s the right mode for still pictures, and the recommended build of the full corpus went out with it. The manifest now says which of the two, by number, because &quot;still&quot; on its own already fooled me once.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Variant on the same 2,048 cards&lt;/th&gt;&lt;th&gt;Size&lt;/th&gt;&lt;th&gt;Visual quality&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;JPEG XL repack, lossless&lt;/td&gt;&lt;td&gt;1.12x smaller&lt;/td&gt;&lt;td&gt;same as source&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;AV1 from the first build&lt;/td&gt;&lt;td&gt;3.3x smaller&lt;/td&gt;&lt;td&gt;52&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;AV1 with tune=still&lt;/td&gt;&lt;td&gt;3.0x smaller&lt;/td&gt;&lt;td&gt;63&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;AV1 with inter&lt;/td&gt;&lt;td&gt;2.5x smaller&lt;/td&gt;&lt;td&gt;62&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;AVIF&lt;/td&gt;&lt;td&gt;5.5x smaller&lt;/td&gt;&lt;td&gt;43&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The AVIF row looks like the winner and it isn&#39;t. It sits at a much lower point on the quality curve, so you can&#39;t compare it on the same number. Calibrated to the same quality as AV1, it comes out about 12% smaller, and that left an open question about which backend to use for stills. And inter, my favorite bet, came out bigger than &lt;code&gt;tune=still&lt;/code&gt;. I explain that in the next post, because the configuration was wrong and the problem was something else.&lt;/p&gt;
&lt;p&gt;I won&#39;t table the rest of the nineteen, but three findings stayed with me. x264, the encoder for a 2003 standard, was the fastest inside the quality window by an enormous margin, and VVC, the newest codec on the list, cost a hundred times x264&#39;s time to land in the middle of the table.&lt;/p&gt;
&lt;p&gt;Two different SVT-AV1 presets spat out byte-for-byte identical streams, because the version I use maps both to the same internal mode when the stream is all keyframes. One more case of a parameter I was passing and the encoder was ignoring, this time documented. And lossy JPEG XL, which I expected to compete, only competes at high quality, well above the window that matters for a deck tile. It&#39;s a good codec for a target this corpus doesn&#39;t ask for.&lt;/p&gt;
&lt;h2 id=&quot;capcut-was-throwing-cards-away&quot;&gt;CapCut was throwing cards away&lt;/h2&gt;
&lt;p&gt;I had written in the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/apostas-antes-de-comprimir-as-cartas/&quot;&gt;bets&lt;/a&gt; that speeding up the video cut bytes, because I had done it in CapCut and the file came out half the size. I reproduced it programmatically: speed it up sixty times, export at thirty frames per second in H.264, the way the editor does. The file came out with half the bytes. And half the cards.&lt;/p&gt;
&lt;p&gt;The export samples the sped-up timeline and drops whatever falls between one frame and the next. Of the 2,048 cards, 993 disappeared. What was left got re-encoded to H.264 on top of AV1 that was already lossy. CapCut didn&#39;t compress anything, it threw half the dataset away and called it optimization, and I looked at the file size and believed it. Changing only the fps, without speeding up, doesn&#39;t change a single byte. It&#39;s a timestamp annotation in the container, as &lt;code&gt;ffprobe&lt;/code&gt; had already told me on day one and I hadn&#39;t properly written down.&lt;/p&gt;
&lt;p&gt;The wrong bet yielded a real bug. With an fps other than one in the manifest, the code that fetches a frame by index asked for an instant that didn&#39;t exist, because it assumed ordinal and timestamp were the same thing. Fixed, and tested with the fps I never should have changed. The real gain CapCut was pointing at, redundancy between neighboring frames, does exist. It just has to be measured with inter, without losing cards, and that&#39;s the subject of the next late night.&lt;/p&gt;
&lt;h2 id=&quot;what-else-is-the-manifest-getting-wrong&quot;&gt;What else is the manifest getting wrong&lt;/h2&gt;
&lt;p&gt;I closed the day with two things validated by hash and a bigger doubt than when I started. The encoder mode was a constant I thought was on, and it had been off since the beginning. CapCut was a number I thought was compression, and it was data loss. Both lived in a manifest or an export I had read and accepted without checking against anything.&lt;/p&gt;
&lt;p&gt;If the encoder wasn&#39;t doing what the manifest said, the question left over was what else I was reading as a summary instead of validating on disk. I didn&#39;t know yet that the answer would come in the middle of the night, with a meeting on the calendar.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>My bets before compressing the cards</title>
        <published>2026-09-01T00:00:00+00:00</published>
        <updated>2026-09-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/apostas-antes-de-comprimir-as-cartas/"/>
        <id>https://brennercruvinel.blog/en/blog/apostas-antes-de-comprimir-as-cartas/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/apostas-antes-de-comprimir-as-cartas/">&lt;p&gt;The compression can&#39;t lose anything, even if we have to invent it. I wrote that sentence in the plan, that confidently, after the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/&quot;&gt;first build&lt;/a&gt;. Two lines later I was saying lossless video with the cards sorted by similarity was a path, and further up I had dismissed &lt;code&gt;jpegtran&lt;/code&gt; because &quot;lossless doesn&#39;t count&quot;. I only noticed when I sat down to write the bets and had to define what I was going to measure, and measuring requires saying against what. Lossless relative to what? I had no answer. I had used the same word for three things and hadn&#39;t noticed.&lt;/p&gt;
&lt;h2 id=&quot;why-zstd-can-t-bite-into-a-jpeg&quot;&gt;Why zstd can&#39;t bite into a JPEG&lt;/h2&gt;
&lt;p&gt;A JPEG has already gone through three stages that exhaust the redundancy a general-purpose compressor knows how to find. The image becomes frequency coefficients, the high-frequency coefficients get rounded (that&#39;s where the loss happens), and the result goes through an entropy coder. What comes out is a bitstream where the repeated patterns have already been eaten. zstd looks for exactly that, repeated patterns. It&#39;s like trying to fold a sheet of paper that&#39;s already folded to the limit: it doesn&#39;t get smaller, it just crumples.&lt;/p&gt;
&lt;p&gt;So &quot;lossless&quot; on top of JPEG has three meanings, and only one deserves the name. The first is repacking the JPEG in a way where the inverse operation gives back the original file byte for byte. JPEG XL does this: it takes the already-rounded coefficients and rewrites them with a better entropy coder, keeping enough to undo it. The JPEG&#39;s loss is still there, but the packaging is tighter.&lt;/p&gt;
&lt;p&gt;The second is re-saving the same JPEG with &lt;code&gt;jpegtran&lt;/code&gt; or &lt;code&gt;jpegoptim&lt;/code&gt;. The pixels stay identical and the bytes change. Lossless for whoever looks, not for whoever checks the hash. And if the source is already optimized, like the files from Scryfall, the gain tends to zero.&lt;/p&gt;
&lt;p&gt;The third is what I was proposing without knowing it: decode the pixels and recompress them with another lossless codec, PNG, WebP, or FFV1 video. It looks like the purest meaning and it&#39;s the worst. The new codec receives pixels carrying the JPEG&#39;s rounding noise as if it were signal, and spends bits representing that noise faithfully. Everything points to the result being bigger than the source JPEG, not smaller. I wrote that and still didn&#39;t believe it, hence the bet.&lt;/p&gt;
&lt;p&gt;There&#39;s a fourth thing, which isn&#39;t a meaning but a gap. In 2016, a group from Tianjin University and Microsoft Research Asia published &lt;a rel=&quot;external&quot; href=&quot;https://doi.org/10.1109/TIP.2016.2551366&quot;&gt;&quot;Lossless Compression of JPEG Coded Photo Collections&quot;&lt;/a&gt;: they arrange a collection of JPEG photos as a pseudo-video, compensate the difference between neighboring photos, remove the remaining redundancy in the frequency domain, and still give back every original JPEG without losing a bit. Average savings above 31%. That&#39;s exactly what my invented idea wanted to be, done the right way. Except I couldn&#39;t find an open implementation, and I wasn&#39;t going to write a codec that week. So what I could measure was the ceiling with the tools that exist. The real ceiling, if that paper reproduces on Magic cards, is higher, and I want to say that before giving my number.&lt;/p&gt;
&lt;h2 id=&quot;what-i-bet-and-why&quot;&gt;What I bet and why&lt;/h2&gt;
&lt;p&gt;I wrote it down before running anything, because the last time I published a number without a written bet I had to correct it two days later, and correcting a published number is worse than losing a bet in public.&lt;/p&gt;
&lt;p&gt;Does lossy AV1 video compress much better than loose JPEGs? Yes, that&#39;s what the first build showed and what I had seen with dermoscopy. The more alike neighboring cards are, the more the video gains? Yes, and strongly: the database has 100 thousand printings for 52 thousand illustrations, almost half the cards repeat art that already exists on another, and they all share the same frame. If there&#39;s a video gain, it lives there.&lt;/p&gt;
&lt;p&gt;Does speeding up the video cut bytes? I had dropped one of the MP4s into CapCut, sped it up sixty times, and the file came out half the size. I bet yes, and if it&#39;s true it&#39;s the cheapest item on the list. Does tar plus zstd gain anything? I think not, for the reason in the section above. And does sorting by similarity help with unique cards too? I think not, I&#39;ve seen sorting do nothing on another dataset, but the shared frame leaves me unsure.&lt;/p&gt;
&lt;p&gt;And the one I wanted most: there&#39;s a lossless option better than JPEG, &quot;even if we invent it&quot;. After writing out the three meanings, I changed my mind halfway through the bet. The candidate became JPEG XL, and the lossless video sorted by similarity, the invented idea, is going to lose. The two I most wanted to be true, CapCut and lossless, were the two I could least defend. That&#39;s usually a sign.&lt;/p&gt;
&lt;p&gt;The lossless run went that same night, ten variants on the same 2,048 cards. Tar plus zstd came out one to one, exactly. Re-saving with &lt;code&gt;jpegtran&lt;/code&gt; gave less than one percent, the Scryfall JPEGs are already optimized. The ceiling was the JPEG XL repack, about 11% smaller, with all 2,048 original files rebuilt and validated by hash. Pushing the encoder effort to the maximum gives less than one percent more for six times the time, so the default stayed at medium effort.&lt;/p&gt;
&lt;p&gt;And the invented idea lost badly. FFV1 and x264 without quantization, with the cards sorted by similarity, came out three to four times bigger than the source JPEG. Sorting, in that regime, barely moved the size at all, a fraction of a megabyte on a file of hundreds. A decoded JPEG pixel carries noise and no codec predicts noise, just as I had written that morning without believing it.&lt;/p&gt;
&lt;p&gt;That became Nest&#39;s archive profile: anyone who won&#39;t accept losing anything gets the JPEG XL repack, 3.6 GB for the whole corpus, and every JPEG comes back bit for bit on request. It&#39;s not the number I wanted. It&#39;s the number I can defend with a hash, and I prefer that one. The other bets, the lossy ones, were running while I wrote this. The CapCut one, I was already afraid to look at.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Nest has the potential to be a Magic card archiver</title>
        <published>2026-08-31T00:00:00+00:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/"/>
        <id>https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/">&lt;p&gt;Felipe Navarro, a friend of mine, is building &lt;a rel=&quot;external&quot; href=&quot;https://github.com/ofelipenavarro/pasta.cards&quot;&gt;pasta.cards&lt;/a&gt;, an open source app for managing your collection and Commander decks. It runs locally, keeps everything in SQLite, and to work offline it downloads the art of every Magic card ever printed, in two versions: the full card and just the illustration. On my Mac that came to 83 thousand JPGs and 7 GB. Every card that exists, twice, on my disk, and Finder dragging its feet to open the folder.&lt;/p&gt;
&lt;p&gt;What caught my attention in that number wasn&#39;t the size, disk is cheap. It was the structure. Cards share the frame, the text box, the typography, the mana symbols. And the database next to it shows that almost half of all printings reuse art that already appeared on another card. A loose JPG knows nothing about that. A good catalog points instead of copying, and a good codec doesn&#39;t spend bits on a pixel it can predict from its neighbor. I like both for the same reason.&lt;/p&gt;
&lt;p&gt;I had &lt;a href=&quot;https://brennercruvinel.blog/en/blog/nest-vector-db-soberano/&quot;&gt;Nest&lt;/a&gt; at hand, the single-file vector database I&#39;ve been building this year, and since August it stores images too. The question I asked that morning was far more naive than everything that came after: how does it work, does it turn this into a video? I have 38 thousand images and a database, what does it do with them?&lt;/p&gt;
&lt;h2 id=&quot;does-it-turn-this-into-a-video&quot;&gt;Does it turn this into a video?&lt;/h2&gt;
&lt;p&gt;It really does turn it into a video. Each image becomes a frame of an AV1 stream inside the file, and search runs on vectors taken from the decoded frames, not from the source JPEG. That&#39;s on purpose: the index has to describe what the file serves, and what the file serves is the frame after the codec. Video codecs are the most optimized image compression technology there is, so what looks like a hack is just borrowing decades of other people&#39;s work.&lt;/p&gt;
&lt;p&gt;I had already measured this on skin. Dermoscopy, biopsy slides, pages of old books, the datasets I had on hand from my work at Hoff. What those numbers taught me is that how much something compresses is a property of the domain, not the codec. Same encoder, same settings: dermoscopy compressed 151 times and the book page compressed 4. Smooth skin compresses well because there&#39;s little detail to keep. Small print is the opposite, every edge costs bits.&lt;/p&gt;
&lt;p&gt;A Magic card has small print. It has the same frame on every card, which the codec may or may not exploit. It has reprints of the same art, something dermoscopy doesn&#39;t have. And it has a product constraint that no search metric measures: the deck tile has to look good on the app&#39;s screen. I thought the question was how much it compresses. It was, but not for long.&lt;/p&gt;
&lt;p&gt;Before running anything, I took a calmer look at the raw folder, and it had surprises of its own. Half the weight was the cropped version of the art, and my first idea was to throw it away and crop from the full card when needed. An agent read the app&#39;s code and came back with bad news: the database doesn&#39;t store any crop coordinates, and the app uses the crop directly in the deck tiles. The cropped art is a class of its own, not a derivative. And next to the image folder there was a directory called &lt;code&gt;_redundant&lt;/code&gt;, holding another seven gigs of copies of things that were already there. That one I deleted without mercy.&lt;/p&gt;
&lt;h2 id=&quot;the-first-build&quot;&gt;The first build&lt;/h2&gt;
&lt;p&gt;The pilot with a few hundred cards died with a segfault before doing anything. Nest&#39;s Python binding had been compiled without the flag that says &quot;use the process&#39;s Python, don&#39;t bring your own&quot;, and the Python I use is static, so two interpreters were fighting over the same memory. One flag and done. A small bug, and the kind that makes me suspicious: how many other environment assumptions does the build carry without declaring them?&lt;/p&gt;
&lt;p&gt;With the flag, the full build ran in the background and I opened one of the videos in the player. Cards going by, one per second, forty minutes of cards. My first hypothesis was that the encoder was repeating frames to hold each card on screen, and that this could be cut. One line of &lt;code&gt;ffprobe&lt;/code&gt; killed the hypothesis: one frame per card, and the duration is just a timestamp annotation in the container, it costs no bytes.&lt;/p&gt;
&lt;p&gt;7 GB became 1.9 GB, with image search working on top. It ran on the first try, which in a project of mine is rare enough to make me suspicious. Then I opened the output folder.&lt;/p&gt;
&lt;h2 id=&quot;an-archiver-with-38-mp4s-next-to-it-isn-t-an-archiver&quot;&gt;An archiver with 38 MP4s next to it isn&#39;t an archiver&lt;/h2&gt;
&lt;p&gt;38 video files, a &lt;code&gt;.nest&lt;/code&gt; with the indexes, a JSON manifest and a cache directory. Four kinds of file to serve one thing. Nest exists to be the database in one file, and what came out was a database with forty things next to it. It worked, the way a piece of furniture assembled with leftover screws works. The point of Nest was getting lost there, and compression wasn&#39;t even the problem. Nest has the potential to be an archiver for Magic cards, for maps, for any image dataset with a database beside it. But an archiver is a file, not a folder.&lt;/p&gt;
&lt;p&gt;The second question came from the model. The script used CLIP because that was what the &lt;code&gt;forge&lt;/code&gt; had, and I didn&#39;t really know what CLIP did with a card. I looked into it: it looks at the art and ignores the printed text. For a Magic card that means looking at the less useful half, because what identifies the card is the name written at the top. I asked for a search on Hugging Face and GitHub of the open multimodal models available that month, and the answer was a Tencent family called &lt;a rel=&quot;external&quot; href=&quot;https://huggingface.co/tencent/WeMM-Embedding-2B&quot;&gt;WeMM&lt;/a&gt;, which reads text inside images, plus SigLIP and Jina. I already had the two-billion-parameter WeMM downloaded for another reason.&lt;/p&gt;
&lt;p&gt;In a comparison with 1,500 cards and sixty queries like &quot;the art of such-and-such card&quot;, CLIP found the right card one time in four. WeMM got nine in ten, SigLIP came close, Jina landed in the middle. WeMM&#39;s price is being about thirty times slower than CLIP, and running it over the whole corpus would take almost a day on my machine. So the comparison stayed on the sample, and the full corpus kept CLIP for now.&lt;/p&gt;
&lt;p&gt;What came out of this wasn&#39;t &quot;switch to WeMM&quot;. It was that the model can&#39;t be a constant at the top of a script. It has to be a list of presets inside the engine, and whoever builds picks which one, or which ones, at build time. And the video has to fit inside the file, not next to it. Before either of those two things, I wanted to know how far I could compress without losing anything. Even if we had to invent it.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Image compression for computer vision: February&#39;s proposal and September&#39;s answer</title>
        <published>2026-03-12T00:00:00+00:00</published>
        <updated>2026-03-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/compressao-imagem-computer-vision/"/>
        <id>https://brennercruvinel.blog/en/blog/compressao-imagem-computer-vision/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/compressao-imagem-computer-vision/">&lt;p&gt;In February 2026 I wrote a proposal for dermML&#39;s dermatology dataset, which was already stored as AV1 video: sort the images by visual similarity before encoding, so neighbouring frames would look alike and the codec could take advantage of it. I estimated compression would go from about 30 times to 50 to 80 times. I did not implement it at the time.&lt;/p&gt;
&lt;p&gt;In September I tested the same idea in Nest, with Magic cards, and it did not survive the real corpus. This post brings both ends together: what I was reading, what I proposed and what the data answered.&lt;/p&gt;
&lt;h2 id=&quot;what-i-was-reading&quot;&gt;What I was reading&lt;/h2&gt;
&lt;p&gt;Two readings convinced me that lossy compression of training data was safe and that video was a real dataset format.&lt;/p&gt;
&lt;p&gt;The first was a two-part series by Kaleigh Mentzer at Granica. In &lt;a rel=&quot;external&quot; href=&quot;https://www.granica.ai/blog/using-image-compression-to-improve-computer-vision-part-1&quot;&gt;part 1&lt;/a&gt;, from November 2023, Food101 classification with a Vision Transformer barely lost accuracy until compression went past about 70%, and segmentation models on Cityscapes could be trained on data compressed by 97%, using only 3% of the original storage, with no relevant drop in mIoU. Their explanation is that lossy compression throws away high frequencies first, and the model depends little on them too; in a test with 32 images, perturbing high-frequency DCT coefficients barely moved the prediction. In &lt;a rel=&quot;external&quot; href=&quot;https://www.granica.ai/blog/using-image-compression-to-improve-computer-vision-part-2&quot;&gt;part 2&lt;/a&gt;, with a fixed budget of about 0.5 GB, the original JPEGs fit 9,985 images, and compressing to Butteraugli distance 10 fit about 5.6 times more. With fewer than 100 images per class, more worse images beat fewer better ones by more than 10 accuracy points.&lt;/p&gt;
&lt;p&gt;The second was Hugging Face. In the &lt;a rel=&quot;external&quot; href=&quot;https://huggingface.co/blog/video-encoding&quot;&gt;post on encoding robotics datasets as video&lt;/a&gt;, from August 2024, datasets ended up at 14% of their original size on average, reaching 0.2% in the best case, and loading several consecutive frames took 25% to 50% of the time of loading the same frames from PNG. &lt;a rel=&quot;external&quot; href=&quot;https://huggingface.co/blog/lerobot-datasets-v3&quot;&gt;LeRobotDataset v3.0&lt;/a&gt;, from September 2025, went further: several episodes per file, metadata in Parquet and streaming straight from the Hub.&lt;/p&gt;
&lt;h2 id=&quot;the-proposal&quot;&gt;The proposal&lt;/h2&gt;
&lt;p&gt;The problem I saw in dermML was frame order. Images went into the video in file system order, alphabetical by name:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Frame 0: ISIC_0024306.jpg  (dark melanoma, light skin)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Frame 1: ISIC_0024307.jpg  (benign nevus, dark skin)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Frame 2: ISIC_0024308.jpg  (dermatofibroma, medium skin)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AV1 encodes a frame from its difference to a reference frame. If the neighbour is a different lesion on different skin, the difference is large and inter-frame prediction does not help. My reading was that the 30 times came almost entirely from compression inside each image.&lt;/p&gt;
&lt;p&gt;The idea: DermLIP embeddings, with 512 dimensions, already existed in the indexing pipeline. Finding the order that minimizes the distance between consecutive frames is a travelling salesman problem in embedding space, intractable to solve exactly for 58 thousand points, so the proposal used the simplest heuristic, greedy nearest neighbour:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot; &gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;import&lt;/span&gt;&lt;span&gt; numpy&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; as&lt;/span&gt;&lt;span&gt; np&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;from&lt;/span&gt;&lt;span&gt; sklearn&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;metrics&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pairwise&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; import&lt;/span&gt;&lt;span&gt; cosine_distances&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#586E75, #93A1A1);font-weight: bold;&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; sort_by_visual_similarity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;embeddings&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    order&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; visited&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; current&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D33682, #D33682);&quot;&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D33682, #D33682);&quot;&gt;0&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D33682, #D33682);&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;    for&lt;/span&gt;&lt;span&gt; _&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; range&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;len&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;embeddings&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D33682, #D33682);&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        dists&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; =&lt;/span&gt;&lt;span&gt; cosine_distances&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;embeddings&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;current&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;current&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D33682, #D33682);&quot;&gt; 1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; embeddings&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D33682, #D33682);&quot;&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        dists&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;list&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;visited&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; float&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;inf&lt;/span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        current&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; int&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;np&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;argmin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;dists&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        visited&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;current&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        order&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;current&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;    return&lt;/span&gt;&lt;span&gt; order&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the estimate I wrote down:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Scenario&lt;/th&gt;&lt;th&gt;Estimated compression&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;File system order&lt;/td&gt;&lt;td&gt;~30x&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Sorted by similarity&lt;/td&gt;&lt;td&gt;~50 to 80x&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Sorted, with CRF and GOP tuning&lt;/td&gt;&lt;td&gt;~60 to 100x&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;For the full dataset, that would take the 52 GB from something like 1.7 GB to somewhere between 650 MB and 1 GB. The proposal already called for &lt;code&gt;keyint=250&lt;/code&gt; and &lt;code&gt;scd=0&lt;/code&gt; in SVT-AV1, to turn off scene cut detection and avoid an extra keyframe at every new lesion, and for one more SQLite column to map each sorted frame back to its original image. The validation plan was to encode with and without sorting, compare size and quality, and check that search still found the same things.&lt;/p&gt;
&lt;h2 id=&quot;what-september-answered&quot;&gt;What September answered&lt;/h2&gt;
&lt;p&gt;In August Nest started storing images, and in September the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/&quot;&gt;Magic card series&lt;/a&gt; became the test I had not done in February.&lt;/p&gt;
&lt;p&gt;The first round with inter-frame prediction came out bigger than without it. The reason was exactly what the proposal had warned about: scene cut detection was on, and one different card after another is a scene cut on every frame. With it off, on a corpus of almost three thousand reprints of the same art, prediction made the file 29% smaller. Shuffling the reprints cost only two points, because part of the gain came from the frame every card shares. And that 29% compares bytes only, not equivalent quality.&lt;/p&gt;
&lt;p&gt;On the real corpus, of unique cards, the sorted build and the plain build came out at the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/no-final-do-dia-ta-a-mesma-coisa-1-3-gb/&quot;&gt;same 1.3 GB&lt;/a&gt;. Nest tests both ways per segment before deciding, and it vetoed prediction in all of them. There was nothing to predict: the next card is another card, with another artwork. In the lossless regime it was worse: FFV1 and x264 without quantization, with the cards sorted, came out &lt;a href=&quot;https://brennercruvinel.blog/en/blog/apostas-antes-de-comprimir-as-cartas/&quot;&gt;three to four times bigger&lt;/a&gt; than the source JPEGs, and sorting barely moved the size.&lt;/p&gt;
&lt;h2 id=&quot;what-remains&quot;&gt;What remains&lt;/h2&gt;
&lt;p&gt;The thesis from the readings still stands: training data tolerates a lot of compression, and video is a good dataset container. What fell was my extrapolation. Sorting by similarity only pays off when the corpus has near-duplicate images, like card reprints or consecutive robot frames. In dermoscopy, each image is a different lesion from a different person, and the estimate of 50 to 80 times was probably optimistic for the same reason as the card one.&lt;/p&gt;
&lt;p&gt;The other lesson is about method. Compression is a property of the domain before it is a property of the codec, and the only way to know is to encode the real corpus and look at the file, not the summary.&lt;/p&gt;
</content>
        
    </entry>
</feed>
