{
  "created": "2026-07-04T14:07:30Z",
  "hierarchy": [
    {
      "name": "ROOT",
      "type": "folder",
      "uri": "/ROOT"
    },
    {
      "name": "Photography Notes",
      "type": "article",
      "uri": "Photography_Notes"
    }
  ],
  "html": "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"/><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/><meta name=\"name\" content=\"Nikhil's Personal Wiki\"/><meta name=\"description\" content=\"Photography Notes – Nikhil's Personal Wiki\"/><meta name=\"author\" content=\"Nikhil Anand\"/><meta name=\"theme-color\" content=\"#ffe9a00\"/><meta property=\"og:title\" content=\"Photography Notes – Nikhil's Personal Wiki\"/><meta property=\"og:description\" content=\"Photography Notes – Nikhil's Personal Wiki\"/><meta property=\"og:type\" content=\"website\"/><meta property=\"og:image\" content=\"/img/logo512.png\"/><meta property=\"og:site_name\" content=\"Nikhil's Personal Wiki\"/><link rel=\"og:image\" href=\"/img/logo512.png\"/><link rel=\"icon\" href=\"/img/favicon.png\"/><link rel=\"apple-touch-icon\" href=\"/img/logo192.png\"/><link rel=\"stylesheet\" href=\"/css/styles.css\"/><title>Photography Notes – Nikhil's Personal Wiki</title><script defer data-domain=\"wiki.nikhil.io\" src=\"https://plausible.io/js/plausible.js\"></script></head><body><noscript>👉 A few things won't work if you have JavaScript disabled.</noscript><div class=\"container article\"><header><nav><ul><li><a href=\"/archive\" class title=\"Archive\"><span>Archive</span></a></li><li><a href=\"/Home\" class title=\"Home\"><span>Home</span></a></li><li><a href=\"/random\" class title=\"See a random article\"><span>Random</span></a></li><li><a href=\"/Photography_Notes/raw.txt\" title=\"View Source\"><span>Raw</span></a></li><li><a href=\"/Photography_Notes/revisions\" class title=\"View revisions\"><span>Revisions</span></a></li><li><a href=\"/Photography_Notes/index.json\" title=\"View JSON Object\"><span>JSON</span></a></li></ul></nav></header><main><article><article-hierarchy><ul><li><a data-entity-type=\"folder\" href=\"/ROOT\" title=\"ROOT\">Root</a></li><li><a data-entity-type=\"article\" href=\"/Photography_Notes\" title=\"Photography Notes\">Photography Notes</a></li></ul></article-hierarchy><header><h1>Photography Notes</h1></header><article-content><p>Assorted notes from me trying to teach myself Photography.</p>\n<h2 id=\"the-triangle\">The Triangle</h2>\n<p>It&rsquo;s a balance between Aperture, Shutter Speed, and ISO.</p>\n<h2 id=\"metering\">Metering</h2>\n<p>Your camera will try and advise you. People used to use light meters for this. It will say its a good exposure if it is &ldquo;18% Gray&rdquo; which is what light meters were historically calibrated to (the caps on Kodak film and the insides of dedicated camera bags are this gray too.)</p>\n<h2 id=\"the-histogram\">The Histogram</h2>\n<ul>\n<li>Luminance value on x-axis and pixel count on y-axis.</li>\n<li>Leftmost edge is Luminance of zero, Rightmost is Luminance of one. Any pixels touching these edges are lost information, they&rsquo;re just crushed out of existence. You can use Zebra stripes to tell you which parts of your image are/will be &ldquo;blown out&rdquo; in either direction.</li>\n<li>You section it into, say, five equal portions. From L-R, you are looking at <em>blacks</em>, <em>shadows</em>, <em>midtones</em>, <em>highlights</em>, and <em>whites</em>.</li>\n</ul>\n<p>Usually you want a histogram that looks like a mountain or is kinda balanced out but depends on what you&rsquo;re trying to do, the look you&rsquo;re going for.</p>\n<h2 id=\"on-aperture\">On Aperture</h2>\n<p>Measured by f-stop. &ldquo;f/2&rdquo; means you take the focal length and divide it by two.</p>\n<p>&ldquo;<a href=\"https://www.youtube.com/watch?v=DQcFQ-2dtB0\">f/8 and Be There.</a>.&rdquo; Like <em>be there</em>. Your camera primarily exists to take pictures of things and events.</p>\n<h2 id=\"on-iso\">On ISO</h2>\n<p><a href=\"https://www.youtube.com/watch?v=ZWSvHBG7X0w\">See this video</a>. TLDR: This is tricky so let the camera figure it out.</p>\n<h2 id=\"tripods\">Tripods</h2>\n<ul>\n<li>Cheap, Stable, Light —- pick two. But don&rsquo;t be cheap. You&rsquo;ll change cameras before tripods.</li>\n<li><a href=\"/assets/tripods-bythom.html\">Read this</a> thoroughly.</li>\n<li>Components are: Legs, Head, Plate, Bag\n<ul>\n<li>Each component may be purchased separately.</li>\n<li>Buying a head doesn&rsquo;t necessarily include a plate.</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"lenses\">Lenses</h2>\n<p>There are Prime lenses (fixed focal length) and Zoom lenses (can be wide, normal, and/or telephoto lenses).</p>\n<p>Prime lenses teach you a lot about photography because they make your lazy ass <em>move</em> and <em>explore</em> a scene: distances and angles.</p>\n<h2 id=\"presets-and-luts\">Presets and LUTs</h2>\n<ul>\n<li><a href=\"https://presetmator.com/\">Presetmator</a> &ndash; Photomator/Pixelmator</li>\n<li><a href=\"https://bodega.supply/collection/photo-presets\">Bodega Supply</a> &ndash; Photomator/Pixelmator</li>\n<li><a href=\"https://github.com/abpy/FujifilmCameraProfiles\">FujifilmCameraProfiles</a></li>\n<li><a href=\"https://johnrellis.com/lightroom/exportlut.htm\">Export LUT</a> Utility</li>\n</ul>\n<h3 id=\"hald-cluts\">Hald CLUTs</h3>\n<p><a href=\"https://github.com/cedeber/hald-clut\">These are great</a>!. This gives you a bunch of PNGs that you can &ldquo;infer LUTs&rdquo; from. Quick Python script that will do this is at the end of this page.</p>\n<p>Now the LUT grid size with the PNGs is 144 x 144 x 144 = 2,985,984 which makes the sizes of the <code>.cube</code> files explode. That script will downsample to 32 x 32 x 32.</p>\n<p><a href=\"https://public.nikhil.io/hald-clut-cubes.tgz\">I&rsquo;ve saved the generated <code>.cube</code> files here</a>.</p>\n<h2 id=\"other\">Other</h2>\n<ul>\n<li><a href=\"https://www.youtube.com/watch?v=Ky78k8j2-zc\">What This NYT/Getty/AP Photojournalist Has Learned After More Than 25 Years in the Field</a></li>\n<li><a href=\"https://www.youtube.com/watch?v=qYGc7QPU3tI\">How Metering Works</a> &ndash; A must-watch</li>\n</ul>\n<h3 id=\"infer-lut-from-png-script\">Infer LUT from PNG Script</h3>\n<pre tabindex=\"0\" class=\"chroma\"><code><span class=\"line\"><span class=\"cl\"><span class=\"ch\">#!/usr/bin/env python3</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">&#34;&#34;&#34;Infer a 3D color LUT from images and export as .cube file.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">Modes:\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">  hald   — Extract LUT from a processed Hald CLUT image.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">  pair   — Approximate LUT from a before/after image pair.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">  generate-hald — Generate a neutral Hald CLUT image to use as a reference.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">Usage:\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">  python infer_lut.py hald processed_hald.png -o my.cube\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">  python infer_lut.py pair original.png graded.png -o my.cube\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">  python infer_lut.py generate-hald -s 8 -o identity_hald.png\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">&#34;&#34;&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">argparse</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">numpy</span> <span class=\"k\">as</span> <span class=\"nn\">np</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">from</span> <span class=\"nn\">pathlib</span> <span class=\"kn\">import</span> <span class=\"n\">Path</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">from</span> <span class=\"nn\">PIL</span> <span class=\"kn\">import</span> <span class=\"n\">Image</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Hald CLUT helpers</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">hald_level_from_image</span><span class=\"p\">(</span><span class=\"n\">img</span><span class=\"p\">:</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">Image</span><span class=\"p\">)</span> <span class=\"o\">-&gt;</span> <span class=\"nb\">int</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"s2\">&#34;&#34;&#34;Determine the Hald level from image dimensions.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    A Hald CLUT of level N has dimensions (N^3) x (N^3).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    The resulting 3D LUT has N^2 entries per axis.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    Common levels: 4 → 64x64 image, 16-entry LUT\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">                   8 → 512x512 image, 64-entry LUT\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">                  12 → 1728x1728 image, 144-entry LUT\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    &#34;&#34;&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">w</span><span class=\"p\">,</span> <span class=\"n\">h</span> <span class=\"o\">=</span> <span class=\"n\">img</span><span class=\"o\">.</span><span class=\"n\">size</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">if</span> <span class=\"n\">w</span> <span class=\"o\">!=</span> <span class=\"n\">h</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"k\">raise</span> <span class=\"ne\">ValueError</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Hald CLUT must be square, got </span><span class=\"si\">{</span><span class=\"n\">w</span><span class=\"si\">}</span><span class=\"s2\">x</span><span class=\"si\">{</span><span class=\"n\">h</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># w = level^3</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">level</span> <span class=\"o\">=</span> <span class=\"nb\">round</span><span class=\"p\">(</span><span class=\"n\">w</span> <span class=\"o\">**</span> <span class=\"p\">(</span><span class=\"mi\">1</span> <span class=\"o\">/</span> <span class=\"mi\">3</span><span class=\"p\">))</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">if</span> <span class=\"n\">level</span><span class=\"o\">**</span><span class=\"mi\">3</span> <span class=\"o\">!=</span> <span class=\"n\">w</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"k\">raise</span> <span class=\"ne\">ValueError</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"sa\">f</span><span class=\"s2\">&#34;Image size </span><span class=\"si\">{</span><span class=\"n\">w</span><span class=\"si\">}</span><span class=\"s2\">x</span><span class=\"si\">{</span><span class=\"n\">h</span><span class=\"si\">}</span><span class=\"s2\"> is not a valid Hald CLUT dimension (need N^3 x N^3)&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">return</span> <span class=\"n\">level</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">generate_hald</span><span class=\"p\">(</span><span class=\"n\">level</span><span class=\"p\">:</span> <span class=\"nb\">int</span><span class=\"p\">)</span> <span class=\"o\">-&gt;</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">Image</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"s2\">&#34;&#34;&#34;Generate a neutral (identity) Hald CLUT image.&#34;&#34;&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">lut_size</span> <span class=\"o\">=</span> <span class=\"n\">level</span> <span class=\"o\">*</span> <span class=\"n\">level</span>  <span class=\"c1\"># entries per axis in the 3D LUT</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">dim</span> <span class=\"o\">=</span> <span class=\"n\">level</span><span class=\"o\">**</span><span class=\"mi\">3</span>  <span class=\"c1\"># pixel width/height of the Hald image</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">pixels</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">zeros</span><span class=\"p\">((</span><span class=\"n\">dim</span> <span class=\"o\">*</span> <span class=\"n\">dim</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">),</span> <span class=\"n\">dtype</span><span class=\"o\">=</span><span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">uint8</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">for</span> <span class=\"n\">i</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">dim</span> <span class=\"o\">*</span> <span class=\"n\">dim</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">r</span> <span class=\"o\">=</span> <span class=\"n\">i</span> <span class=\"o\">%</span> <span class=\"n\">lut_size</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">g</span> <span class=\"o\">=</span> <span class=\"p\">(</span><span class=\"n\">i</span> <span class=\"o\">//</span> <span class=\"n\">lut_size</span><span class=\"p\">)</span> <span class=\"o\">%</span> <span class=\"n\">lut_size</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">b</span> <span class=\"o\">=</span> <span class=\"n\">i</span> <span class=\"o\">//</span> <span class=\"p\">(</span><span class=\"n\">lut_size</span> <span class=\"o\">*</span> <span class=\"n\">lut_size</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">pixels</span><span class=\"p\">[</span><span class=\"n\">i</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"p\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"nb\">round</span><span class=\"p\">(</span><span class=\"n\">r</span> <span class=\"o\">/</span> <span class=\"p\">(</span><span class=\"n\">lut_size</span> <span class=\"o\">-</span> <span class=\"mi\">1</span><span class=\"p\">)</span> <span class=\"o\">*</span> <span class=\"mi\">255</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"nb\">round</span><span class=\"p\">(</span><span class=\"n\">g</span> <span class=\"o\">/</span> <span class=\"p\">(</span><span class=\"n\">lut_size</span> <span class=\"o\">-</span> <span class=\"mi\">1</span><span class=\"p\">)</span> <span class=\"o\">*</span> <span class=\"mi\">255</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"nb\">round</span><span class=\"p\">(</span><span class=\"n\">b</span> <span class=\"o\">/</span> <span class=\"p\">(</span><span class=\"n\">lut_size</span> <span class=\"o\">-</span> <span class=\"mi\">1</span><span class=\"p\">)</span> <span class=\"o\">*</span> <span class=\"mi\">255</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">return</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">fromarray</span><span class=\"p\">(</span><span class=\"n\">pixels</span><span class=\"o\">.</span><span class=\"n\">reshape</span><span class=\"p\">(</span><span class=\"n\">dim</span><span class=\"p\">,</span> <span class=\"n\">dim</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">))</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">lut_from_hald</span><span class=\"p\">(</span><span class=\"n\">img</span><span class=\"p\">:</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">Image</span><span class=\"p\">)</span> <span class=\"o\">-&gt;</span> <span class=\"nb\">tuple</span><span class=\"p\">[</span><span class=\"nb\">int</span><span class=\"p\">,</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">ndarray</span><span class=\"p\">]:</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"s2\">&#34;&#34;&#34;Extract a 3D LUT from a processed Hald CLUT image.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    Returns (lut_size, lut) where lut has shape (lut_size, lut_size, lut_size, 3)\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    with float values in [0, 1].\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    &#34;&#34;&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">level</span> <span class=\"o\">=</span> <span class=\"n\">hald_level_from_image</span><span class=\"p\">(</span><span class=\"n\">img</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">lut_size</span> <span class=\"o\">=</span> <span class=\"n\">level</span> <span class=\"o\">*</span> <span class=\"n\">level</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">dim</span> <span class=\"o\">=</span> <span class=\"n\">level</span><span class=\"o\">**</span><span class=\"mi\">3</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">pixels</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"p\">(</span><span class=\"n\">img</span><span class=\"o\">.</span><span class=\"n\">convert</span><span class=\"p\">(</span><span class=\"s2\">&#34;RGB&#34;</span><span class=\"p\">),</span> <span class=\"n\">dtype</span><span class=\"o\">=</span><span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">float64</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">reshape</span><span class=\"p\">(</span><span class=\"o\">-</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">)</span> <span class=\"o\">/</span> <span class=\"mf\">255.0</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">expected</span> <span class=\"o\">=</span> <span class=\"n\">dim</span> <span class=\"o\">*</span> <span class=\"n\">dim</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">if</span> <span class=\"nb\">len</span><span class=\"p\">(</span><span class=\"n\">pixels</span><span class=\"p\">)</span> <span class=\"o\">!=</span> <span class=\"n\">expected</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"k\">raise</span> <span class=\"ne\">ValueError</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Expected </span><span class=\"si\">{</span><span class=\"n\">expected</span><span class=\"si\">}</span><span class=\"s2\"> pixels, got </span><span class=\"si\">{</span><span class=\"nb\">len</span><span class=\"p\">(</span><span class=\"n\">pixels</span><span class=\"p\">)</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">lut</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">zeros</span><span class=\"p\">((</span><span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">),</span> <span class=\"n\">dtype</span><span class=\"o\">=</span><span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">float64</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">for</span> <span class=\"n\">i</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">expected</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">r</span> <span class=\"o\">=</span> <span class=\"n\">i</span> <span class=\"o\">%</span> <span class=\"n\">lut_size</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">g</span> <span class=\"o\">=</span> <span class=\"p\">(</span><span class=\"n\">i</span> <span class=\"o\">//</span> <span class=\"n\">lut_size</span><span class=\"p\">)</span> <span class=\"o\">%</span> <span class=\"n\">lut_size</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">b</span> <span class=\"o\">=</span> <span class=\"n\">i</span> <span class=\"o\">//</span> <span class=\"p\">(</span><span class=\"n\">lut_size</span> <span class=\"o\">*</span> <span class=\"n\">lut_size</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">lut</span><span class=\"p\">[</span><span class=\"n\">b</span><span class=\"p\">,</span> <span class=\"n\">g</span><span class=\"p\">,</span> <span class=\"n\">r</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"n\">pixels</span><span class=\"p\">[</span><span class=\"n\">i</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">return</span> <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">resample_lut</span><span class=\"p\">(</span><span class=\"n\">lut</span><span class=\"p\">:</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">ndarray</span><span class=\"p\">,</span> <span class=\"n\">src_size</span><span class=\"p\">:</span> <span class=\"nb\">int</span><span class=\"p\">,</span> <span class=\"n\">dst_size</span><span class=\"p\">:</span> <span class=\"nb\">int</span><span class=\"p\">)</span> <span class=\"o\">-&gt;</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">ndarray</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"s2\">&#34;&#34;&#34;Resample a 3D LUT to a different grid size via trilinear interpolation.&#34;&#34;&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"kn\">from</span> <span class=\"nn\">scipy.interpolate</span> <span class=\"kn\">import</span> <span class=\"n\">RegularGridInterpolator</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">axis</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">linspace</span><span class=\"p\">(</span><span class=\"mi\">0</span><span class=\"p\">,</span> <span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"n\">src_size</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">channels</span> <span class=\"o\">=</span> <span class=\"p\">[]</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">for</span> <span class=\"n\">ch</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"mi\">3</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">interp</span> <span class=\"o\">=</span> <span class=\"n\">RegularGridInterpolator</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"p\">(</span><span class=\"n\">axis</span><span class=\"p\">,</span> <span class=\"n\">axis</span><span class=\"p\">,</span> <span class=\"n\">axis</span><span class=\"p\">),</span> <span class=\"n\">lut</span><span class=\"p\">[:,</span> <span class=\"p\">:,</span> <span class=\"p\">:,</span> <span class=\"n\">ch</span><span class=\"p\">],</span> <span class=\"n\">method</span><span class=\"o\">=</span><span class=\"s2\">&#34;linear&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">dst_axis</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">linspace</span><span class=\"p\">(</span><span class=\"mi\">0</span><span class=\"p\">,</span> <span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"n\">dst_size</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">grid</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">meshgrid</span><span class=\"p\">(</span><span class=\"n\">dst_axis</span><span class=\"p\">,</span> <span class=\"n\">dst_axis</span><span class=\"p\">,</span> <span class=\"n\">dst_axis</span><span class=\"p\">,</span> <span class=\"n\">indexing</span><span class=\"o\">=</span><span class=\"s2\">&#34;ij&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">pts</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">stack</span><span class=\"p\">(</span><span class=\"n\">grid</span><span class=\"p\">,</span> <span class=\"n\">axis</span><span class=\"o\">=-</span><span class=\"mi\">1</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">reshape</span><span class=\"p\">(</span><span class=\"o\">-</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">channels</span><span class=\"o\">.</span><span class=\"n\">append</span><span class=\"p\">(</span><span class=\"n\">interp</span><span class=\"p\">(</span><span class=\"n\">pts</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">reshape</span><span class=\"p\">(</span><span class=\"n\">dst_size</span><span class=\"p\">,</span> <span class=\"n\">dst_size</span><span class=\"p\">,</span> <span class=\"n\">dst_size</span><span class=\"p\">))</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">return</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">clip</span><span class=\"p\">(</span><span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">stack</span><span class=\"p\">(</span><span class=\"n\">channels</span><span class=\"p\">,</span> <span class=\"n\">axis</span><span class=\"o\">=-</span><span class=\"mi\">1</span><span class=\"p\">),</span> <span class=\"mi\">0</span><span class=\"p\">,</span> <span class=\"mi\">1</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Before/after pair → LUT approximation</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">lut_from_pair</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">original</span><span class=\"p\">:</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">Image</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">graded</span><span class=\"p\">:</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">Image</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">lut_size</span><span class=\"p\">:</span> <span class=\"nb\">int</span> <span class=\"o\">=</span> <span class=\"mi\">33</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span> <span class=\"o\">-&gt;</span> <span class=\"nb\">tuple</span><span class=\"p\">[</span><span class=\"nb\">int</span><span class=\"p\">,</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">ndarray</span><span class=\"p\">]:</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"s2\">&#34;&#34;&#34;Approximate a 3D LUT by comparing original ↔ graded pixel pairs.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    Bins every pixel by its original RGB value into a 3D grid, averages the\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    corresponding graded values per bin. Fills empty bins via nearest-neighbor\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    interpolation.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    &#34;&#34;&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">if</span> <span class=\"n\">original</span><span class=\"o\">.</span><span class=\"n\">size</span> <span class=\"o\">!=</span> <span class=\"n\">graded</span><span class=\"o\">.</span><span class=\"n\">size</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"c1\"># resize graded to match original</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">graded</span> <span class=\"o\">=</span> <span class=\"n\">graded</span><span class=\"o\">.</span><span class=\"n\">resize</span><span class=\"p\">(</span><span class=\"n\">original</span><span class=\"o\">.</span><span class=\"n\">size</span><span class=\"p\">,</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">LANCZOS</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">orig_px</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"p\">(</span><span class=\"n\">original</span><span class=\"o\">.</span><span class=\"n\">convert</span><span class=\"p\">(</span><span class=\"s2\">&#34;RGB&#34;</span><span class=\"p\">),</span> <span class=\"n\">dtype</span><span class=\"o\">=</span><span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">float64</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">reshape</span><span class=\"p\">(</span><span class=\"o\">-</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">grad_px</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"p\">(</span><span class=\"n\">graded</span><span class=\"o\">.</span><span class=\"n\">convert</span><span class=\"p\">(</span><span class=\"s2\">&#34;RGB&#34;</span><span class=\"p\">),</span> <span class=\"n\">dtype</span><span class=\"o\">=</span><span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">float64</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">reshape</span><span class=\"p\">(</span><span class=\"o\">-</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># Quantize original pixels into LUT bins</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">bin_indices</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">clip</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"p\">(</span><span class=\"n\">orig_px</span> <span class=\"o\">/</span> <span class=\"mf\">255.0</span> <span class=\"o\">*</span> <span class=\"p\">(</span><span class=\"n\">lut_size</span> <span class=\"o\">-</span> <span class=\"mi\">1</span><span class=\"p\">)</span> <span class=\"o\">+</span> <span class=\"mf\">0.5</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">astype</span><span class=\"p\">(</span><span class=\"nb\">int</span><span class=\"p\">),</span> <span class=\"mi\">0</span><span class=\"p\">,</span> <span class=\"n\">lut_size</span> <span class=\"o\">-</span> <span class=\"mi\">1</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># Accumulate</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">lut_sum</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">zeros</span><span class=\"p\">((</span><span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">),</span> <span class=\"n\">dtype</span><span class=\"o\">=</span><span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">float64</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">lut_count</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">zeros</span><span class=\"p\">((</span><span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut_size</span><span class=\"p\">),</span> <span class=\"n\">dtype</span><span class=\"o\">=</span><span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">float64</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">for</span> <span class=\"n\">idx</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"nb\">len</span><span class=\"p\">(</span><span class=\"n\">orig_px</span><span class=\"p\">)):</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">r</span><span class=\"p\">,</span> <span class=\"n\">g</span><span class=\"p\">,</span> <span class=\"n\">b</span> <span class=\"o\">=</span> <span class=\"n\">bin_indices</span><span class=\"p\">[</span><span class=\"n\">idx</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">lut_sum</span><span class=\"p\">[</span><span class=\"n\">b</span><span class=\"p\">,</span> <span class=\"n\">g</span><span class=\"p\">,</span> <span class=\"n\">r</span><span class=\"p\">]</span> <span class=\"o\">+=</span> <span class=\"n\">grad_px</span><span class=\"p\">[</span><span class=\"n\">idx</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">lut_count</span><span class=\"p\">[</span><span class=\"n\">b</span><span class=\"p\">,</span> <span class=\"n\">g</span><span class=\"p\">,</span> <span class=\"n\">r</span><span class=\"p\">]</span> <span class=\"o\">+=</span> <span class=\"mi\">1</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># Average filled bins</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">mask</span> <span class=\"o\">=</span> <span class=\"n\">lut_count</span> <span class=\"o\">&gt;</span> <span class=\"mi\">0</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">lut</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">zeros_like</span><span class=\"p\">(</span><span class=\"n\">lut_sum</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">lut</span><span class=\"p\">[</span><span class=\"n\">mask</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"n\">lut_sum</span><span class=\"p\">[</span><span class=\"n\">mask</span><span class=\"p\">]</span> <span class=\"o\">/</span> <span class=\"n\">lut_count</span><span class=\"p\">[</span><span class=\"n\">mask</span><span class=\"p\">,</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">newaxis</span><span class=\"p\">]</span> <span class=\"o\">/</span> <span class=\"mf\">255.0</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># Fill empty bins via nearest-neighbor from filled bins</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">empty</span> <span class=\"o\">=</span> <span class=\"o\">~</span><span class=\"n\">mask</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">n_empty</span> <span class=\"o\">=</span> <span class=\"n\">empty</span><span class=\"o\">.</span><span class=\"n\">sum</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">if</span> <span class=\"n\">n_empty</span> <span class=\"o\">&gt;</span> <span class=\"mi\">0</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"kn\">from</span> <span class=\"nn\">scipy.ndimage</span> <span class=\"kn\">import</span> <span class=\"n\">distance_transform_edt</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"c1\"># distance_transform_edt returns distances and indices of nearest filled cell</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">_</span><span class=\"p\">,</span> <span class=\"n\">nearest_idx</span> <span class=\"o\">=</span> <span class=\"n\">distance_transform_edt</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">empty</span><span class=\"p\">,</span> <span class=\"n\">return_distances</span><span class=\"o\">=</span><span class=\"kc\">True</span><span class=\"p\">,</span> <span class=\"n\">return_indices</span><span class=\"o\">=</span><span class=\"kc\">True</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"k\">for</span> <span class=\"n\">b_i</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">lut_size</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"k\">for</span> <span class=\"n\">g_i</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">lut_size</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">                <span class=\"k\">for</span> <span class=\"n\">r_i</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">lut_size</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">                    <span class=\"k\">if</span> <span class=\"n\">empty</span><span class=\"p\">[</span><span class=\"n\">b_i</span><span class=\"p\">,</span> <span class=\"n\">g_i</span><span class=\"p\">,</span> <span class=\"n\">r_i</span><span class=\"p\">]:</span>\n</span></span><span class=\"line\"><span class=\"cl\">                        <span class=\"n\">nb</span><span class=\"p\">,</span> <span class=\"n\">ng</span><span class=\"p\">,</span> <span class=\"n\">nr</span> <span class=\"o\">=</span> <span class=\"n\">nearest_idx</span><span class=\"p\">[:,</span> <span class=\"n\">b_i</span><span class=\"p\">,</span> <span class=\"n\">g_i</span><span class=\"p\">,</span> <span class=\"n\">r_i</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">                        <span class=\"n\">lut</span><span class=\"p\">[</span><span class=\"n\">b_i</span><span class=\"p\">,</span> <span class=\"n\">g_i</span><span class=\"p\">,</span> <span class=\"n\">r_i</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"n\">lut</span><span class=\"p\">[</span><span class=\"n\">nb</span><span class=\"p\">,</span> <span class=\"n\">ng</span><span class=\"p\">,</span> <span class=\"n\">nr</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">return</span> <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># .cube export</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">write_cube</span><span class=\"p\">(</span><span class=\"n\">path</span><span class=\"p\">:</span> <span class=\"nb\">str</span><span class=\"p\">,</span> <span class=\"n\">lut_size</span><span class=\"p\">:</span> <span class=\"nb\">int</span><span class=\"p\">,</span> <span class=\"n\">lut</span><span class=\"p\">:</span> <span class=\"n\">np</span><span class=\"o\">.</span><span class=\"n\">ndarray</span><span class=\"p\">,</span> <span class=\"n\">title</span><span class=\"p\">:</span> <span class=\"nb\">str</span> <span class=\"o\">=</span> <span class=\"s2\">&#34;Inferred LUT&#34;</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"s2\">&#34;&#34;&#34;Write a 3D LUT in Adobe .cube format.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    lut shape: (lut_size, lut_size, lut_size, 3) with values in [0, 1].\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    Iteration order: R fastest, then G, then B (standard .cube convention).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s2\">    &#34;&#34;&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">with</span> <span class=\"nb\">open</span><span class=\"p\">(</span><span class=\"n\">path</span><span class=\"p\">,</span> <span class=\"s2\">&#34;w&#34;</span><span class=\"p\">)</span> <span class=\"k\">as</span> <span class=\"n\">f</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">f</span><span class=\"o\">.</span><span class=\"n\">write</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s1\">&#39;TITLE &#34;</span><span class=\"si\">{</span><span class=\"n\">title</span><span class=\"si\">}</span><span class=\"s1\">&#34;</span><span class=\"se\">\\n</span><span class=\"s1\">&#39;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">f</span><span class=\"o\">.</span><span class=\"n\">write</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;LUT_3D_SIZE </span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"se\">\\n</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">f</span><span class=\"o\">.</span><span class=\"n\">write</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;DOMAIN_MIN 0.0 0.0 0.0</span><span class=\"se\">\\n</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">f</span><span class=\"o\">.</span><span class=\"n\">write</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;DOMAIN_MAX 1.0 1.0 1.0</span><span class=\"se\">\\n\\n</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"k\">for</span> <span class=\"n\">b</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">lut_size</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"k\">for</span> <span class=\"n\">g</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">lut_size</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">                <span class=\"k\">for</span> <span class=\"n\">r</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">lut_size</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">                    <span class=\"n\">rv</span><span class=\"p\">,</span> <span class=\"n\">gv</span><span class=\"p\">,</span> <span class=\"n\">bv</span> <span class=\"o\">=</span> <span class=\"n\">lut</span><span class=\"p\">[</span><span class=\"n\">b</span><span class=\"p\">,</span> <span class=\"n\">g</span><span class=\"p\">,</span> <span class=\"n\">r</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">                    <span class=\"n\">f</span><span class=\"o\">.</span><span class=\"n\">write</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;</span><span class=\"si\">{</span><span class=\"n\">rv</span><span class=\"si\">:</span><span class=\"s2\">.6f</span><span class=\"si\">}</span><span class=\"s2\"> </span><span class=\"si\">{</span><span class=\"n\">gv</span><span class=\"si\">:</span><span class=\"s2\">.6f</span><span class=\"si\">}</span><span class=\"s2\"> </span><span class=\"si\">{</span><span class=\"n\">bv</span><span class=\"si\">:</span><span class=\"s2\">.6f</span><span class=\"si\">}</span><span class=\"se\">\\n</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># CLI</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">main</span><span class=\"p\">():</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">parser</span> <span class=\"o\">=</span> <span class=\"n\">argparse</span><span class=\"o\">.</span><span class=\"n\">ArgumentParser</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">description</span><span class=\"o\">=</span><span class=\"s2\">&#34;Infer a 3D color LUT from images.&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">formatter_class</span><span class=\"o\">=</span><span class=\"n\">argparse</span><span class=\"o\">.</span><span class=\"n\">RawDescriptionHelpFormatter</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">epilog</span><span class=\"o\">=</span><span class=\"vm\">__doc__</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">sub</span> <span class=\"o\">=</span> <span class=\"n\">parser</span><span class=\"o\">.</span><span class=\"n\">add_subparsers</span><span class=\"p\">(</span><span class=\"n\">dest</span><span class=\"o\">=</span><span class=\"s2\">&#34;command&#34;</span><span class=\"p\">,</span> <span class=\"n\">required</span><span class=\"o\">=</span><span class=\"kc\">True</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># -- hald --</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_hald</span> <span class=\"o\">=</span> <span class=\"n\">sub</span><span class=\"o\">.</span><span class=\"n\">add_parser</span><span class=\"p\">(</span><span class=\"s2\">&#34;hald&#34;</span><span class=\"p\">,</span> <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Extract LUT from a processed Hald CLUT image&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_hald</span><span class=\"o\">.</span><span class=\"n\">add_argument</span><span class=\"p\">(</span><span class=\"s2\">&#34;image&#34;</span><span class=\"p\">,</span> <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Processed Hald CLUT image (PNG, TIFF, etc.)&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_hald</span><span class=\"o\">.</span><span class=\"n\">add_argument</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"s2\">&#34;-o&#34;</span><span class=\"p\">,</span> <span class=\"s2\">&#34;--output&#34;</span><span class=\"p\">,</span> <span class=\"n\">default</span><span class=\"o\">=</span><span class=\"s2\">&#34;output.cube&#34;</span><span class=\"p\">,</span> <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Output .cube file&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_hald</span><span class=\"o\">.</span><span class=\"n\">add_argument</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"s2\">&#34;-s&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"s2\">&#34;--size&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">type</span><span class=\"o\">=</span><span class=\"nb\">int</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">default</span><span class=\"o\">=</span><span class=\"mi\">35</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Output LUT grid size per axis (default 35)&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># -- pair --</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_pair</span> <span class=\"o\">=</span> <span class=\"n\">sub</span><span class=\"o\">.</span><span class=\"n\">add_parser</span><span class=\"p\">(</span><span class=\"s2\">&#34;pair&#34;</span><span class=\"p\">,</span> <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Approximate LUT from original + graded pair&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_pair</span><span class=\"o\">.</span><span class=\"n\">add_argument</span><span class=\"p\">(</span><span class=\"s2\">&#34;original&#34;</span><span class=\"p\">,</span> <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Original (ungraded) image&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_pair</span><span class=\"o\">.</span><span class=\"n\">add_argument</span><span class=\"p\">(</span><span class=\"s2\">&#34;graded&#34;</span><span class=\"p\">,</span> <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Graded image&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_pair</span><span class=\"o\">.</span><span class=\"n\">add_argument</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"s2\">&#34;-o&#34;</span><span class=\"p\">,</span> <span class=\"s2\">&#34;--output&#34;</span><span class=\"p\">,</span> <span class=\"n\">default</span><span class=\"o\">=</span><span class=\"s2\">&#34;output.cube&#34;</span><span class=\"p\">,</span> <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Output .cube file&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_pair</span><span class=\"o\">.</span><span class=\"n\">add_argument</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"s2\">&#34;-s&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"s2\">&#34;--size&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">type</span><span class=\"o\">=</span><span class=\"nb\">int</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">default</span><span class=\"o\">=</span><span class=\"mi\">33</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;LUT grid size per axis (default 33, industry standard)&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># -- generate-hald --</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_gen</span> <span class=\"o\">=</span> <span class=\"n\">sub</span><span class=\"o\">.</span><span class=\"n\">add_parser</span><span class=\"p\">(</span><span class=\"s2\">&#34;generate-hald&#34;</span><span class=\"p\">,</span> <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Generate a neutral Hald CLUT image&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_gen</span><span class=\"o\">.</span><span class=\"n\">add_argument</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"s2\">&#34;-s&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"s2\">&#34;--level&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">type</span><span class=\"o\">=</span><span class=\"nb\">int</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">default</span><span class=\"o\">=</span><span class=\"mi\">8</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Hald level (default 8 → 512x512 image, 64-entry LUT)&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">p_gen</span><span class=\"o\">.</span><span class=\"n\">add_argument</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"s2\">&#34;-o&#34;</span><span class=\"p\">,</span> <span class=\"s2\">&#34;--output&#34;</span><span class=\"p\">,</span> <span class=\"n\">default</span><span class=\"o\">=</span><span class=\"s2\">&#34;identity_hald.png&#34;</span><span class=\"p\">,</span> <span class=\"n\">help</span><span class=\"o\">=</span><span class=\"s2\">&#34;Output image path&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">args</span> <span class=\"o\">=</span> <span class=\"n\">parser</span><span class=\"o\">.</span><span class=\"n\">parse_args</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">if</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">command</span> <span class=\"o\">==</span> <span class=\"s2\">&#34;generate-hald&#34;</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Generating identity Hald CLUT (level </span><span class=\"si\">{</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">level</span><span class=\"si\">}</span><span class=\"s2\">)...&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">img</span> <span class=\"o\">=</span> <span class=\"n\">generate_hald</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">level</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">img</span><span class=\"o\">.</span><span class=\"n\">save</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">output</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">lut_size</span> <span class=\"o\">=</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">level</span><span class=\"o\">**</span><span class=\"mi\">2</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Saved </span><span class=\"si\">{</span><span class=\"n\">img</span><span class=\"o\">.</span><span class=\"n\">size</span><span class=\"p\">[</span><span class=\"mi\">0</span><span class=\"p\">]</span><span class=\"si\">}</span><span class=\"s2\">x</span><span class=\"si\">{</span><span class=\"n\">img</span><span class=\"o\">.</span><span class=\"n\">size</span><span class=\"p\">[</span><span class=\"mi\">1</span><span class=\"p\">]</span><span class=\"si\">}</span><span class=\"s2\"> image → </span><span class=\"si\">{</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">output</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">print</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"sa\">f</span><span class=\"s2\">&#34;Resulting LUT will have </span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"s2\"> entries per axis (</span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"s2\">^3 = </span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"o\">**</span><span class=\"mi\">3</span><span class=\"si\">}</span><span class=\"s2\"> total)&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">elif</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">command</span> <span class=\"o\">==</span> <span class=\"s2\">&#34;hald&#34;</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Reading Hald CLUT: </span><span class=\"si\">{</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">image</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">img</span> <span class=\"o\">=</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">open</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">image</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut</span> <span class=\"o\">=</span> <span class=\"n\">lut_from_hald</span><span class=\"p\">(</span><span class=\"n\">img</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"k\">if</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">size</span> <span class=\"o\">!=</span> <span class=\"n\">lut_size</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Resampling </span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"s2\">³ → </span><span class=\"si\">{</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">size</span><span class=\"si\">}</span><span class=\"s2\">³...&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">lut</span> <span class=\"o\">=</span> <span class=\"n\">resample_lut</span><span class=\"p\">(</span><span class=\"n\">lut</span><span class=\"p\">,</span> <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">size</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">lut_size</span> <span class=\"o\">=</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">size</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">write_cube</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">output</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">lut_size</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">lut</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">title</span><span class=\"o\">=</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Hald-inferred from </span><span class=\"si\">{</span><span class=\"n\">Path</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">image</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">name</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Extracted </span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"s2\">x</span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"s2\">x</span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"s2\"> LUT → </span><span class=\"si\">{</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">output</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">elif</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">command</span> <span class=\"o\">==</span> <span class=\"s2\">&#34;pair&#34;</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Original: </span><span class=\"si\">{</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">original</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Graded:   </span><span class=\"si\">{</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">graded</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">orig</span> <span class=\"o\">=</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">open</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">original</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">grad</span> <span class=\"o\">=</span> <span class=\"n\">Image</span><span class=\"o\">.</span><span class=\"n\">open</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">graded</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">lut_size</span><span class=\"p\">,</span> <span class=\"n\">lut</span> <span class=\"o\">=</span> <span class=\"n\">lut_from_pair</span><span class=\"p\">(</span><span class=\"n\">orig</span><span class=\"p\">,</span> <span class=\"n\">grad</span><span class=\"p\">,</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">size</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"n\">write_cube</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">output</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">lut_size</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">lut</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">            <span class=\"n\">title</span><span class=\"o\">=</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Pair-inferred from </span><span class=\"si\">{</span><span class=\"n\">Path</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">original</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">name</span><span class=\"si\">}</span><span class=\"s2\"> → </span><span class=\"si\">{</span><span class=\"n\">Path</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">graded</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">name</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">        <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"s2\">&#34;Approximated </span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"s2\">x</span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"s2\">x</span><span class=\"si\">{</span><span class=\"n\">lut_size</span><span class=\"si\">}</span><span class=\"s2\"> LUT → </span><span class=\"si\">{</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">output</span><span class=\"si\">}</span><span class=\"s2\">&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"s2\">&#34;Done.&#34;</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">if</span> <span class=\"vm\">__name__</span> <span class=\"o\">==</span> <span class=\"s2\">&#34;__main__&#34;</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">main</span><span class=\"p\">()</span>\n</span></span></code></pre><h3 id=\"random\">Random</h3>\n<ul>\n<li>&ldquo;Dragging your shutter&rdquo; means using a low shutter speed for things like movement.</li>\n</ul>\n</article-content></article></main><footer><p></p><ul><li>13,330 bytes</li><li>Created on Saturday, 4 July 2026 at 14:07 UTC</li><li>Modified on Friday, 18 September 2026 at 21:11 UTC</li><br/><li><a href=\"https://github.com/afreeorange/wiki.nikhil.io.articles/edit/master/Photography Notes.md\" title=\"Edit this article\">Edit this article</a></li><li><a href=\"https://github.com/afreeorange/bock\" title=\"View the project that generates this wiki on Github\"><span>bock 6.3.0-alpha</span></a></li></ul></footer></div><script src=\"/js/table-sort.js\" defer></script><script type=\"text/javascript\">\n        document.body.addEventListener(\n          \"keypress\", (e) => e.key === \"f\"\n          ? window.location.assign(\"/archive\")\n          : null);\n      </script></body></html>",
  "id": "48933311-d0e7-5add-8598-f3b2af75a62c",
  "modified": "2026-09-18T21:11:04Z",
  "revisions": [
    {
      "authorEmail": "anand.nikhil@gmail.com",
      "authorName": "afreeorange",
      "date": "2026-09-18T21:11:04Z",
      "id": "78c6e6008b89129c6969832fea88266d04da0ec7",
      "shortId": "78c6e600",
      "subject": "Random note\n",
      "content": "Assorted notes from me trying to teach myself Photography.\n\n## The Triangle\n\nIt's a balance between Aperture, Shutter Speed, and ISO.\n\n## Metering\n\nYour camera will try and advise you. People used to use light meters for this. It will say its a good exposure if it is \"18% Gray\" which is what light meters were historically calibrated to (the caps on Kodak film and the insides of dedicated camera bags are this gray too.)\n\n## The Histogram\n\n- Luminance value on x-axis and pixel count on y-axis.\n- Leftmost edge is Luminance of zero, Rightmost is Luminance of one. Any pixels touching these edges are lost information, they're just crushed out of existence. You can use Zebra stripes to tell you which parts of your image are/will be \"blown out\" in either direction.\n- You section it into, say, five equal portions. From L-R, you are looking at _blacks_, _shadows_, _midtones_, _highlights_, and _whites_.\n\nUsually you want a histogram that looks like a mountain or is kinda balanced out but depends on what you're trying to do, the look you're going for.\n\n## On Aperture\n\nMeasured by f-stop. \"f/2\" means you take the focal length and divide it by two.\n\n\"[f/8 and Be There.](https://www.youtube.com/watch?v=DQcFQ-2dtB0).\" Like _be there_. Your camera primarily exists to take pictures of things and events.\n\n## On ISO\n\n[See this video](https://www.youtube.com/watch?v=ZWSvHBG7X0w). TLDR: This is tricky so let the camera figure it out.\n\n## Tripods\n\n- Cheap, Stable, Light —- pick two. But don't be cheap. You'll change cameras before tripods.\n- [Read this](/assets/tripods-bythom.html) thoroughly.\n- Components are: Legs, Head, Plate, Bag\n  - Each component may be purchased separately.\n  - Buying a head doesn't necessarily include a plate.\n\n## Lenses\n\nThere are Prime lenses (fixed focal length) and Zoom lenses (can be wide, normal, and/or telephoto lenses).\n\nPrime lenses teach you a lot about photography because they make your lazy ass _move_ and _explore_ a scene: distances and angles.\n\n## Presets and LUTs\n\n- [Presetmator](https://presetmator.com/) -- Photomator/Pixelmator\n- [Bodega Supply](https://bodega.supply/collection/photo-presets) -- Photomator/Pixelmator\n- [FujifilmCameraProfiles](https://github.com/abpy/FujifilmCameraProfiles)\n- [Export LUT](https://johnrellis.com/lightroom/exportlut.htm) Utility\n\n### Hald CLUTs\n\n[These are great](https://github.com/cedeber/hald-clut)!. This gives you a bunch of PNGs that you can \"infer LUTs\" from. Quick Python script that will do this is at the end of this page.\n\nNow the LUT grid size with the PNGs is 144 x 144 x 144 = 2,985,984 which makes the sizes of the `.cube` files explode. That script will downsample to 32 x 32 x 32.\n\n[I've saved the generated `.cube` files here](https://public.nikhil.io/hald-clut-cubes.tgz).\n\n## Other\n\n- [What This NYT/Getty/AP Photojournalist Has Learned After More Than 25 Years in the Field](https://www.youtube.com/watch?v=Ky78k8j2-zc)\n- [How Metering Works](https://www.youtube.com/watch?v=qYGc7QPU3tI) -- A must-watch\n\n### Infer LUT from PNG Script\n\n```python\n#!/usr/bin/env python3\n\"\"\"Infer a 3D color LUT from images and export as .cube file.\n\nModes:\n  hald   — Extract LUT from a processed Hald CLUT image.\n  pair   — Approximate LUT from a before/after image pair.\n  generate-hald — Generate a neutral Hald CLUT image to use as a reference.\n\nUsage:\n  python infer_lut.py hald processed_hald.png -o my.cube\n  python infer_lut.py pair original.png graded.png -o my.cube\n  python infer_lut.py generate-hald -s 8 -o identity_hald.png\n\"\"\"\n\nimport argparse\nimport numpy as np\nfrom pathlib import Path\nfrom PIL import Image\n\n\n# ---------------------------------------------------------------------------\n# Hald CLUT helpers\n# ---------------------------------------------------------------------------\n\n\ndef hald_level_from_image(img: Image.Image) -> int:\n    \"\"\"Determine the Hald level from image dimensions.\n\n    A Hald CLUT of level N has dimensions (N^3) x (N^3).\n    The resulting 3D LUT has N^2 entries per axis.\n    Common levels: 4 → 64x64 image, 16-entry LUT\n                   8 → 512x512 image, 64-entry LUT\n                  12 → 1728x1728 image, 144-entry LUT\n    \"\"\"\n    w, h = img.size\n    if w != h:\n        raise ValueError(f\"Hald CLUT must be square, got {w}x{h}\")\n    # w = level^3\n    level = round(w ** (1 / 3))\n    if level**3 != w:\n        raise ValueError(\n            f\"Image size {w}x{h} is not a valid Hald CLUT dimension (need N^3 x N^3)\"\n        )\n    return level\n\n\ndef generate_hald(level: int) -> Image.Image:\n    \"\"\"Generate a neutral (identity) Hald CLUT image.\"\"\"\n    lut_size = level * level  # entries per axis in the 3D LUT\n    dim = level**3  # pixel width/height of the Hald image\n    pixels = np.zeros((dim * dim, 3), dtype=np.uint8)\n\n    for i in range(dim * dim):\n        r = i % lut_size\n        g = (i // lut_size) % lut_size\n        b = i // (lut_size * lut_size)\n        pixels[i] = [\n            round(r / (lut_size - 1) * 255),\n            round(g / (lut_size - 1) * 255),\n            round(b / (lut_size - 1) * 255),\n        ]\n\n    return Image.fromarray(pixels.reshape(dim, dim, 3))\n\n\ndef lut_from_hald(img: Image.Image) -> tuple[int, np.ndarray]:\n    \"\"\"Extract a 3D LUT from a processed Hald CLUT image.\n\n    Returns (lut_size, lut) where lut has shape (lut_size, lut_size, lut_size, 3)\n    with float values in [0, 1].\n    \"\"\"\n    level = hald_level_from_image(img)\n    lut_size = level * level\n    dim = level**3\n\n    pixels = np.array(img.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3) / 255.0\n    expected = dim * dim\n    if len(pixels) != expected:\n        raise ValueError(f\"Expected {expected} pixels, got {len(pixels)}\")\n\n    lut = np.zeros((lut_size, lut_size, lut_size, 3), dtype=np.float64)\n    for i in range(expected):\n        r = i % lut_size\n        g = (i // lut_size) % lut_size\n        b = i // (lut_size * lut_size)\n        lut[b, g, r] = pixels[i]\n\n    return lut_size, lut\n\n\ndef resample_lut(lut: np.ndarray, src_size: int, dst_size: int) -> np.ndarray:\n    \"\"\"Resample a 3D LUT to a different grid size via trilinear interpolation.\"\"\"\n    from scipy.interpolate import RegularGridInterpolator\n\n    axis = np.linspace(0, 1, src_size)\n    channels = []\n    for ch in range(3):\n        interp = RegularGridInterpolator(\n            (axis, axis, axis), lut[:, :, :, ch], method=\"linear\"\n        )\n        dst_axis = np.linspace(0, 1, dst_size)\n        grid = np.meshgrid(dst_axis, dst_axis, dst_axis, indexing=\"ij\")\n        pts = np.stack(grid, axis=-1).reshape(-1, 3)\n        channels.append(interp(pts).reshape(dst_size, dst_size, dst_size))\n\n    return np.clip(np.stack(channels, axis=-1), 0, 1)\n\n\n# ---------------------------------------------------------------------------\n# Before/after pair → LUT approximation\n# ---------------------------------------------------------------------------\n\n\ndef lut_from_pair(\n    original: Image.Image,\n    graded: Image.Image,\n    lut_size: int = 33,\n) -> tuple[int, np.ndarray]:\n    \"\"\"Approximate a 3D LUT by comparing original ↔ graded pixel pairs.\n\n    Bins every pixel by its original RGB value into a 3D grid, averages the\n    corresponding graded values per bin. Fills empty bins via nearest-neighbor\n    interpolation.\n    \"\"\"\n    if original.size != graded.size:\n        # resize graded to match original\n        graded = graded.resize(original.size, Image.LANCZOS)\n\n    orig_px = np.array(original.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3)\n    grad_px = np.array(graded.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3)\n\n    # Quantize original pixels into LUT bins\n    bin_indices = np.clip(\n        (orig_px / 255.0 * (lut_size - 1) + 0.5).astype(int), 0, lut_size - 1\n    )\n\n    # Accumulate\n    lut_sum = np.zeros((lut_size, lut_size, lut_size, 3), dtype=np.float64)\n    lut_count = np.zeros((lut_size, lut_size, lut_size), dtype=np.float64)\n\n    for idx in range(len(orig_px)):\n        r, g, b = bin_indices[idx]\n        lut_sum[b, g, r] += grad_px[idx]\n        lut_count[b, g, r] += 1\n\n    # Average filled bins\n    mask = lut_count > 0\n    lut = np.zeros_like(lut_sum)\n    lut[mask] = lut_sum[mask] / lut_count[mask, np.newaxis] / 255.0\n\n    # Fill empty bins via nearest-neighbor from filled bins\n    empty = ~mask\n    n_empty = empty.sum()\n    if n_empty > 0:\n        from scipy.ndimage import distance_transform_edt\n\n        # distance_transform_edt returns distances and indices of nearest filled cell\n        _, nearest_idx = distance_transform_edt(\n            empty, return_distances=True, return_indices=True\n        )\n        for b_i in range(lut_size):\n            for g_i in range(lut_size):\n                for r_i in range(lut_size):\n                    if empty[b_i, g_i, r_i]:\n                        nb, ng, nr = nearest_idx[:, b_i, g_i, r_i]\n                        lut[b_i, g_i, r_i] = lut[nb, ng, nr]\n\n    return lut_size, lut\n\n\n# ---------------------------------------------------------------------------\n# .cube export\n# ---------------------------------------------------------------------------\n\n\ndef write_cube(path: str, lut_size: int, lut: np.ndarray, title: str = \"Inferred LUT\"):\n    \"\"\"Write a 3D LUT in Adobe .cube format.\n\n    lut shape: (lut_size, lut_size, lut_size, 3) with values in [0, 1].\n    Iteration order: R fastest, then G, then B (standard .cube convention).\n    \"\"\"\n    with open(path, \"w\") as f:\n        f.write(f'TITLE \"{title}\"\\n')\n        f.write(f\"LUT_3D_SIZE {lut_size}\\n\")\n        f.write(f\"DOMAIN_MIN 0.0 0.0 0.0\\n\")\n        f.write(f\"DOMAIN_MAX 1.0 1.0 1.0\\n\\n\")\n\n        for b in range(lut_size):\n            for g in range(lut_size):\n                for r in range(lut_size):\n                    rv, gv, bv = lut[b, g, r]\n                    f.write(f\"{rv:.6f} {gv:.6f} {bv:.6f}\\n\")\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Infer a 3D color LUT from images.\",\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=__doc__,\n    )\n    sub = parser.add_subparsers(dest=\"command\", required=True)\n\n    # -- hald --\n    p_hald = sub.add_parser(\"hald\", help=\"Extract LUT from a processed Hald CLUT image\")\n    p_hald.add_argument(\"image\", help=\"Processed Hald CLUT image (PNG, TIFF, etc.)\")\n    p_hald.add_argument(\n        \"-o\", \"--output\", default=\"output.cube\", help=\"Output .cube file\"\n    )\n    p_hald.add_argument(\n        \"-s\",\n        \"--size\",\n        type=int,\n        default=35,\n        help=\"Output LUT grid size per axis (default 35)\",\n    )\n\n    # -- pair --\n    p_pair = sub.add_parser(\"pair\", help=\"Approximate LUT from original + graded pair\")\n    p_pair.add_argument(\"original\", help=\"Original (ungraded) image\")\n    p_pair.add_argument(\"graded\", help=\"Graded image\")\n    p_pair.add_argument(\n        \"-o\", \"--output\", default=\"output.cube\", help=\"Output .cube file\"\n    )\n    p_pair.add_argument(\n        \"-s\",\n        \"--size\",\n        type=int,\n        default=33,\n        help=\"LUT grid size per axis (default 33, industry standard)\",\n    )\n\n    # -- generate-hald --\n    p_gen = sub.add_parser(\"generate-hald\", help=\"Generate a neutral Hald CLUT image\")\n    p_gen.add_argument(\n        \"-s\",\n        \"--level\",\n        type=int,\n        default=8,\n        help=\"Hald level (default 8 → 512x512 image, 64-entry LUT)\",\n    )\n    p_gen.add_argument(\n        \"-o\", \"--output\", default=\"identity_hald.png\", help=\"Output image path\"\n    )\n\n    args = parser.parse_args()\n\n    if args.command == \"generate-hald\":\n        print(f\"Generating identity Hald CLUT (level {args.level})...\")\n        img = generate_hald(args.level)\n        img.save(args.output)\n        lut_size = args.level**2\n        print(f\"Saved {img.size[0]}x{img.size[1]} image → {args.output}\")\n        print(\n            f\"Resulting LUT will have {lut_size} entries per axis ({lut_size}^3 = {lut_size**3} total)\"\n        )\n\n    elif args.command == \"hald\":\n        print(f\"Reading Hald CLUT: {args.image}\")\n        img = Image.open(args.image)\n        lut_size, lut = lut_from_hald(img)\n        if args.size != lut_size:\n            print(f\"Resampling {lut_size}³ → {args.size}³...\")\n            lut = resample_lut(lut, lut_size, args.size)\n            lut_size = args.size\n        write_cube(\n            args.output,\n            lut_size,\n            lut,\n            title=f\"Hald-inferred from {Path(args.image).name}\",\n        )\n        print(f\"Extracted {lut_size}x{lut_size}x{lut_size} LUT → {args.output}\")\n\n    elif args.command == \"pair\":\n        print(f\"Original: {args.original}\")\n        print(f\"Graded:   {args.graded}\")\n        orig = Image.open(args.original)\n        grad = Image.open(args.graded)\n        lut_size, lut = lut_from_pair(orig, grad, args.size)\n        write_cube(\n            args.output,\n            lut_size,\n            lut,\n            title=f\"Pair-inferred from {Path(args.original).name} → {Path(args.graded).name}\",\n        )\n        print(f\"Approximated {lut_size}x{lut_size}x{lut_size} LUT → {args.output}\")\n\n    print(\"Done.\")\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n### Random\n\n- \"Dragging your shutter\" means using a low shutter speed for things like movement.\n"
    },
    {
      "authorEmail": "anand.nikhil@gmail.com",
      "authorName": "afreeorange",
      "date": "2026-08-04T20:51:16Z",
      "id": "3466f38a62ebe0c63fafb2bb05b981a59689d40c",
      "shortId": "3466f38a",
      "subject": "Add link to cube files\n",
      "content": "Assorted notes from me trying to teach myself Photography.\n\n## The Triangle\n\nIt's a balance between Aperture, Shutter Speed, and ISO.\n\n## Metering\n\nYour camera will try and advise you. People used to use light meters for this. It will say its a good exposure if it is \"18% Gray\" which is what light meters were historically calibrated to (the caps on Kodak film and the insides of dedicated camera bags are this gray too.)\n\n## The Histogram\n\n- Luminance value on x-axis and pixel count on y-axis.\n- Leftmost edge is Luminance of zero, Rightmost is Luminance of one. Any pixels touching these edges are lost information, they're just crushed out of existence. You can use Zebra stripes to tell you which parts of your image are/will be \"blown out\" in either direction.\n- You section it into, say, five equal portions. From L-R, you are looking at _blacks_, _shadows_, _midtones_, _highlights_, and _whites_.\n\nUsually you want a histogram that looks like a mountain or is kinda balanced out but depends on what you're trying to do, the look you're going for.\n\n## On Aperture\n\nMeasured by f-stop. \"f/2\" means you take the focal length and divide it by two.\n\n\"[f/8 and Be There.](https://www.youtube.com/watch?v=DQcFQ-2dtB0).\" Like _be there_. Your camera primarily exists to take pictures of things and events.\n\n## On ISO\n\n[See this video](https://www.youtube.com/watch?v=ZWSvHBG7X0w). TLDR: This is tricky so let the camera figure it out.\n\n## Tripods\n\n- Cheap, Stable, Light —- pick two. But don't be cheap. You'll change cameras before tripods.\n- [Read this](/assets/tripods-bythom.html) thoroughly.\n- Components are: Legs, Head, Plate, Bag\n  - Each component may be purchased separately.\n  - Buying a head doesn't necessarily include a plate.\n\n## Lenses\n\nThere are Prime lenses (fixed focal length) and Zoom lenses (can be wide, normal, and/or telephoto lenses).\n\nPrime lenses teach you a lot about photography because they make your lazy ass _move_ and _explore_ a scene: distances and angles.\n\n## Presets and LUTs\n\n- [Presetmator](https://presetmator.com/) -- Photomator/Pixelmator\n- [Bodega Supply](https://bodega.supply/collection/photo-presets) -- Photomator/Pixelmator\n- [FujifilmCameraProfiles](https://github.com/abpy/FujifilmCameraProfiles)\n- [Export LUT](https://johnrellis.com/lightroom/exportlut.htm) Utility\n\n### Hald CLUTs\n\n[These are great](https://github.com/cedeber/hald-clut)!. This gives you a bunch of PNGs that you can \"infer LUTs\" from. Quick Python script that will do this is at the end of this page.\n\nNow the LUT grid size with the PNGs is 144 x 144 x 144 = 2,985,984 which makes the sizes of the `.cube` files explode. That script will downsample to 32 x 32 x 32.\n\n[I've saved the generated `.cube` files here](https://public.nikhil.io/hald-clut-cubes.tgz).\n\n## Other\n\n- [What This NYT/Getty/AP Photojournalist Has Learned After More Than 25 Years in the Field](https://www.youtube.com/watch?v=Ky78k8j2-zc)\n- [How Metering Works](https://www.youtube.com/watch?v=qYGc7QPU3tI) -- A must-watch\n\n### Infer LUT from PNG Script\n\n```python\n#!/usr/bin/env python3\n\"\"\"Infer a 3D color LUT from images and export as .cube file.\n\nModes:\n  hald   — Extract LUT from a processed Hald CLUT image.\n  pair   — Approximate LUT from a before/after image pair.\n  generate-hald — Generate a neutral Hald CLUT image to use as a reference.\n\nUsage:\n  python infer_lut.py hald processed_hald.png -o my.cube\n  python infer_lut.py pair original.png graded.png -o my.cube\n  python infer_lut.py generate-hald -s 8 -o identity_hald.png\n\"\"\"\n\nimport argparse\nimport numpy as np\nfrom pathlib import Path\nfrom PIL import Image\n\n\n# ---------------------------------------------------------------------------\n# Hald CLUT helpers\n# ---------------------------------------------------------------------------\n\n\ndef hald_level_from_image(img: Image.Image) -> int:\n    \"\"\"Determine the Hald level from image dimensions.\n\n    A Hald CLUT of level N has dimensions (N^3) x (N^3).\n    The resulting 3D LUT has N^2 entries per axis.\n    Common levels: 4 → 64x64 image, 16-entry LUT\n                   8 → 512x512 image, 64-entry LUT\n                  12 → 1728x1728 image, 144-entry LUT\n    \"\"\"\n    w, h = img.size\n    if w != h:\n        raise ValueError(f\"Hald CLUT must be square, got {w}x{h}\")\n    # w = level^3\n    level = round(w ** (1 / 3))\n    if level**3 != w:\n        raise ValueError(\n            f\"Image size {w}x{h} is not a valid Hald CLUT dimension (need N^3 x N^3)\"\n        )\n    return level\n\n\ndef generate_hald(level: int) -> Image.Image:\n    \"\"\"Generate a neutral (identity) Hald CLUT image.\"\"\"\n    lut_size = level * level  # entries per axis in the 3D LUT\n    dim = level**3  # pixel width/height of the Hald image\n    pixels = np.zeros((dim * dim, 3), dtype=np.uint8)\n\n    for i in range(dim * dim):\n        r = i % lut_size\n        g = (i // lut_size) % lut_size\n        b = i // (lut_size * lut_size)\n        pixels[i] = [\n            round(r / (lut_size - 1) * 255),\n            round(g / (lut_size - 1) * 255),\n            round(b / (lut_size - 1) * 255),\n        ]\n\n    return Image.fromarray(pixels.reshape(dim, dim, 3))\n\n\ndef lut_from_hald(img: Image.Image) -> tuple[int, np.ndarray]:\n    \"\"\"Extract a 3D LUT from a processed Hald CLUT image.\n\n    Returns (lut_size, lut) where lut has shape (lut_size, lut_size, lut_size, 3)\n    with float values in [0, 1].\n    \"\"\"\n    level = hald_level_from_image(img)\n    lut_size = level * level\n    dim = level**3\n\n    pixels = np.array(img.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3) / 255.0\n    expected = dim * dim\n    if len(pixels) != expected:\n        raise ValueError(f\"Expected {expected} pixels, got {len(pixels)}\")\n\n    lut = np.zeros((lut_size, lut_size, lut_size, 3), dtype=np.float64)\n    for i in range(expected):\n        r = i % lut_size\n        g = (i // lut_size) % lut_size\n        b = i // (lut_size * lut_size)\n        lut[b, g, r] = pixels[i]\n\n    return lut_size, lut\n\n\ndef resample_lut(lut: np.ndarray, src_size: int, dst_size: int) -> np.ndarray:\n    \"\"\"Resample a 3D LUT to a different grid size via trilinear interpolation.\"\"\"\n    from scipy.interpolate import RegularGridInterpolator\n\n    axis = np.linspace(0, 1, src_size)\n    channels = []\n    for ch in range(3):\n        interp = RegularGridInterpolator(\n            (axis, axis, axis), lut[:, :, :, ch], method=\"linear\"\n        )\n        dst_axis = np.linspace(0, 1, dst_size)\n        grid = np.meshgrid(dst_axis, dst_axis, dst_axis, indexing=\"ij\")\n        pts = np.stack(grid, axis=-1).reshape(-1, 3)\n        channels.append(interp(pts).reshape(dst_size, dst_size, dst_size))\n\n    return np.clip(np.stack(channels, axis=-1), 0, 1)\n\n\n# ---------------------------------------------------------------------------\n# Before/after pair → LUT approximation\n# ---------------------------------------------------------------------------\n\n\ndef lut_from_pair(\n    original: Image.Image,\n    graded: Image.Image,\n    lut_size: int = 33,\n) -> tuple[int, np.ndarray]:\n    \"\"\"Approximate a 3D LUT by comparing original ↔ graded pixel pairs.\n\n    Bins every pixel by its original RGB value into a 3D grid, averages the\n    corresponding graded values per bin. Fills empty bins via nearest-neighbor\n    interpolation.\n    \"\"\"\n    if original.size != graded.size:\n        # resize graded to match original\n        graded = graded.resize(original.size, Image.LANCZOS)\n\n    orig_px = np.array(original.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3)\n    grad_px = np.array(graded.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3)\n\n    # Quantize original pixels into LUT bins\n    bin_indices = np.clip(\n        (orig_px / 255.0 * (lut_size - 1) + 0.5).astype(int), 0, lut_size - 1\n    )\n\n    # Accumulate\n    lut_sum = np.zeros((lut_size, lut_size, lut_size, 3), dtype=np.float64)\n    lut_count = np.zeros((lut_size, lut_size, lut_size), dtype=np.float64)\n\n    for idx in range(len(orig_px)):\n        r, g, b = bin_indices[idx]\n        lut_sum[b, g, r] += grad_px[idx]\n        lut_count[b, g, r] += 1\n\n    # Average filled bins\n    mask = lut_count > 0\n    lut = np.zeros_like(lut_sum)\n    lut[mask] = lut_sum[mask] / lut_count[mask, np.newaxis] / 255.0\n\n    # Fill empty bins via nearest-neighbor from filled bins\n    empty = ~mask\n    n_empty = empty.sum()\n    if n_empty > 0:\n        from scipy.ndimage import distance_transform_edt\n\n        # distance_transform_edt returns distances and indices of nearest filled cell\n        _, nearest_idx = distance_transform_edt(\n            empty, return_distances=True, return_indices=True\n        )\n        for b_i in range(lut_size):\n            for g_i in range(lut_size):\n                for r_i in range(lut_size):\n                    if empty[b_i, g_i, r_i]:\n                        nb, ng, nr = nearest_idx[:, b_i, g_i, r_i]\n                        lut[b_i, g_i, r_i] = lut[nb, ng, nr]\n\n    return lut_size, lut\n\n\n# ---------------------------------------------------------------------------\n# .cube export\n# ---------------------------------------------------------------------------\n\n\ndef write_cube(path: str, lut_size: int, lut: np.ndarray, title: str = \"Inferred LUT\"):\n    \"\"\"Write a 3D LUT in Adobe .cube format.\n\n    lut shape: (lut_size, lut_size, lut_size, 3) with values in [0, 1].\n    Iteration order: R fastest, then G, then B (standard .cube convention).\n    \"\"\"\n    with open(path, \"w\") as f:\n        f.write(f'TITLE \"{title}\"\\n')\n        f.write(f\"LUT_3D_SIZE {lut_size}\\n\")\n        f.write(f\"DOMAIN_MIN 0.0 0.0 0.0\\n\")\n        f.write(f\"DOMAIN_MAX 1.0 1.0 1.0\\n\\n\")\n\n        for b in range(lut_size):\n            for g in range(lut_size):\n                for r in range(lut_size):\n                    rv, gv, bv = lut[b, g, r]\n                    f.write(f\"{rv:.6f} {gv:.6f} {bv:.6f}\\n\")\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Infer a 3D color LUT from images.\",\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=__doc__,\n    )\n    sub = parser.add_subparsers(dest=\"command\", required=True)\n\n    # -- hald --\n    p_hald = sub.add_parser(\"hald\", help=\"Extract LUT from a processed Hald CLUT image\")\n    p_hald.add_argument(\"image\", help=\"Processed Hald CLUT image (PNG, TIFF, etc.)\")\n    p_hald.add_argument(\n        \"-o\", \"--output\", default=\"output.cube\", help=\"Output .cube file\"\n    )\n    p_hald.add_argument(\n        \"-s\",\n        \"--size\",\n        type=int,\n        default=35,\n        help=\"Output LUT grid size per axis (default 35)\",\n    )\n\n    # -- pair --\n    p_pair = sub.add_parser(\"pair\", help=\"Approximate LUT from original + graded pair\")\n    p_pair.add_argument(\"original\", help=\"Original (ungraded) image\")\n    p_pair.add_argument(\"graded\", help=\"Graded image\")\n    p_pair.add_argument(\n        \"-o\", \"--output\", default=\"output.cube\", help=\"Output .cube file\"\n    )\n    p_pair.add_argument(\n        \"-s\",\n        \"--size\",\n        type=int,\n        default=33,\n        help=\"LUT grid size per axis (default 33, industry standard)\",\n    )\n\n    # -- generate-hald --\n    p_gen = sub.add_parser(\"generate-hald\", help=\"Generate a neutral Hald CLUT image\")\n    p_gen.add_argument(\n        \"-s\",\n        \"--level\",\n        type=int,\n        default=8,\n        help=\"Hald level (default 8 → 512x512 image, 64-entry LUT)\",\n    )\n    p_gen.add_argument(\n        \"-o\", \"--output\", default=\"identity_hald.png\", help=\"Output image path\"\n    )\n\n    args = parser.parse_args()\n\n    if args.command == \"generate-hald\":\n        print(f\"Generating identity Hald CLUT (level {args.level})...\")\n        img = generate_hald(args.level)\n        img.save(args.output)\n        lut_size = args.level**2\n        print(f\"Saved {img.size[0]}x{img.size[1]} image → {args.output}\")\n        print(\n            f\"Resulting LUT will have {lut_size} entries per axis ({lut_size}^3 = {lut_size**3} total)\"\n        )\n\n    elif args.command == \"hald\":\n        print(f\"Reading Hald CLUT: {args.image}\")\n        img = Image.open(args.image)\n        lut_size, lut = lut_from_hald(img)\n        if args.size != lut_size:\n            print(f\"Resampling {lut_size}³ → {args.size}³...\")\n            lut = resample_lut(lut, lut_size, args.size)\n            lut_size = args.size\n        write_cube(\n            args.output,\n            lut_size,\n            lut,\n            title=f\"Hald-inferred from {Path(args.image).name}\",\n        )\n        print(f\"Extracted {lut_size}x{lut_size}x{lut_size} LUT → {args.output}\")\n\n    elif args.command == \"pair\":\n        print(f\"Original: {args.original}\")\n        print(f\"Graded:   {args.graded}\")\n        orig = Image.open(args.original)\n        grad = Image.open(args.graded)\n        lut_size, lut = lut_from_pair(orig, grad, args.size)\n        write_cube(\n            args.output,\n            lut_size,\n            lut,\n            title=f\"Pair-inferred from {Path(args.original).name} → {Path(args.graded).name}\",\n        )\n        print(f\"Approximated {lut_size}x{lut_size}x{lut_size} LUT → {args.output}\")\n\n    print(\"Done.\")\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n"
    },
    {
      "authorEmail": "anand.nikhil@gmail.com",
      "authorName": "afreeorange",
      "date": "2026-08-04T20:39:12Z",
      "id": "42b3b55afcca1d933b32b84144fb8c9abd2fd8a7",
      "shortId": "42b3b55a",
      "subject": "Hald CLUTs + reorg\n",
      "content": "Assorted notes from me trying to teach myself Photography.\n\n## The Triangle\n\nIt's a balance between Aperture, Shutter Speed, and ISO.\n\n## Metering\n\nYour camera will try and advise you. People used to use light meters for this. It will say its a good exposure if it is \"18% Gray\" which is what light meters were historically calibrated to (the caps on Kodak film and the insides of dedicated camera bags are this gray too.)\n\n## The Histogram\n\n- Luminance value on x-axis and pixel count on y-axis.\n- Leftmost edge is Luminance of zero, Rightmost is Luminance of one. Any pixels touching these edges are lost information, they're just crushed out of existence. You can use Zebra stripes to tell you which parts of your image are/will be \"blown out\" in either direction.\n- You section it into, say, five equal portions. From L-R, you are looking at _blacks_, _shadows_, _midtones_, _highlights_, and _whites_.\n\nUsually you want a histogram that looks like a mountain or is kinda balanced out but depends on what you're trying to do, the look you're going for.\n\n## On Aperture\n\nMeasured by f-stop. \"f/2\" means you take the focal length and divide it by two.\n\n\"[f/8 and Be There.](https://www.youtube.com/watch?v=DQcFQ-2dtB0).\" Like _be there_. Your camera primarily exists to take pictures of things and events.\n\n## On ISO\n\n[See this video](https://www.youtube.com/watch?v=ZWSvHBG7X0w). TLDR: This is tricky so let the camera figure it out.\n\n## Tripods\n\n- Cheap, Stable, Light —- pick two. But don't be cheap. You'll change cameras before tripods.\n- [Read this](/assets/tripods-bythom.html) thoroughly.\n- Components are: Legs, Head, Plate, Bag\n  - Each component may be purchased separately.\n  - Buying a head doesn't necessarily include a plate.\n\n## Lenses\n\nThere are Prime lenses (fixed focal length) and Zoom lenses (can be wide, normal, and/or telephoto lenses).\n\nPrime lenses teach you a lot about photography because they make your lazy ass _move_ and _explore_ a scene: distances and angles.\n\n## Presets and LUTs\n\n- [Presetmator](https://presetmator.com/) -- Photomator/Pixelmator\n- [Bodega Supply](https://bodega.supply/collection/photo-presets) -- Photomator/Pixelmator\n- [FujifilmCameraProfiles](https://github.com/abpy/FujifilmCameraProfiles)\n- [Export LUT](https://johnrellis.com/lightroom/exportlut.htm) Utility\n\n### Hald CLUTs\n\n[These are great](https://github.com/cedeber/hald-clut)!. This gives you a bunch of PNGs that you can \"infer LUTs\" from. Quick Python script that will do this is at the end of this page.\n\nNow the LUT grid size with the PNGs is 144 x 144 x 144 = 2,985,984 which makes the sizes of the `.cube` files explode. That script will downsample to 32 x 32 x 32.\n\n## Other\n\n- [What This NYT/Getty/AP Photojournalist Has Learned After More Than 25 Years in the Field](https://www.youtube.com/watch?v=Ky78k8j2-zc)\n- [How Metering Works](https://www.youtube.com/watch?v=qYGc7QPU3tI) -- A must-watch\n\n### Infer LUT from PNG Script\n\n```python\n#!/usr/bin/env python3\n\"\"\"Infer a 3D color LUT from images and export as .cube file.\n\nModes:\n  hald   — Extract LUT from a processed Hald CLUT image.\n  pair   — Approximate LUT from a before/after image pair.\n  generate-hald — Generate a neutral Hald CLUT image to use as a reference.\n\nUsage:\n  python infer_lut.py hald processed_hald.png -o my.cube\n  python infer_lut.py pair original.png graded.png -o my.cube\n  python infer_lut.py generate-hald -s 8 -o identity_hald.png\n\"\"\"\n\nimport argparse\nimport numpy as np\nfrom pathlib import Path\nfrom PIL import Image\n\n\n# ---------------------------------------------------------------------------\n# Hald CLUT helpers\n# ---------------------------------------------------------------------------\n\n\ndef hald_level_from_image(img: Image.Image) -> int:\n    \"\"\"Determine the Hald level from image dimensions.\n\n    A Hald CLUT of level N has dimensions (N^3) x (N^3).\n    The resulting 3D LUT has N^2 entries per axis.\n    Common levels: 4 → 64x64 image, 16-entry LUT\n                   8 → 512x512 image, 64-entry LUT\n                  12 → 1728x1728 image, 144-entry LUT\n    \"\"\"\n    w, h = img.size\n    if w != h:\n        raise ValueError(f\"Hald CLUT must be square, got {w}x{h}\")\n    # w = level^3\n    level = round(w ** (1 / 3))\n    if level**3 != w:\n        raise ValueError(\n            f\"Image size {w}x{h} is not a valid Hald CLUT dimension (need N^3 x N^3)\"\n        )\n    return level\n\n\ndef generate_hald(level: int) -> Image.Image:\n    \"\"\"Generate a neutral (identity) Hald CLUT image.\"\"\"\n    lut_size = level * level  # entries per axis in the 3D LUT\n    dim = level**3  # pixel width/height of the Hald image\n    pixels = np.zeros((dim * dim, 3), dtype=np.uint8)\n\n    for i in range(dim * dim):\n        r = i % lut_size\n        g = (i // lut_size) % lut_size\n        b = i // (lut_size * lut_size)\n        pixels[i] = [\n            round(r / (lut_size - 1) * 255),\n            round(g / (lut_size - 1) * 255),\n            round(b / (lut_size - 1) * 255),\n        ]\n\n    return Image.fromarray(pixels.reshape(dim, dim, 3))\n\n\ndef lut_from_hald(img: Image.Image) -> tuple[int, np.ndarray]:\n    \"\"\"Extract a 3D LUT from a processed Hald CLUT image.\n\n    Returns (lut_size, lut) where lut has shape (lut_size, lut_size, lut_size, 3)\n    with float values in [0, 1].\n    \"\"\"\n    level = hald_level_from_image(img)\n    lut_size = level * level\n    dim = level**3\n\n    pixels = np.array(img.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3) / 255.0\n    expected = dim * dim\n    if len(pixels) != expected:\n        raise ValueError(f\"Expected {expected} pixels, got {len(pixels)}\")\n\n    lut = np.zeros((lut_size, lut_size, lut_size, 3), dtype=np.float64)\n    for i in range(expected):\n        r = i % lut_size\n        g = (i // lut_size) % lut_size\n        b = i // (lut_size * lut_size)\n        lut[b, g, r] = pixels[i]\n\n    return lut_size, lut\n\n\ndef resample_lut(lut: np.ndarray, src_size: int, dst_size: int) -> np.ndarray:\n    \"\"\"Resample a 3D LUT to a different grid size via trilinear interpolation.\"\"\"\n    from scipy.interpolate import RegularGridInterpolator\n\n    axis = np.linspace(0, 1, src_size)\n    channels = []\n    for ch in range(3):\n        interp = RegularGridInterpolator(\n            (axis, axis, axis), lut[:, :, :, ch], method=\"linear\"\n        )\n        dst_axis = np.linspace(0, 1, dst_size)\n        grid = np.meshgrid(dst_axis, dst_axis, dst_axis, indexing=\"ij\")\n        pts = np.stack(grid, axis=-1).reshape(-1, 3)\n        channels.append(interp(pts).reshape(dst_size, dst_size, dst_size))\n\n    return np.clip(np.stack(channels, axis=-1), 0, 1)\n\n\n# ---------------------------------------------------------------------------\n# Before/after pair → LUT approximation\n# ---------------------------------------------------------------------------\n\n\ndef lut_from_pair(\n    original: Image.Image,\n    graded: Image.Image,\n    lut_size: int = 33,\n) -> tuple[int, np.ndarray]:\n    \"\"\"Approximate a 3D LUT by comparing original ↔ graded pixel pairs.\n\n    Bins every pixel by its original RGB value into a 3D grid, averages the\n    corresponding graded values per bin. Fills empty bins via nearest-neighbor\n    interpolation.\n    \"\"\"\n    if original.size != graded.size:\n        # resize graded to match original\n        graded = graded.resize(original.size, Image.LANCZOS)\n\n    orig_px = np.array(original.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3)\n    grad_px = np.array(graded.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3)\n\n    # Quantize original pixels into LUT bins\n    bin_indices = np.clip(\n        (orig_px / 255.0 * (lut_size - 1) + 0.5).astype(int), 0, lut_size - 1\n    )\n\n    # Accumulate\n    lut_sum = np.zeros((lut_size, lut_size, lut_size, 3), dtype=np.float64)\n    lut_count = np.zeros((lut_size, lut_size, lut_size), dtype=np.float64)\n\n    for idx in range(len(orig_px)):\n        r, g, b = bin_indices[idx]\n        lut_sum[b, g, r] += grad_px[idx]\n        lut_count[b, g, r] += 1\n\n    # Average filled bins\n    mask = lut_count > 0\n    lut = np.zeros_like(lut_sum)\n    lut[mask] = lut_sum[mask] / lut_count[mask, np.newaxis] / 255.0\n\n    # Fill empty bins via nearest-neighbor from filled bins\n    empty = ~mask\n    n_empty = empty.sum()\n    if n_empty > 0:\n        from scipy.ndimage import distance_transform_edt\n\n        # distance_transform_edt returns distances and indices of nearest filled cell\n        _, nearest_idx = distance_transform_edt(\n            empty, return_distances=True, return_indices=True\n        )\n        for b_i in range(lut_size):\n            for g_i in range(lut_size):\n                for r_i in range(lut_size):\n                    if empty[b_i, g_i, r_i]:\n                        nb, ng, nr = nearest_idx[:, b_i, g_i, r_i]\n                        lut[b_i, g_i, r_i] = lut[nb, ng, nr]\n\n    return lut_size, lut\n\n\n# ---------------------------------------------------------------------------\n# .cube export\n# ---------------------------------------------------------------------------\n\n\ndef write_cube(path: str, lut_size: int, lut: np.ndarray, title: str = \"Inferred LUT\"):\n    \"\"\"Write a 3D LUT in Adobe .cube format.\n\n    lut shape: (lut_size, lut_size, lut_size, 3) with values in [0, 1].\n    Iteration order: R fastest, then G, then B (standard .cube convention).\n    \"\"\"\n    with open(path, \"w\") as f:\n        f.write(f'TITLE \"{title}\"\\n')\n        f.write(f\"LUT_3D_SIZE {lut_size}\\n\")\n        f.write(f\"DOMAIN_MIN 0.0 0.0 0.0\\n\")\n        f.write(f\"DOMAIN_MAX 1.0 1.0 1.0\\n\\n\")\n\n        for b in range(lut_size):\n            for g in range(lut_size):\n                for r in range(lut_size):\n                    rv, gv, bv = lut[b, g, r]\n                    f.write(f\"{rv:.6f} {gv:.6f} {bv:.6f}\\n\")\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Infer a 3D color LUT from images.\",\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=__doc__,\n    )\n    sub = parser.add_subparsers(dest=\"command\", required=True)\n\n    # -- hald --\n    p_hald = sub.add_parser(\"hald\", help=\"Extract LUT from a processed Hald CLUT image\")\n    p_hald.add_argument(\"image\", help=\"Processed Hald CLUT image (PNG, TIFF, etc.)\")\n    p_hald.add_argument(\n        \"-o\", \"--output\", default=\"output.cube\", help=\"Output .cube file\"\n    )\n    p_hald.add_argument(\n        \"-s\",\n        \"--size\",\n        type=int,\n        default=35,\n        help=\"Output LUT grid size per axis (default 35)\",\n    )\n\n    # -- pair --\n    p_pair = sub.add_parser(\"pair\", help=\"Approximate LUT from original + graded pair\")\n    p_pair.add_argument(\"original\", help=\"Original (ungraded) image\")\n    p_pair.add_argument(\"graded\", help=\"Graded image\")\n    p_pair.add_argument(\n        \"-o\", \"--output\", default=\"output.cube\", help=\"Output .cube file\"\n    )\n    p_pair.add_argument(\n        \"-s\",\n        \"--size\",\n        type=int,\n        default=33,\n        help=\"LUT grid size per axis (default 33, industry standard)\",\n    )\n\n    # -- generate-hald --\n    p_gen = sub.add_parser(\"generate-hald\", help=\"Generate a neutral Hald CLUT image\")\n    p_gen.add_argument(\n        \"-s\",\n        \"--level\",\n        type=int,\n        default=8,\n        help=\"Hald level (default 8 → 512x512 image, 64-entry LUT)\",\n    )\n    p_gen.add_argument(\n        \"-o\", \"--output\", default=\"identity_hald.png\", help=\"Output image path\"\n    )\n\n    args = parser.parse_args()\n\n    if args.command == \"generate-hald\":\n        print(f\"Generating identity Hald CLUT (level {args.level})...\")\n        img = generate_hald(args.level)\n        img.save(args.output)\n        lut_size = args.level**2\n        print(f\"Saved {img.size[0]}x{img.size[1]} image → {args.output}\")\n        print(\n            f\"Resulting LUT will have {lut_size} entries per axis ({lut_size}^3 = {lut_size**3} total)\"\n        )\n\n    elif args.command == \"hald\":\n        print(f\"Reading Hald CLUT: {args.image}\")\n        img = Image.open(args.image)\n        lut_size, lut = lut_from_hald(img)\n        if args.size != lut_size:\n            print(f\"Resampling {lut_size}³ → {args.size}³...\")\n            lut = resample_lut(lut, lut_size, args.size)\n            lut_size = args.size\n        write_cube(\n            args.output,\n            lut_size,\n            lut,\n            title=f\"Hald-inferred from {Path(args.image).name}\",\n        )\n        print(f\"Extracted {lut_size}x{lut_size}x{lut_size} LUT → {args.output}\")\n\n    elif args.command == \"pair\":\n        print(f\"Original: {args.original}\")\n        print(f\"Graded:   {args.graded}\")\n        orig = Image.open(args.original)\n        grad = Image.open(args.graded)\n        lut_size, lut = lut_from_pair(orig, grad, args.size)\n        write_cube(\n            args.output,\n            lut_size,\n            lut,\n            title=f\"Pair-inferred from {Path(args.original).name} → {Path(args.graded).name}\",\n        )\n        print(f\"Approximated {lut_size}x{lut_size}x{lut_size} LUT → {args.output}\")\n\n    print(\"Done.\")\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n"
    },
    {
      "authorEmail": "anand.nikhil@gmail.com",
      "authorName": "afreeorange",
      "date": "2026-07-26T23:46:03Z",
      "id": "68a4c4b0d884962bd9068eabb592c7a25df259aa",
      "shortId": "68a4c4b0",
      "subject": "Clarify histo regions\n",
      "content": "Assorted notes from me trying to teach myself Photography.\n\n## The Triangle\n\nIt's a balance between Aperture, Shutter Speed, and ISO.\n\n## Metering\n\nYour camera will try and advise you. People used to use light meters for this. It will say its a good exposure if it is \"18% Gray\" which is what light meters were historically calibrated to (the caps on Kodak film and the insides of dedicated camera bags are this gray too.)\n\n## The Histogram\n\n- Luminance value on x-axis and pixel count on y-axis.\n- Leftmost edge is Luminance of zero, Rightmost is Luminance of one. Any pixels touching these edges are lost information, they're just crushed out of existence. You can use Zebra stripes to tell you which parts of your image are/will be \"blown out\" in either direction.\n- You section it into, say, five equal portions. From L-R, you are looking at _blacks_, _shadows_, _midtones_, _highlights_, and _whites_.\n\nUsually you want a histogram that looks like a mountain or is kinda balanced out but depends on what you're trying to do, the look you're going for.\n\n## On Aperture\n\nMeasured by f-stop. \"f/2\" means you take the focal length and divide it by two.\n\n\"[f/8 and Be There.](https://www.youtube.com/watch?v=DQcFQ-2dtB0).\" Like _be there_. Your camera primarily exists to take pictures of things and events.\n\n## On ISO\n\n[See this video](https://www.youtube.com/watch?v=ZWSvHBG7X0w). TLDR: This is tricky so let the camera figure it out.\n\n## Tripods\n\n- Cheap, Stable, Light —- pick two. But don't be cheap. You'll change cameras before tripods.\n- [Read this](/assets/tripods-bythom.html) thoroughly.\n- Components are: Legs, Head, Plate, Bag\n  - Each component may be purchased separately.\n  - Buying a head doesn't necessarily include a plate.\n\n## Presets and LUTs\n\nFor Photomator/Pixelmator\n\n- [Presetmator](https://presetmator.com/)\n- [Bodega Supply](https://bodega.supply/collection/photo-presets)\n\nOther\n\n- [FujifilmCameraProfiles](https://github.com/abpy/FujifilmCameraProfiles)\n- [Export LUT](https://johnrellis.com/lightroom/exportlut.htm) Utility\n- [How Metering Works](https://www.youtube.com/watch?v=qYGc7QPU3tI) -- A must-watch\n\n## Lenses\n\nThere are Prime lenses (fixed focal length) and Zoom lenses (can be wide, normal, and/or telephoto lenses).\n\nPrime lenses teach you a lot about photography because they make your lazy ass _move_ and _explore_ a scene: distances and angles.\n\n## Other\n\n- [What This NYT/Getty/AP Photojournalist Has Learned After More Than 25 Years in the Field](https://www.youtube.com/watch?v=Ky78k8j2-zc)\n"
    },
    {
      "authorEmail": "anand.nikhil@gmail.com",
      "authorName": "afreeorange",
      "date": "2026-07-26T22:18:09Z",
      "id": "89789ad57e3a3147a95887f7daee276f55790483",
      "shortId": "89789ad5",
      "subject": "New incomplete sections\n",
      "content": "Assorted notes from me trying to teach myself Photography.\n\n## The Triangle\n\nIt's a balance between Aperture, Shutter Speed, and ISO.\n\n## Metering\n\nYour camera will try and advise you. People used to use light meters for this. It will say its a good exposure if it is \"18% Gray\" which is what light meters were historically calibrated to (the caps on Kodak film and the insides of dedicated camera bags are this gray too.)\n\n## The Histogram\n\n## On Aperture\n\nMeasured by f-stop. \"f/2\" means you take the focal length and divide it by two.\n\n\"[f/8 and Be There.](https://www.youtube.com/watch?v=DQcFQ-2dtB0).\" Like _be there_. Your camera primarily exists to take pictures of things and events.\n\n## On ISO\n\n[See this video](https://www.youtube.com/watch?v=ZWSvHBG7X0w). TLDR: This is tricky so let the camera figure it out.\n\n## Tripods\n\n- Cheap, Stable, Light —- pick two. But don't be cheap. You'll change cameras before tripods.\n- [Read this](/assets/tripods-bythom.html) thoroughly.\n- Components are: Legs, Head, Plate, Bag\n  - Each component may be purchased separately.\n  - Buying a head doesn't necessarily include a plate.\n\n## Presets and LUTs\n\nFor Photomator/Pixelmator\n\n- [Presetmator](https://presetmator.com/)\n- [Bodega Supply](https://bodega.supply/collection/photo-presets)\n\nOther\n\n- [FujifilmCameraProfiles](https://github.com/abpy/FujifilmCameraProfiles)\n- [Export LUT](https://johnrellis.com/lightroom/exportlut.htm) Utility\n- [How Metering Works](https://www.youtube.com/watch?v=qYGc7QPU3tI) -- A must-watch\n\n## Lenses\n\nThere are Prime lenses (fixed focal length) and Zoom lenses (can be wide, normal, and/or telephoto lenses).\n\nPrime lenses teach you a lot about photography because they make your lazy ass _move_ and _explore_ a scene: distances and angles.\n\n## Other\n\n- [What This NYT/Getty/AP Photojournalist Has Learned After More Than 25 Years in the Field](https://www.youtube.com/watch?v=Ky78k8j2-zc)\n"
    },
    {
      "authorEmail": "mail@nikhil.io",
      "authorName": "Nikhil Anand",
      "date": "2026-07-04T14:07:30Z",
      "id": "b9ab94ea8b06b7ec3e3662f309aa5171e3701c00",
      "shortId": "b9ab94ea",
      "subject": "Initial commit\n",
      "content": "Assorted notes from me trying to teach myself Photography.\n\n## On ISO\n\n[See this video](https://www.youtube.com/watch?v=ZWSvHBG7X0w). TLDR: This is tricky so let the camera figure it out.\n\n## Tripods\n\n- Cheap, Stable, Light — pick two. But don't be cheap. You'll change cameras before tripods.\n- [Read this](/assets/tripods-bythom.html) thoroughly.\n- Components are: Legs, Head, Plate, Bag\n  - Each component may be purchased separately.\n  - Buying a head doesn't necessarily include a plate.\n"
    }
  ],
  "sizeInBytes": 13330,
  "source": "Assorted notes from me trying to teach myself Photography.\n\n## The Triangle\n\nIt's a balance between Aperture, Shutter Speed, and ISO.\n\n## Metering\n\nYour camera will try and advise you. People used to use light meters for this. It will say its a good exposure if it is \"18% Gray\" which is what light meters were historically calibrated to (the caps on Kodak film and the insides of dedicated camera bags are this gray too.)\n\n## The Histogram\n\n- Luminance value on x-axis and pixel count on y-axis.\n- Leftmost edge is Luminance of zero, Rightmost is Luminance of one. Any pixels touching these edges are lost information, they're just crushed out of existence. You can use Zebra stripes to tell you which parts of your image are/will be \"blown out\" in either direction.\n- You section it into, say, five equal portions. From L-R, you are looking at _blacks_, _shadows_, _midtones_, _highlights_, and _whites_.\n\nUsually you want a histogram that looks like a mountain or is kinda balanced out but depends on what you're trying to do, the look you're going for.\n\n## On Aperture\n\nMeasured by f-stop. \"f/2\" means you take the focal length and divide it by two.\n\n\"[f/8 and Be There.](https://www.youtube.com/watch?v=DQcFQ-2dtB0).\" Like _be there_. Your camera primarily exists to take pictures of things and events.\n\n## On ISO\n\n[See this video](https://www.youtube.com/watch?v=ZWSvHBG7X0w). TLDR: This is tricky so let the camera figure it out.\n\n## Tripods\n\n- Cheap, Stable, Light —- pick two. But don't be cheap. You'll change cameras before tripods.\n- [Read this](/assets/tripods-bythom.html) thoroughly.\n- Components are: Legs, Head, Plate, Bag\n  - Each component may be purchased separately.\n  - Buying a head doesn't necessarily include a plate.\n\n## Lenses\n\nThere are Prime lenses (fixed focal length) and Zoom lenses (can be wide, normal, and/or telephoto lenses).\n\nPrime lenses teach you a lot about photography because they make your lazy ass _move_ and _explore_ a scene: distances and angles.\n\n## Presets and LUTs\n\n- [Presetmator](https://presetmator.com/) -- Photomator/Pixelmator\n- [Bodega Supply](https://bodega.supply/collection/photo-presets) -- Photomator/Pixelmator\n- [FujifilmCameraProfiles](https://github.com/abpy/FujifilmCameraProfiles)\n- [Export LUT](https://johnrellis.com/lightroom/exportlut.htm) Utility\n\n### Hald CLUTs\n\n[These are great](https://github.com/cedeber/hald-clut)!. This gives you a bunch of PNGs that you can \"infer LUTs\" from. Quick Python script that will do this is at the end of this page.\n\nNow the LUT grid size with the PNGs is 144 x 144 x 144 = 2,985,984 which makes the sizes of the `.cube` files explode. That script will downsample to 32 x 32 x 32.\n\n[I've saved the generated `.cube` files here](https://public.nikhil.io/hald-clut-cubes.tgz).\n\n## Other\n\n- [What This NYT/Getty/AP Photojournalist Has Learned After More Than 25 Years in the Field](https://www.youtube.com/watch?v=Ky78k8j2-zc)\n- [How Metering Works](https://www.youtube.com/watch?v=qYGc7QPU3tI) -- A must-watch\n\n### Infer LUT from PNG Script\n\n```python\n#!/usr/bin/env python3\n\"\"\"Infer a 3D color LUT from images and export as .cube file.\n\nModes:\n  hald   — Extract LUT from a processed Hald CLUT image.\n  pair   — Approximate LUT from a before/after image pair.\n  generate-hald — Generate a neutral Hald CLUT image to use as a reference.\n\nUsage:\n  python infer_lut.py hald processed_hald.png -o my.cube\n  python infer_lut.py pair original.png graded.png -o my.cube\n  python infer_lut.py generate-hald -s 8 -o identity_hald.png\n\"\"\"\n\nimport argparse\nimport numpy as np\nfrom pathlib import Path\nfrom PIL import Image\n\n\n# ---------------------------------------------------------------------------\n# Hald CLUT helpers\n# ---------------------------------------------------------------------------\n\n\ndef hald_level_from_image(img: Image.Image) -> int:\n    \"\"\"Determine the Hald level from image dimensions.\n\n    A Hald CLUT of level N has dimensions (N^3) x (N^3).\n    The resulting 3D LUT has N^2 entries per axis.\n    Common levels: 4 → 64x64 image, 16-entry LUT\n                   8 → 512x512 image, 64-entry LUT\n                  12 → 1728x1728 image, 144-entry LUT\n    \"\"\"\n    w, h = img.size\n    if w != h:\n        raise ValueError(f\"Hald CLUT must be square, got {w}x{h}\")\n    # w = level^3\n    level = round(w ** (1 / 3))\n    if level**3 != w:\n        raise ValueError(\n            f\"Image size {w}x{h} is not a valid Hald CLUT dimension (need N^3 x N^3)\"\n        )\n    return level\n\n\ndef generate_hald(level: int) -> Image.Image:\n    \"\"\"Generate a neutral (identity) Hald CLUT image.\"\"\"\n    lut_size = level * level  # entries per axis in the 3D LUT\n    dim = level**3  # pixel width/height of the Hald image\n    pixels = np.zeros((dim * dim, 3), dtype=np.uint8)\n\n    for i in range(dim * dim):\n        r = i % lut_size\n        g = (i // lut_size) % lut_size\n        b = i // (lut_size * lut_size)\n        pixels[i] = [\n            round(r / (lut_size - 1) * 255),\n            round(g / (lut_size - 1) * 255),\n            round(b / (lut_size - 1) * 255),\n        ]\n\n    return Image.fromarray(pixels.reshape(dim, dim, 3))\n\n\ndef lut_from_hald(img: Image.Image) -> tuple[int, np.ndarray]:\n    \"\"\"Extract a 3D LUT from a processed Hald CLUT image.\n\n    Returns (lut_size, lut) where lut has shape (lut_size, lut_size, lut_size, 3)\n    with float values in [0, 1].\n    \"\"\"\n    level = hald_level_from_image(img)\n    lut_size = level * level\n    dim = level**3\n\n    pixels = np.array(img.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3) / 255.0\n    expected = dim * dim\n    if len(pixels) != expected:\n        raise ValueError(f\"Expected {expected} pixels, got {len(pixels)}\")\n\n    lut = np.zeros((lut_size, lut_size, lut_size, 3), dtype=np.float64)\n    for i in range(expected):\n        r = i % lut_size\n        g = (i // lut_size) % lut_size\n        b = i // (lut_size * lut_size)\n        lut[b, g, r] = pixels[i]\n\n    return lut_size, lut\n\n\ndef resample_lut(lut: np.ndarray, src_size: int, dst_size: int) -> np.ndarray:\n    \"\"\"Resample a 3D LUT to a different grid size via trilinear interpolation.\"\"\"\n    from scipy.interpolate import RegularGridInterpolator\n\n    axis = np.linspace(0, 1, src_size)\n    channels = []\n    for ch in range(3):\n        interp = RegularGridInterpolator(\n            (axis, axis, axis), lut[:, :, :, ch], method=\"linear\"\n        )\n        dst_axis = np.linspace(0, 1, dst_size)\n        grid = np.meshgrid(dst_axis, dst_axis, dst_axis, indexing=\"ij\")\n        pts = np.stack(grid, axis=-1).reshape(-1, 3)\n        channels.append(interp(pts).reshape(dst_size, dst_size, dst_size))\n\n    return np.clip(np.stack(channels, axis=-1), 0, 1)\n\n\n# ---------------------------------------------------------------------------\n# Before/after pair → LUT approximation\n# ---------------------------------------------------------------------------\n\n\ndef lut_from_pair(\n    original: Image.Image,\n    graded: Image.Image,\n    lut_size: int = 33,\n) -> tuple[int, np.ndarray]:\n    \"\"\"Approximate a 3D LUT by comparing original ↔ graded pixel pairs.\n\n    Bins every pixel by its original RGB value into a 3D grid, averages the\n    corresponding graded values per bin. Fills empty bins via nearest-neighbor\n    interpolation.\n    \"\"\"\n    if original.size != graded.size:\n        # resize graded to match original\n        graded = graded.resize(original.size, Image.LANCZOS)\n\n    orig_px = np.array(original.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3)\n    grad_px = np.array(graded.convert(\"RGB\"), dtype=np.float64).reshape(-1, 3)\n\n    # Quantize original pixels into LUT bins\n    bin_indices = np.clip(\n        (orig_px / 255.0 * (lut_size - 1) + 0.5).astype(int), 0, lut_size - 1\n    )\n\n    # Accumulate\n    lut_sum = np.zeros((lut_size, lut_size, lut_size, 3), dtype=np.float64)\n    lut_count = np.zeros((lut_size, lut_size, lut_size), dtype=np.float64)\n\n    for idx in range(len(orig_px)):\n        r, g, b = bin_indices[idx]\n        lut_sum[b, g, r] += grad_px[idx]\n        lut_count[b, g, r] += 1\n\n    # Average filled bins\n    mask = lut_count > 0\n    lut = np.zeros_like(lut_sum)\n    lut[mask] = lut_sum[mask] / lut_count[mask, np.newaxis] / 255.0\n\n    # Fill empty bins via nearest-neighbor from filled bins\n    empty = ~mask\n    n_empty = empty.sum()\n    if n_empty > 0:\n        from scipy.ndimage import distance_transform_edt\n\n        # distance_transform_edt returns distances and indices of nearest filled cell\n        _, nearest_idx = distance_transform_edt(\n            empty, return_distances=True, return_indices=True\n        )\n        for b_i in range(lut_size):\n            for g_i in range(lut_size):\n                for r_i in range(lut_size):\n                    if empty[b_i, g_i, r_i]:\n                        nb, ng, nr = nearest_idx[:, b_i, g_i, r_i]\n                        lut[b_i, g_i, r_i] = lut[nb, ng, nr]\n\n    return lut_size, lut\n\n\n# ---------------------------------------------------------------------------\n# .cube export\n# ---------------------------------------------------------------------------\n\n\ndef write_cube(path: str, lut_size: int, lut: np.ndarray, title: str = \"Inferred LUT\"):\n    \"\"\"Write a 3D LUT in Adobe .cube format.\n\n    lut shape: (lut_size, lut_size, lut_size, 3) with values in [0, 1].\n    Iteration order: R fastest, then G, then B (standard .cube convention).\n    \"\"\"\n    with open(path, \"w\") as f:\n        f.write(f'TITLE \"{title}\"\\n')\n        f.write(f\"LUT_3D_SIZE {lut_size}\\n\")\n        f.write(f\"DOMAIN_MIN 0.0 0.0 0.0\\n\")\n        f.write(f\"DOMAIN_MAX 1.0 1.0 1.0\\n\\n\")\n\n        for b in range(lut_size):\n            for g in range(lut_size):\n                for r in range(lut_size):\n                    rv, gv, bv = lut[b, g, r]\n                    f.write(f\"{rv:.6f} {gv:.6f} {bv:.6f}\\n\")\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Infer a 3D color LUT from images.\",\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=__doc__,\n    )\n    sub = parser.add_subparsers(dest=\"command\", required=True)\n\n    # -- hald --\n    p_hald = sub.add_parser(\"hald\", help=\"Extract LUT from a processed Hald CLUT image\")\n    p_hald.add_argument(\"image\", help=\"Processed Hald CLUT image (PNG, TIFF, etc.)\")\n    p_hald.add_argument(\n        \"-o\", \"--output\", default=\"output.cube\", help=\"Output .cube file\"\n    )\n    p_hald.add_argument(\n        \"-s\",\n        \"--size\",\n        type=int,\n        default=35,\n        help=\"Output LUT grid size per axis (default 35)\",\n    )\n\n    # -- pair --\n    p_pair = sub.add_parser(\"pair\", help=\"Approximate LUT from original + graded pair\")\n    p_pair.add_argument(\"original\", help=\"Original (ungraded) image\")\n    p_pair.add_argument(\"graded\", help=\"Graded image\")\n    p_pair.add_argument(\n        \"-o\", \"--output\", default=\"output.cube\", help=\"Output .cube file\"\n    )\n    p_pair.add_argument(\n        \"-s\",\n        \"--size\",\n        type=int,\n        default=33,\n        help=\"LUT grid size per axis (default 33, industry standard)\",\n    )\n\n    # -- generate-hald --\n    p_gen = sub.add_parser(\"generate-hald\", help=\"Generate a neutral Hald CLUT image\")\n    p_gen.add_argument(\n        \"-s\",\n        \"--level\",\n        type=int,\n        default=8,\n        help=\"Hald level (default 8 → 512x512 image, 64-entry LUT)\",\n    )\n    p_gen.add_argument(\n        \"-o\", \"--output\", default=\"identity_hald.png\", help=\"Output image path\"\n    )\n\n    args = parser.parse_args()\n\n    if args.command == \"generate-hald\":\n        print(f\"Generating identity Hald CLUT (level {args.level})...\")\n        img = generate_hald(args.level)\n        img.save(args.output)\n        lut_size = args.level**2\n        print(f\"Saved {img.size[0]}x{img.size[1]} image → {args.output}\")\n        print(\n            f\"Resulting LUT will have {lut_size} entries per axis ({lut_size}^3 = {lut_size**3} total)\"\n        )\n\n    elif args.command == \"hald\":\n        print(f\"Reading Hald CLUT: {args.image}\")\n        img = Image.open(args.image)\n        lut_size, lut = lut_from_hald(img)\n        if args.size != lut_size:\n            print(f\"Resampling {lut_size}³ → {args.size}³...\")\n            lut = resample_lut(lut, lut_size, args.size)\n            lut_size = args.size\n        write_cube(\n            args.output,\n            lut_size,\n            lut,\n            title=f\"Hald-inferred from {Path(args.image).name}\",\n        )\n        print(f\"Extracted {lut_size}x{lut_size}x{lut_size} LUT → {args.output}\")\n\n    elif args.command == \"pair\":\n        print(f\"Original: {args.original}\")\n        print(f\"Graded:   {args.graded}\")\n        orig = Image.open(args.original)\n        grad = Image.open(args.graded)\n        lut_size, lut = lut_from_pair(orig, grad, args.size)\n        write_cube(\n            args.output,\n            lut_size,\n            lut,\n            title=f\"Pair-inferred from {Path(args.original).name} → {Path(args.graded).name}\",\n        )\n        print(f\"Approximated {lut_size}x{lut_size}x{lut_size} LUT → {args.output}\")\n\n    print(\"Done.\")\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n### Random\n\n- \"Dragging your shutter\" means using a low shutter speed for things like movement.\n",
  "title": "Photography Notes",
  "untracked": false,
  "uri": "/Photography_Notes",
  "relativePath": "Photography Notes.md"
}
