<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jakub Kopańko</title>
    <description>The latest articles on DEV Community by Jakub Kopańko (@pcktm).</description>
    <link>https://dev.to/pcktm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F624330%2Fee75ff5f-4b55-4561-a367-05676eb05cc8.jpeg</url>
      <title>DEV Community: Jakub Kopańko</title>
      <link>https://dev.to/pcktm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pcktm"/>
    <language>en</language>
    <item>
      <title>Always Winning at Juwenalia: Hacking Rewards from the Festival App's Mini-Games</title>
      <dc:creator>Jakub Kopańko</dc:creator>
      <pubDate>Mon, 26 May 2025 10:45:32 +0000</pubDate>
      <link>https://dev.to/pcktm/always-winning-at-juwenalia-hacking-rewards-from-the-festival-apps-mini-games-ngm</link>
      <guid>https://dev.to/pcktm/always-winning-at-juwenalia-hacking-rewards-from-the-festival-apps-mini-games-ngm</guid>
      <description>&lt;p&gt;Ah, Juwenalia. If you're a student in Poland, particularly here in Kraków, you know exactly what that means. It's that time of the year when the city transforms into a student playground. Uni classes are suspended and replaced by days filled with concerts, events, and a significant amount of beer.&lt;/p&gt;

&lt;h4&gt;
  
  
  The backstory
&lt;/h4&gt;

&lt;p&gt;Last year, alongside the usual festivities, the organizers introduced "JuweAppka". Developed by a third-party contractor, the app promised drops of Juwenalia merchandise — tote bags, t-shirts, in-house Juwe beer, and other cool prizes. The mechanic was simple — each device could claim a single "lottery ticket" per drop. At a specific time of the day, you'd tap a button in the app, and if luck was on your side, you'd win a prize tied to your device ID.&lt;/p&gt;

&lt;p&gt;This presented an interesting opportunity. Since prizes were linked only to a device ID, and reinstalling the app would generate a new ID, I realized I could automate this process. A quick peek with &lt;code&gt;mitmproxy&lt;/code&gt;, revealed the simplicity of the backend. The endpoints were completely unprotected - they consumed the device ID and returned either a the details of the prize if you won, or an empty array if you didn't. There was no real complex authentication or validation. &lt;/p&gt;

&lt;p&gt;I quickly whipped up a small Node.js script that would generate random device IDs, send a request to the lottery endpoint at designated time and see if it won. By running this script many times with different IDs, I could accumulate numerous, real, prizes across these virtual devices. &lt;/p&gt;

&lt;p&gt;But how to collect all these prizes on my actual phone? This is where &lt;code&gt;mitmproxy&lt;/code&gt; came back into play, this time for its ability to modify traffic in-flight. I could intercept the request the real app made to fetch the list of won prizes for my &lt;em&gt;real&lt;/em&gt; device ID. Then, I could modify the response from the server, injecting all the prize data I had collected from my multitude of virtual devices. The app, none the wiser, would display this combined list of prizes, allowing me to claim them all at once. It felt less like cheating and more like... efficient prize collection, leveraging the system's design. After all, anyone &lt;em&gt;could&lt;/em&gt; have reinstalled the app repeatedly; I just automated the process.&lt;/p&gt;

&lt;p&gt;Fast forward to this year. The JuweAppka is back, but this time it's an in-house production, built with React Native and backed by Firebase. The simple lottery is gone, replaced by more engaging mini-games — think cookie clicker mechanics or falling fruit challenges, complete with leaderboards. The top players at the end of the day win the coveted prizes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb2a58aej0e8roeqi4gcr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb2a58aej0e8roeqi4gcr.webp" width="800" height="666"&gt;&lt;/a&gt;&lt;br&gt;A screenshot showing one of the mini-games.
  &lt;/p&gt;

&lt;p&gt;Crucially, they've implemented an account system. This is a significant change. While it prevents the simple device ID spoofing of last year, it also simplifies the prize collection. I can now potentially manipulate my score within an emulator and then simply log into my account on my physical phone to collect any prizes won, without needing to spoof the final prize list request.&lt;/p&gt;

&lt;p&gt;My initial thought process was straightforward: the app must send my score to the server via a POST request to update the leaderboard. If I could intercept this request using &lt;code&gt;mitmproxy&lt;/code&gt;, I could simply edit the score value before it reached the server, artificially boosting my rank on the leaderboard. Easy, right?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;While I'll walk you through my process, this post is for educational purposes and isn't a step-by-step guide for replication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  The setup
&lt;/h4&gt;

&lt;p&gt;Ok, so let's get into this. Here's the list of ingredients you'll need to follow along:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Android Emulator&lt;/strong&gt;: You'll need a &lt;strong&gt;rooted&lt;/strong&gt; Android emulator, preferably running a release image with Google Play services installed. I would target older, but still supported Android versions. Installing Magisk on the emulator is highly recommended. You can use &lt;a href="https://github.com/shakalaca/MagiskOnEmulator" rel="noopener noreferrer"&gt;MagiskOnEmulator&lt;/a&gt; to do this.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mitmproxy&lt;/code&gt; and &lt;code&gt;frida&lt;/code&gt; tools: Ensure you have both installed on your host machine. For mitmproxy you'll also need to install its CA cert into the Android system certificate store on your emulator. Magisk makes this significantly easier with a module. You'll also need to configure your emulator to route all traffic through the proxy running on your machine. While there are multiple ways to approach MITM attacks, I generally add the CA to the system store and use Frida to bypass SSL certificate pinning anyway as it's often surprisingly effortless. Magisk is also invaluable here, use &lt;a href="https://github.com/ViRb3/magisk-frida" rel="noopener noreferrer"&gt;magisk-frida&lt;/a&gt; module.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note that the following can also be done on a non-rooted, stock Android device using &lt;a href="https://frida.re/docs/gadget/" rel="noopener noreferrer"&gt;Frida Gadget&lt;/a&gt; patched directly into the APK.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  The execution
&lt;/h4&gt;

&lt;p&gt;The goal was simple: play a game, capture the network traffic when the score was submitted, and identify the relevant request.&lt;/p&gt;

&lt;p&gt;I fired up one of the mini-games, played for a bit to get a non-zero score, and finished the round. As expected, &lt;code&gt;mitmproxy&lt;/code&gt; immediately showed network activity. Sifting through the requests, I quickly spotted a &lt;code&gt;POST&lt;/code&gt; request sent a Firebase endpoint, likely containing my score update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;https://REDACTED_URL/result&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2.0&lt;/span&gt;
&lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer REDACTED_JWT&lt;/span&gt;
&lt;span class="na"&gt;authtoken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;f9c32a74560ef09523a21f7798c836dcc17448a7f0bc7b03d121015779922bf4&lt;/span&gt;
&lt;span class="na"&gt;game&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lap_co_leci&lt;/span&gt;
&lt;span class="na"&gt;content-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="na"&gt;content-length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;12&lt;/span&gt;
&lt;span class="na"&gt;accept-encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gzip&lt;/span&gt;
&lt;span class="na"&gt;user-agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;okhttp/4.9.2&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My initial thought was confirmed: there's a &lt;code&gt;score&lt;/code&gt; field in the JSON payload. Modifying that with the proxy is trivially easy. But then I noticed it — the &lt;code&gt;authtoken&lt;/code&gt; header. There already is the Authorization JWT header and this looks... different. A long string of hex characters.&lt;/p&gt;

&lt;p&gt;After playing the game a couple more times with different scores, I observed a pattern: the &lt;code&gt;authtoken&lt;/code&gt; value changed whenever the score changed, but two games with the &lt;em&gt;exact same score&lt;/em&gt; resulted in the &lt;em&gt;exact same authtoken&lt;/em&gt;. This strongly suggested the &lt;code&gt;authtoken&lt;/code&gt; was not a random session token, but rather a value derived directly from the score itself, likely some form of hash. Given its length and appearance, a SHA-256 hash seemed like a prime candidate.&lt;/p&gt;

&lt;p&gt;This is a common, albeit often insufficient, security pattern. The server likely recalculates the hash on its end using the received score and a secret key (a salt or a prefix) and compares it to the &lt;code&gt;authtoken&lt;/code&gt; provided by the client. If they match, the score is considered valid.&lt;/p&gt;

&lt;p&gt;My simple plan of just changing the score in the request body hit a roadblock. If I just changed the score, the &lt;code&gt;authtoken&lt;/code&gt; wouldn't match the server's calculation, and the request would be rejected. I needed to generate a &lt;em&gt;valid&lt;/em&gt; &lt;code&gt;authtoken&lt;/code&gt; for my desired, artificially high score.&lt;/p&gt;

&lt;p&gt;This is precisely where Frida shines — it allows us to peer inside the running application process. Since the app is performing this hashing operation locally before sending the request, I could use Frida to hook into the native crypto functions being called by the React Native application. This approach is quite elegant in that &lt;strong&gt;we don't have to modify or even read messy compiled Hermes bytecode — we modify the platform instead!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By intercepting the call to the hashing function (like a SHA-256 implementation), I could log the input data being fed into it — this input would likely be the score combined with the secret salt/prefix. Once I had that input format, I could replicate the hashing process myself and generate valid &lt;code&gt;authtoken&lt;/code&gt; for any score I wanted.&lt;/p&gt;

&lt;p&gt;Writing a Frida script to hook native crypto functions is surprisingly straightforward once you know what to look for. Frida injects a full JavaScript engine into the target process, giving you incredible power to inspect, modify, and trace function calls at runtime.&lt;/p&gt;

&lt;p&gt;Here's the script I wrote that got injected into the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bytesToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;str&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Find the class that we want to hook&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;MessageDigest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;java.security.MessageDigest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Replace native implementation with our JS hook&lt;/span&gt;
  &lt;span class="nx"&gt;MessageDigest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;overload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;implementation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAlgorithm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[*] MessageDigest.update called with SHA-256 algorithm.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;stringInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytesToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[*] SHA-256 input (string): &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;stringInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Call the original method&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[*] MessageDigest hooks installed for SHA-256.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immediately after injecting the script and playing the game, the console output from Frida revealed the secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;frida &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; com.kuvus.juwenalia_krakowskie &lt;span class="nt"&gt;-l&lt;/span&gt; unpin.js &lt;span class="nt"&gt;-l&lt;/span&gt; sha_hook.js
&lt;span class="go"&gt;Spawning `com.kuvus.juwenalia_krakowskie`...
Spawned `com.kuvus.juwenalia_krakowskie`. Resuming main thread!
&lt;/span&gt;&lt;span class="gp"&gt;[Android Emulator 5554::com.kuvus.juwenalia_krakowskie ]-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; MessageDigest hooks installed &lt;span class="k"&gt;for &lt;/span&gt;SHA-256.
&lt;span class="go"&gt;(...)

[*] MessageDigest.update called with SHA-256 algorithm. 
[*] SHA-256 input (string): jsjkek-91
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was! The input string to the SHA-256 hash was the literal string "jsjkek-" concatenated with the score.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsjkek-91&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;f9c32a74560ef09523a21f7798c836dcc17448a7f0bc7b03d121015779922bf4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the key. With &lt;code&gt;mitmproxy&lt;/code&gt; to intercept and modify the request body (setting the desired high score) and the ability to generate the correct &lt;code&gt;authtoken&lt;/code&gt; using the discovered prefix, I could now, in theory, submit any score I wanted to the leaderboard. This effectively bypasses the score validation mechanism and allows for arbitrary score submission.&lt;/p&gt;

&lt;p&gt;As for whether or not I actually used this method to climb the leaderboard and claim prizes... well, I've been advised not to comment on that.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>programming</category>
      <category>security</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The $75 Photoshop Plugin That Cost Me... Nothing (I Built It Instead)</title>
      <dc:creator>Jakub Kopańko</dc:creator>
      <pubDate>Wed, 07 May 2025 12:59:33 +0000</pubDate>
      <link>https://dev.to/pcktm/the-75-photoshop-plugin-that-cost-me-nothing-i-built-it-instead-3ieb</link>
      <guid>https://dev.to/pcktm/the-75-photoshop-plugin-that-cost-me-nothing-i-built-it-instead-3ieb</guid>
      <description>&lt;p&gt;I was scrolling through Instagram Reels recently when an ad, presented as a "tutorial," caught my eye. It was for a Photoshop plugin called "DITHERTONE Pro," showcasing a rather striking visual effect – lines that seemed to follow an image's contours, giving it a distinct, almost cyberpunk aesthetic. It genuinely looked impressive. Then, the price appeared: $75.&lt;/p&gt;

&lt;p&gt;That figure made me pause. Not because I'm against paying for well-crafted software, but because the effect itself felt incredibly familiar. It was a strong sense of déjà vu, like I'd seen this exact visual style generated somewhere else, in a context far removed from polished, commercial plugins.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ghost of Glitch
&lt;/h3&gt;

&lt;p&gt;My mind immediately jumped back a few years. I used to be quite involved in the datamoshing and glitch art scene – not so much these days, but I still have a soft spot for creative data bending. And in that world, a collection of Processing scripts by Tomasz Sulej, called "GenerateMe," was pretty well-known. He'd developed a range of tools, from pixel sorting to very accurate VHS tape emulations.&lt;/p&gt;

&lt;p&gt;And wouldn't you know it, one of those scripts, "FM Modulation," produced an effect that was practically a dead ringer for what "DITHERTONE Pro" was selling. It created those same intricate, flowing lines that followed the image's inherent structure. The core aesthetic was undeniably there, born from a free, open-source script.&lt;/p&gt;

&lt;p&gt;Now, those GenerateMe scripts were (and are) clever pieces of work. But using them today isn't exactly a seamless experience. They are, after all, products of a slightly different era in creative coding.&lt;/p&gt;

&lt;p&gt;For instance, you'll likely need to download a specific, older version of Processing to ensure they run correctly, as they often haven't been updated for the latest Processing releases. The user experience is also very minimal – typically just an image preview. Controlling the FM Modulation script involved moving the mouse cursor to adjust parameters, which, while interesting, isn't the most precise method. And if you wanted to process your own image? That meant editing the source code to change the input file path. So, while not ancient, they require a fair bit of fiddling and aren't what you'd call user-friendly by today's standards – definitely a bit of a pain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs7oe3o8xgwk4i8phl0km.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs7oe3o8xgwk4i8phl0km.png" alt="Example result" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Not Rebuild It for the Web? And Make it Free.
&lt;/h3&gt;

&lt;p&gt;So, there's this cool effect, locked away in an old script that’s a hassle to use, or available via a $75 plugin. The plugin undoubtedly offers a more polished experience and likely more features, but for that core, captivating FM modulation effect? The price felt steep, especially knowing its open-source roots.&lt;/p&gt;

&lt;p&gt;"I could probably build that," I thought. The idea started to form: take the essence of that FM Modulation script and rewrite it for the web. Make it accessible, easy to use, and, importantly, free for anyone to try. I'd done something similar before with an old AVI glitch tool that messed with frame indexes (I wrote about that here), and the challenge was appealing. If the core logic was sound, why not modernize its delivery?&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing the Right Weapons
&lt;/h3&gt;

&lt;p&gt;Alright, so the goal was set: bring FM modulation to the web. The next question was how. My gut feeling, based on what I knew about the FM modulation algorithm – lots of loops, complex math, angles, and bits of signal processing theory – was that a straightforward JavaScript implementation might not cut it. Especially for larger images, I had visions of the browser grinding to a halt, fans spinning up, and a generally laggy, unpleasant experience.&lt;/p&gt;

&lt;p&gt;This is where the idea of using Rust compiled to WebAssembly (WASM) came in. Rust offers near-native performance, which is perfect for CPU-intensive tasks like heavy image processing. WASM allows that Rust code to run in the browser at speeds that JavaScript alone often can't match for these kinds of operations. Plus, the plan was to offload this heavy lifting to a Web Worker, keeping the main browser thread free and the UI responsive. It felt like the right architectural choice to deliver both the complex visual effect and a smooth user experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Build: Rust to WASM Delivers
&lt;/h3&gt;

&lt;p&gt;With the tech stack decided, it was time to get to work. I set up the Rust workspace: fm_core for the actual image processing, and a separate module to interface with wasm-pack for the WebAssembly compilation. I have to say, the experience of getting Rust code to run on the web via WASM was surprisingly smooth. The Rust community has built an impressive ecosystem here.&lt;/p&gt;

&lt;p&gt;Honestly, I was expecting more of a battle – the usual snags and configuration issues when you're trying to make different technologies play nice. But wasm-pack handled its part elegantly, and the necessary glue code between JavaScript and the compiled WASM module was straightforward. Getting the Rust logic up and running in the browser happened with minimal friction, which was a very welcome development and speaks volumes about the maturity of Rust's WASM tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vibe coding critique
&lt;/h3&gt;

&lt;p&gt;Now, about AI tools. I did use GitHub Copilot in agent mode here and there, mostly for helping translate some of the gnarlier math from Processing to Rust. It can be a great assistant for many tasks, and that agentic workflow has great potential for speeding things up significantly. But let's be brutally honest: AI isn't writing your next masterpiece, and it sure as hell isn't a substitute for actual engineering skill. The current crop of "AI can code better than engineers" tech bros and founders, churning out their AI-powered software rot machines, are in for a rude awakening. You can't just prompt your way to a solid, maintainable, secure application – that takes understanding, discipline, and a grasp of fundamentals that these tools simply don't possess. Relying on AI as a crutch without that oversight is just asking for a dumpster fire of a codebase down the road.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Payoff
&lt;/h3&gt;

&lt;p&gt;After all that wrestling with Rust, WASM, and Web Workers, I'm happy to say the project came together. I'm calling it the "Image Frequency Modulation tool," and you can try it out right now at &lt;a href="https://modulate.kopanko.com/?utm_source=devto" rel="noopener noreferrer"&gt;modulate.kopanko.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The front end is built with React and shadcn/ui, giving it a clean and modern interface. When you upload an image and tweak the parameters, the actual heavy lifting – the FM modulation processing – is done by the Rust/WASM module running inside a Web Worker. This means the image processing happens asynchronously, off the main browser thread, so the UI stays snappy and responsive even while your image is being transformed. It’s designed to do that one core thing – generate those cool FM modulation effects – and do it well, without any fuss. And, of course, it's completely free to use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ogezzrawz9w8zsxklv8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ogezzrawz9w8zsxklv8.webp" alt="A screenshot of the tool's interface" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion: So, About That $75 Plugin...
&lt;/h3&gt;

&lt;p&gt;And there you have it. From a slightly overpriced Instagram ad to a free, performant web tool. It was a fun challenge, a good excuse to dive deeper into Rust and WASM, and ultimately, a way to bring a cool visual effect to more people without a price tag.&lt;/p&gt;

&lt;p&gt;While the commercial plugin might offer a broader suite of features for professional workflows, if you're just looking to experiment with this specific FM modulation aesthetic, my little tool gets the job done. Sometimes, the satisfaction of building it yourself (and then sharing it) is worth more than any plugin.&lt;/p&gt;

&lt;p&gt;So go ahead, give it a spin &lt;a href="https://modulate.kopanko.com/?utm_source=devto" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Upload an image, play with the settings, and see what kind of visual weirdness you can create. Maybe you'll save yourself $75 in the process.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>rust</category>
      <category>design</category>
    </item>
    <item>
      <title>Finding a Needle in the Image Stack: A Deep Dive into Content-Based Image Retrieval</title>
      <dc:creator>Jakub Kopańko</dc:creator>
      <pubDate>Tue, 28 Mar 2023 22:37:29 +0000</pubDate>
      <link>https://dev.to/pcktm/finding-a-needle-in-the-image-stack-a-deep-dive-into-content-based-image-retrieval-1g0o</link>
      <guid>https://dev.to/pcktm/finding-a-needle-in-the-image-stack-a-deep-dive-into-content-based-image-retrieval-1g0o</guid>
      <description>&lt;p&gt;As an undergraduate with a keen interest in computer vision, I found myself fascinated by the subject of digital image processing, particularly Content-Based Image Retrieval (CBIR). When my professor encouraged me to work on a project of my choice, I eagerly delved into the fascinating field of digital image processing beyond the standard lab exercises. While my peers were focused on their routine lab work, I was immersed in the world of CBIR. In this blog post, I will share the knowledge I gained, from conventional methods to state-of-the-art neural networks. I'll also discuss how these techniques can help you find the right image every time. So join me on an enlightening journey as I reveal what I've learned!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---3RLbEQA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3x2rb5tevx2taxqqrro3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---3RLbEQA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3x2rb5tevx2taxqqrro3.png" alt="Pictures of dogs on a beach" width="880" height="122"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is important to emphasize that the primary goal of all content-based image retrieval techniques is to transform images into numerical representations, typically in the form of n-dimensional vectors. This allows us to compare and manipulate images using mathematical operations. By using various distance metrics and clustering algorithms, we can assess the similarity between images and categorize them into relevant groups.&lt;/p&gt;

&lt;p&gt;The code for my experiments can be found at &lt;a href="https://github.com/pcktm/image-retrieval-experiments"&gt;https://github.com/pcktm/image-retrieval-experiments&lt;/a&gt;, and I wholeheartedly encourage you to check it out and explore the fascinating world of image retrieval!&lt;/p&gt;

&lt;h3&gt;
  
  
  Classical methods
&lt;/h3&gt;

&lt;p&gt;I decided to start with the classical methods of CBIR, which were invented in the early days of digital image processing in the 1990s and early 2000s. These methods are based on extracting global features from images, such as color, texture, and other statistics. Global features represent the overall characteristics of an image and can be used to describe and compare images. Some of the most common global features used in classical CBIR methods are color histograms, texture features based on Gabor filters, and statistical features such as mean, variance, and entropy.&lt;/p&gt;

&lt;p&gt;Excited to get started on my project, I began by following an "&lt;a href="https://arxiv.org/pdf/1608.03811v1.pdf"&gt;Content-based image retrieval tutorial&lt;/a&gt;" by Joani Mitro. The report covered classic CBIR techniques such as color histogram quantization and RGB means and moments, which I was able to implement with ease. However, I hit a roadblock when it came to the next step: implementing a color auto-correlogram.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hQFAg6_N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4yrif6be0ga56vokrvhx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hQFAg6_N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4yrif6be0ga56vokrvhx.png" alt="Histograms" width="880" height="444"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bdBtoVMR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qoj38ycbglhj6o6xkmy6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bdBtoVMR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qoj38ycbglhj6o6xkmy6.png" alt="HSV Channels" width="880" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A color auto-correlogram is a powerful technique invented by Huang, J., Ravi Kumar, S., Mitra, M. et al. in their seminal paper, "&lt;a href="https://link.springer.com/article/10.1023/A:1008108327226"&gt;Spatial Color Indexing and Applications&lt;/a&gt;" published back in 1999. In short, a color auto-correlogram is a tool that allows us to measure the spatial correlation between color values in an image. For example, if we compute the color auto-correlogram for a given image and find that the value for the red-blue pair is high, this indicates that red is likely to be found near blue in the image. This information can be used to identify other images that have similar color patterns and texture characteristics.&lt;/p&gt;

&lt;p&gt;The problem was that when it was time for implementation of the color auto-correlogram, I quickly found that the &lt;a href="https://github.com/raj1603chdry/CSE3018-Content-Based-Image-and-Video-Retrieval-Lab/tree/master/WEEK4%20"&gt;only implementation available&lt;/a&gt; was in MATLAB. Unfortunately, I was working on this project in Python using OpenCV2, and I had no knowledge of MATLAB. This left me with the monumental job of rewriting the entire algorithm in Python.&lt;/p&gt;

&lt;p&gt;It took literally days of hard work and countless hours of debugging, but I finally got it to almost work with the help of Copilot at one point, except instead of a vector of 512 values, I got a vector of insane size. After digging around, I found that MATLAB has a function that palletizes the image to just 64 colors. Unfortunately, OpenCV2 has no equivalent, and I couldn't just trim the least significant bits or use k-means clustering to create a palette for each image, because the resulting vectors wouldn't be comparable across images.&lt;/p&gt;

&lt;p&gt;After some trial and error, I finally came up with a solution: I took a PNG image of the Windows 2000 or something color palette and used it to palletize all my images. And finally, it clicked! The search began to work, and I was able to retrieve images that had a similar color distribution to mine!&lt;/p&gt;

&lt;p&gt;Now, let's put these extracted features to work by testing different distance metrics and searching for images in the &lt;a href="https://shannon.cs.illinois.edu/DenotationGraph/"&gt;flickr30k&lt;/a&gt; dataset using a single global feature vector that combines all of them. Here are some of the results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tI3HaOJi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l7psvobcbmj7nd0s41gt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tI3HaOJi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l7psvobcbmj7nd0s41gt.png" alt="Some search results" width="880" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--daaST7aF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3u21hhmef8l9ochbizba.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--daaST7aF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3u21hhmef8l9ochbizba.png" alt="Some search results" width="880" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Local features based methods
&lt;/h3&gt;

&lt;p&gt;Next, I delved into local feature-based methods, specifically examining the SIFT and ORB keypoint detectors and descriptor generators. I chose these algorithms because they are seamlessly integrated into OpenCV2 and quite easy to use, making them ideal for what I wanted to explore.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--i0_CQ4-j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4im32wnvi1xjncfw97h5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--i0_CQ4-j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4im32wnvi1xjncfw97h5.jpg" alt="SIFT and ORB keypoints and features" width="880" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The bag-of-words approach is a popular method for representing local features in image retrieval. It involves clustering local descriptors (in this case, SIFT or ORB descriptors) into a set of visual words using K-means clustering. A histogram-like representation of the image is then created by assigning each descriptor to the nearest visual word. These histograms are called Bag of Visual Words vectors, which can then be used for similarity searches using plain old distance metrics.&lt;/p&gt;

&lt;p&gt;Okay, so the term Frequency - Inverse Document Frequency (TF-IDF) technique was originally invented for natural language processing, but it turns out that it can be just as useful for image applications! Essentially, TF-IDF is a weighting scheme that assigns weights to words in a text document based on how often they occur in that document and how often they occur across all documents in a corpus. In the context of image retrieval, we can think of an image as a "document" and the visual features as "words". By calculating the TF-IDF scores for each feature, we can identify the most "distinctive" features that are most likely to be useful in distinguishing one image from another.&lt;/p&gt;

&lt;p&gt;To give an example of how IDF works, let's consider the visual word "sky". Imagine there are thousands of images containing the visual word "sky", but only a small percentage of those images contain another visual word "eye". If we use IDF weighting, the visual word "eye" will have a higher weight compared to "sky" because it is less common in the total set of images.&lt;/p&gt;

&lt;p&gt;The whole algorithm looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;First, we extract local feature descriptors from the image (such as SIFT, SURF, or ORB). I personally use &lt;a href="https://doi.org/10.1007/s00521-018-3677-9"&gt;both SIFT and ORB at the same time&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We then place all the descriptors into a K-means classifier and obtain n clusters.For each image, we calculate how many descriptors belong to each cluster, resulting in an n-dimensional Bag of Visual Words vector.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We transform this Bag of Words vector into a TF-IDF vector, which we can use to measure the distance between images using any distance metric (such as cosine or Euclidean distance).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Again, here are some of the results on the flickr30k dataset:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--niTjaAeN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n7u0a74ks5j58xxczapd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--niTjaAeN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n7u0a74ks5j58xxczapd.png" alt="Despite SIFT and ORB not considering color information, the images are still similar." width="880" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;
Despite SIFT and ORB not considering color information, the images are still similar.
&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---BaIgNiV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6syctearcy35o3ybwvrh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---BaIgNiV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6syctearcy35o3ybwvrh.png" alt='Clearly the common visual element across these photos is "dots".' width="880" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;
Clearly the common visual element across these photos is "dots".
&lt;/small&gt;&lt;/center&gt;

&lt;h3&gt;
  
  
  The biggest problem with CBIR
&lt;/h3&gt;

&lt;p&gt;A notable challenge in content-based image retrieval is the scarcity of meaningful datasets. Many scientific articles tend to reduce the complexity of the problem by treating it as a classification problem, rather than focusing on measuring the similarity of image content. This raises important questions: How can we effectively evaluate the performance of our models? Which approach is better - classical methods or SIFT+ORB?&lt;/p&gt;

&lt;p&gt;So I decided to simplify the problem to a classification task. To do this, I used the Landscapes dataset (which, as you'll soon find out, was not the most optimal choice). I assigned each image in the test set a category based on its 3 nearest neighbors from the training set, allowing me to evaluate the performance of the different techniques under this classification framework.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--woVXWi-V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i6qx3fwiqvob2nc3jaf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--woVXWi-V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i6qx3fwiqvob2nc3jaf3.png" alt="Here are the results of using SIFT for searching in the landscape dataset." width="880" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;
Here are the results of using SIFT for searching in the landscape dataset.
&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--h-Q0ned9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfdxvz09psiz6zavazq9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--h-Q0ned9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfdxvz09psiz6zavazq9.png" alt="I may have inadvertently created a watermark detector!" width="880" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;
I may have inadvertently created a watermark detector!
&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;Yeah... the dataset is of low quality which will have an impact on the accuracy. However, it's worth noting that the content of the images is actually very similar, which is exactly what we're looking for!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Q17gDxLB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lpdy6ip0f05loek387ke.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Q17gDxLB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lpdy6ip0f05loek387ke.png" alt="Confusion matrix for classical methods." width="880" height="664"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;
Confusion matrix for classical methods.
&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0vm-wHmB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cgp1ekl5dsj0js1pnb45.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0vm-wHmB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cgp1ekl5dsj0js1pnb45.png" alt="Confusion matrix for SIFT+ORB." width="880" height="665"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;
Confusion matrix for SIFT+ORB.
&lt;/small&gt;&lt;/center&gt;

&lt;h3&gt;
  
  
  Convolutional Conquerors
&lt;/h3&gt;

&lt;p&gt;As the finals were fast approaching, I shifted my focus to neural networks. With limited time, I couldn't afford to train one from scratch, and frankly, I wasn't sure how best to approach it. However, I realized that convolutional neural networks produce an output vector that could potentially be used to measure the distance between images.&lt;/p&gt;

&lt;p&gt;I thought, why not use a pre-trained neural network, "chop off its head", and simply utilize those vectors? So, I went ahead and acquired an &lt;a href="https://tfhub.dev/google/imagenet/efficientnet_v2_imagenet21k_l/feature_vector/2"&gt;EfficientNet without the classification layers&lt;/a&gt;, hoping to evaluate its potential... And it failed spectacularly, producing seemingly random vectors that made the images virtually incomparable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0f3-L7ft--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7phdri2sf9cahk50mjon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0f3-L7ft--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7phdri2sf9cahk50mjon.png" alt="Image description" width="880" height="651"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Convinced that the vectors were not random, but simply not comparable using standard distance measures, I tried to train a similarity-measuring neural network. A rather desperate move, I admit. In the end, the results were far from usable. The classifier consistently picked certain buildings while completely ignoring others, and that wasn't even the primary objective! So I decided to stop wasting time and let it go. I even experimented with machine learning techniques other than neural networks, such as Random Forests and KNN, but unfortunately these efforts were also fruitless.&lt;/p&gt;

&lt;p&gt;With the project deadline looming and time running out, I was almost ready to throw in the towel. I wondered if maybe EfficientNet was to blame for my struggles. In a last-ditch effort, I decided to give the &lt;a href="https://tfhub.dev/google/bit/m-r50x1/1%20"&gt;BiT-M R50x1&lt;/a&gt; model a try and evaluate its performance. To my amazement, it worked like a charm on the first try! I achieved spectacular results with a remarkable 93% accuracy!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9O79SCbh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bz6n5xzvv9appmcdrdho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9O79SCbh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bz6n5xzvv9appmcdrdho.png" alt="Image description" width="880" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thrilled by the impressive performance of the BiT-M model, I decided to test it on the entire Flickr30k dataset, intending to search across these vectors. After waiting patiently for a few hours as the model gradually processed all 30,000 images, I finally had my database. With great anticipation, I ran my first search and was blown away by the results! The images retrieved were not only visually similar to the query, but they also shared the same CONTENT.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Mth3GPpm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d7yoyjrioewvi3489xdg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Mth3GPpm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d7yoyjrioewvi3489xdg.png" alt="Image description" width="880" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fTQO_-Wj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnxsat0ezaut6olirqmo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fTQO_-Wj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnxsat0ezaut6olirqmo.png" alt="Image description" width="880" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OwpnG0UL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xrm72s67rsklsoux47ng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OwpnG0UL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xrm72s67rsklsoux47ng.png" alt="Image description" width="880" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In conclusion, my journey into the world of content-based image retrieval has been nothing short of eye-opening. From exploring classic methods like color auto-correlograms, to diving into local features with SIFT and ORB, and finally delving into the power of neural networks, I've learned a great deal about the different approaches to CBIR.&lt;/p&gt;

&lt;p&gt;Although I faced some challenges along the way, the remarkable success of the BiT-M model on the Flickr30k dataset was truly rewarding. It demonstrated the incredible potential of neural networks to retrieve images based not only on visual similarity, but also on content. As we continue to push the boundaries of what's possible with CBIR, the future of image search is undoubtedly bright and full of innovation.&lt;/p&gt;

&lt;p&gt;I hope you've enjoyed joining me on this exciting journey and that you've gained some valuable insights into the fascinating world of content-based image retrieval.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to glitch video in the age of web</title>
      <dc:creator>Jakub Kopańko</dc:creator>
      <pubDate>Thu, 23 Sep 2021 22:19:11 +0000</pubDate>
      <link>https://dev.to/pcktm/how-to-glitch-video-files-in-the-age-of-web-6a8</link>
      <guid>https://dev.to/pcktm/how-to-glitch-video-files-in-the-age-of-web-6a8</guid>
      <description>&lt;p&gt;&lt;em&gt;The tool described in this post is available at &lt;a href="https://ezglitch.kopanko.com/?mtm_campaign=dev.to"&gt;ezglitch.kopanko.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For years I've been interested in datamoshing and glitch art, but mainly for the computer aspect of it, like, you know, you edit some parts of the file, and it plays differently? How cool is that, right?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa71xhpf9u48m2kidroet.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa71xhpf9u48m2kidroet.gif" alt="one of the resulting videos"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But if you wanted to get into glitching, there's an obvious barrier! Most tutorials rely on old and buggy software or require you to download countless environments and tools onto your computer! Some people argue that if you don't do it with buggy software, it ain't &lt;em&gt;glitch&lt;/em&gt;-art at all!&lt;/p&gt;

&lt;p&gt;In the past, I have had made my own tools to break files for me, like &lt;a href="https://github.com/pcktm/glitchbox-cli" rel="noopener noreferrer"&gt;glitchbox&lt;/a&gt;, which was basically a JavaScript interface to &lt;a href="https://ffglitch.org/" rel="noopener noreferrer"&gt;ffglitch&lt;/a&gt; (back when it had none), always trying to make things as easy as possible for the end-user.&lt;/p&gt;

&lt;p&gt;So, one evening, I sat down and set on rewriting my go-to AVI glitching tool, &lt;a href="https://github.com/itsKaspar/tomato" rel="noopener noreferrer"&gt;tomato&lt;/a&gt; for the web. Let me start by explaining how the AVI file is actually constructed. AVI files consist of three basic parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hdrl buffer - a header of sorts that contains data on the total amount of frames, width, and height of the video, and so on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;movi buffer&lt;/strong&gt; - this is the part we actually care about as it contains raw frame data.&lt;/li&gt;
&lt;li&gt;idx1 buffer - holds the index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, the frames in the movi buffer are arranged as they will be played by the player. Audio data starts with the string &lt;code&gt;01wb&lt;/code&gt; and compressed video with &lt;code&gt;00dc&lt;/code&gt;. They end just before the next such tag or just before the &lt;code&gt;idx1&lt;/code&gt; buffer tag.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe48ban3j37k6ajcp4afn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe48ban3j37k6ajcp4afn.png" alt="actual data illustrated"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the fun part - if we rearrange or copy those frames around, the player will play them right as it sees them. We don't need to know the exact structure of the frame, its DCT coefficients, or some other complicated technical stuff - we just need to be able to move bytes around! Fortunately for us, that is entirely possible in modern browsers!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moviBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;moviMarkerPos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx1MarkerPos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have the entire &lt;code&gt;movi&lt;/code&gt; buffer, we need to construct a frame table. We use some &lt;a href="https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string-search_algorithm" rel="noopener noreferrer"&gt;string-search algorithm&lt;/a&gt; to find all occurrences of &lt;code&gt;00dc&lt;/code&gt; or &lt;code&gt;01wb&lt;/code&gt; in the buffer - they mark the beginning of every frame.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// this is just "00dc" in hexadecimal&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mh"&gt;0x30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x63&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;indices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BoyerMoore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;findIndexes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;moviBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bframes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We do the same thing to I-frames, combine the two, and sort them based on their index. Then, we need to get each frame's byte size (which will come in very handy in a moment):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;moviBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has been a pretty linear and dull process so far, but now we get to have some genuine fun - we get to come up with a function to mess with the frames! Let's do the simplest thing and just reverse the whole array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will, obviously, make the video play backward, but since the frames encoding motion do not take this into account we effectively flipped the motion vectors inside them, which in turn leads to a very odd effect in playback. Keep in mind the frames are still valid, and their data hasn't changed - just their order inside the file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhmluuyb0mzn6m7pbqpml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhmluuyb0mzn6m7pbqpml.png" alt="illustration of frame order inside the movi tag"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OK, so that's it? Well, not yet. We still need to reconstruct the new movi buffer from the frame table and combine it with hdrl and idx1 buffers. How do we approach it? &lt;/p&gt;

&lt;p&gt;The best way to do it is to get the final size of the movi buffer and allocate that much memory beforehand so that we don't ever have to resize our &lt;code&gt;Uint8Array&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;expectedMoviSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;expectedMoviSize&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait, why &lt;code&gt;expectedMoviSize = 4&lt;/code&gt;? Well, now we initialize the TypedArray with the final size and set the first 4 bytes to the &lt;code&gt;movi&lt;/code&gt; tag itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;finalMovi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedMoviSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;finalMovi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mh"&gt;0x6D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x6F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x69&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the final stretch - for every frame in the frame table, we read the data from the original file and write it at the correct offset in the final movi tag. We advance the head by the frame bytesize so that the frames are written sequentially.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// guess why we start at 4&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;final&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;moviBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;finalMovi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;head&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now all there's left is to recombine it with the original &lt;code&gt;hdrl&lt;/code&gt; and &lt;code&gt;idx1&lt;/code&gt; and we're done!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hdrlBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;finalMovi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;idx1Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hdrlBuffer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalMovi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;moviMarkerPos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx1Buffer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;hdrlBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;finalMovi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it, we can now save the complete modified file and enjoy the result we got!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkdpxyh8kucrofsltio7y.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkdpxyh8kucrofsltio7y.gif" alt="Resulting video"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Again, you can find the complete tool &lt;a href="https://ezglitch.kopanko.com/?mtm_campaign=dev.to"&gt;here&lt;/a&gt;.&lt;br&gt;
Thanks for reading, glitch on ✨! &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
