<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/rss.xsl" type="text/xsl"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
	<title>Sacha Chua - category - linux</title>
	<atom:link href="https://sachachua.com/blog/category/linux/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/category/linux" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/category/linux/feed/index.xml</link>
	<description>Emacs, sketches, and life</description>
	<lastBuildDate>Tue, 07 Apr 2026 16:37:16 GMT</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>11ty</generator>
  <item>
		<title>Figuring out how to use ffmpeg to mask a chroma-keyed video based on the differences between images</title>
		<link>https://sachachua.com/blog/2022/12/figuring-out-how-to-use-ffmpeg-to-mask-a-chroma-keyed-video/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sun, 25 Dec 2022 09:06:30 GMT</pubDate>
    <category>linux</category>
<category>geek</category>
<category>ffmpeg</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2022/12/figuring-out-how-to-use-ffmpeg-to-mask-a-chroma-keyed-video/</guid>
		<description><![CDATA[<p>
A- is really into Santa and Christmas because of the books she's read.
Last year, she wanted to set up the GoPro to capture footage during
Christmas Eve. I helped her set it up for a timelapse video. After she
went to bed, we gradually positioned the presents. I extracted the
frames from the video, removed the ones that caught us moving around,
and then used <a href="https://krita.org/en/">Krita</a>'s new animation features to animate sparkles so
that the presents magically appeared. She mentioned the sparkles a
number of times during her deliberations about whether Santa exists or
not.
</p>

<p>
This year, I want to see if I can use green-screen videos like <a href="https://www.youtube.com/watch?v=xgVtyx9XJhI">this
reversed-spin sparkle</a> or this other <a href="https://youtube.com/watch?v=uyxpPKZQSzU">sparkle video</a>. I'm going to take
a series of images, with each image adding one more gift. Then I'm
going to make a mask in Krita with white covering the gift and a
transparent background for the rest of the image. Then I'll use
chroma-key to drop out the green screen of the sparkle video and mask
it in so that the sparkles only happen within the boundaries of the
gift that was added. I also want to fade one image into the other, and
I want the sparkles to fade out as the gift appears.
</p>

<div id="outline-container-org0b393d7" class="outline-2">
<h3 id="org0b393d7">Figuring things out</h3>
<div class="outline-text-2" id="text-org0b393d7">
<p>
I didn't know how to do any of that yet with ffmpeg, so here's how I
started figuring things out. First, I wanted to see how to <a href="https://ffmpeg.org/ffmpeg-filters.html#fade">fade</a>
test.jpg into test2.jpg over 4 seconds.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -loop 1 -i test.jpg -loop 1 -i test2.jpg -filter_complex <span class="org-string">"[1:v]fade=t=in:d=4:alpha=1[fadein];[0:v][fadein]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 -shortest test.webm
</pre>
</div>

<p>
Here's another way using the <a href="https://ffmpeg.org/ffmpeg-filters.html#blend">blend</a> filter:
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -loop 1 -i test.jpg -loop 1 -i test2.jpg -filter_complex <span class="org-string">"[1:v][0:v]blend=all_expr='A*(if(gte(T,4),1,T/4))+B*(1-(if(gte(T,4),1,T/4)))"</span> -t 4 -r 1 test.webm
</pre>
</div>

<p>
Then I looked into <a href="https://ffmpeg.org/ffmpeg-filters.html#chromakey">chromakey</a>ing in the other video. I used balloons
instead of sparkles just in case she happened to look at my screen.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -i test.webm -i balloons.mp4 -filter_complex <span class="org-string">"[1:v]chromakey=0x00ff00:0.1:0.2[ckout];[0:v][ckout]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -shortest -r 1 overlaid.webm
</pre>
</div>

<p>
I experimented with the <a href="https://ffmpeg.org/ffmpeg-filters.html#alphamerge">alphamerge</a> filter.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -i test.jpg -i test2.jpg -i mask.png -filter_complex <span class="org-string">"[1:v][2:v]alphamerge[a];[0:v][a]overlay[out]"</span> -map <span class="org-string">"[out]"</span> masked.jpg
</pre>
</div>

<p>
Okay! That overlaid test.jpg with a masked part of test2.jpg. How about alphamerging in a video? First, I need a mask video&#x2026;
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -loop 1 -i mask.png  -r 1 -t 4  mask.webm
</pre>
</div>

<p>
Then I can combine that:
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i mask.webm -filter_complex <span class="org-string">"[1:v][2:v]alphamerge[masked];[0:v][masked]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 alphamerged.webm
</pre>
</div>

<p>
Great, let's figure out how to combine chroma-key and alphamerge video. The naive approach doesn't work, probably because they're both messing with the alpha layer.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i mask.webm -filter_complex <span class="org-string">"[1:v]chromakey=0x00ff00:0.1:0.2[ckout];[ckout][2:v]alphamerge[masked];[0:v][masked]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 masked.webm
</pre>
</div>

<p>
So I probably need to blend the chromakey and the mask. Let's see if I can extract the chromakey alpha.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i mask.webm -filter_complex <span class="org-string">"[1:v]chromakey=0x00ff00:0.1:0.2,format=rgba,alphaextract[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4
chroma-alpha.webm
</pre>
</div>

<p>
Now let's blend it with the mask.webm.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i mask.webm -filter_complex <span class="org-string">"[1:v]chromakey=0x00ff00:0.1:0.2,format=rgba,alphaextract[ckalpha];[ckalpha][2:v]blend=all_mode=and[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 masked-alpha.webm
</pre>
</div>

<p>
Then let's use it as the alpha:
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i masked-alpha.webm -filter_complex <span class="org-string">"[2:v]format=rgba[mask];[1:v][mask]alphamerge[masked];[0:v][masked]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 alphamerged.webm
</pre>
</div>

<p>
Okay, that worked! Now how do I combine everything into one command? Hmm&#x2026;
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -loop 1 -i test.jpg -t 4 -loop 1 -i test2.jpg -t 4 -i balloons.mp4 -loop 1 -i mask.png -t 4 -filter_complex <span class="org-string">"[1:v][0:v]blend=all_expr='A*(if(gte(T,4),1,T/4))+B*(1-(if(gte(T,4),1,T/4)))'[fade];[2:v]chromakey=0x00ff00:0.1:0.2,format=rgba,alphaextract[ckalpha];[ckalpha][3:v]blend=all_mode=and,format=rgba[maskedalpha];[2:v][maskedalpha]alphamerge[masked];[fade][masked]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 5 -t 4 alphamerged.webm
</pre>
</div>

<p>
Then I wanted to fade the masked video out by the end.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -loop 1 -i test.jpg -t 4 -loop 1 -i test2.jpg -t 4 -i balloons.mp4 -loop 1 -i mask.png -t 4 -filter_complex <span class="org-string">"[1:v][0:v]blend=all_expr='A*(if(gte(T,4),1,T/4))+B*(1-(if(gte(T,4),1,T/4)))'[fade];[2:v]chromakey=0x00ff00:0.1:0.2,format=rgba,alphaextract[ckalpha];[ckalpha][3:v]blend=all_mode=and,format=rgba[maskedalpha];[2:v][maskedalpha]alphamerge[masked];[masked]fade=type=out:st=2:d=1:alpha=1[maskedfade];[fade][maskedfade]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 10 -t 4 alphamerged.webm
</pre>
</div>
</div>
</div>
<div id="outline-container-org0e23e45" class="outline-2">
<h3 id="org0e23e45">Making the video</h3>
<div class="outline-text-2" id="text-org0e23e45">
<p>
When A- finally went to bed, we arranged the presents, using the GoPro to take a picture at each step of the way. I cropped and resized the images, using Krita to figure out the cropping rectangle and offset.
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="org-keyword">for</span> FILE<span class="org-keyword"> in</span> *.JPG; <span class="org-keyword">do</span> convert $<span class="org-variable-name">FILE</span> -crop 1558x876+473+842 -resize 1280x720 cropped/$<span class="org-variable-name">FILE</span>; <span class="org-keyword">done</span>
</pre>
</div>

<p>
I used ImageMagick to calculate the masks automatically.
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="org-variable-name">files</span>=(*.JPG)
<span class="org-variable-name">i</span>=0
<span class="org-variable-name">j</span>=1
<span class="org-variable-name">len</span>=<span class="org-string">"${#files[@]}"</span>
<span class="org-keyword">while</span> [ <span class="org-string">"$j"</span> -lt $<span class="org-variable-name">len</span> ]; <span class="org-keyword">do</span>
  compare -fuzz 15% cropped/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">i</span>]} cropped/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">j</span>]} -compose Src -highlight-color White -lowlight-color Black masks/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">j</span>]}
  convert -morphology Open Disk -morphology Close Disk -blur 20x5 masks/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">j</span>]} processed-masks/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">j</span>]}
  <span class="org-variable-name">i</span>=$((i+1))
  <span class="org-variable-name">j</span>=$((j+1))
<span class="org-keyword">done</span>
</pre>
</div>

<p>
Then I faded the images together to make a video.
</p>

<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> ffmpeg
<span class="org-keyword">import</span> glob
<span class="org-variable-name">files</span> = glob.glob(<span class="org-string">"images/cropped/*.JPG"</span>)
files.sort()
<span class="org-variable-name">fps</span> = 15
<span class="org-variable-name">crf</span> = 32
<span class="org-variable-name">out</span> = ffmpeg.<span class="org-builtin">input</span>(files[0], loop=1, r=fps)
<span class="org-variable-name">duration</span> = 3
<span class="org-keyword">for</span> i <span class="org-keyword">in</span> <span class="org-builtin">range</span>(1, <span class="org-builtin">len</span>(files)):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">out</span> = ffmpeg.<span class="org-builtin">filter</span>([out, ffmpeg.<span class="org-builtin">input</span>(files[i], loop=1, r=fps).<span class="org-builtin">filter</span>(<span class="org-string">'fade'</span>, t=<span class="org-string">'in'</span>, d=duration, st=i*duration, alpha=1)], <span class="org-string">'overlay'</span>)
<span class="org-variable-name">args</span> = out.output(<span class="org-string">'images.webm'</span>, t=<span class="org-builtin">len</span>(files) * duration, r=fps, y=<span class="org-constant">None</span>, crf=crf).<span class="org-builtin">compile</span>()
<span class="org-builtin">print</span>(<span class="org-string">' '</span>.join(f<span class="org-string">'"</span>{item}<span class="org-string">"'</span> <span class="org-keyword">for</span> item <span class="org-keyword">in</span> args))
</pre>
</div>

<p>
"ffmpeg" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2317.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2318.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2319.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2320.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2321.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2322.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2323.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2324.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2325.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2326.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2327.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2328.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2329.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2330.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2331.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2332.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2333.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2334.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2335.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2336.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2337.JPG" "-filter_complex" "[1]fade=alpha=1:d=3:st=3:t=in[s0];[0][s0]overlay[s1];[2]fade=alpha=1:d=3:st=6:t=in[s2];[s1][s2]overlay[s3];[3]fade=alpha=1:d=3:st=9:t=in[s4];[s3][s4]overlay[s5];[4]fade=alpha=1:d=3:st=12:t=in[s6];[s5][s6]overlay[s7];[5]fade=alpha=1:d=3:st=15:t=in[s8];[s7][s8]overlay[s9];[6]fade=alpha=1:d=3:st=18:t=in[s10];[s9][s10]overlay[s11];[7]fade=alpha=1:d=3:st=21:t=in[s12];[s11][s12]overlay[s13];[8]fade=alpha=1:d=3:st=24:t=in[s14];[s13][s14]overlay[s15];[9]fade=alpha=1:d=3:st=27:t=in[s16];[s15][s16]overlay[s17];[10]fade=alpha=1:d=3:st=30:t=in[s18];[s17][s18]overlay[s19];[11]fade=alpha=1:d=3:st=33:t=in[s20];[s19][s20]overlay[s21];[12]fade=alpha=1:d=3:st=36:t=in[s22];[s21][s22]overlay[s23];[13]fade=alpha=1:d=3:st=39:t=in[s24];[s23][s24]overlay[s25];[14]fade=alpha=1:d=3:st=42:t=in[s26];[s25][s26]overlay[s27];[15]fade=alpha=1:d=3:st=45:t=in[s28];[s27][s28]overlay[s29];[16]fade=alpha=1:d=3:st=48:t=in[s30];[s29][s30]overlay[s31];[17]fade=alpha=1:d=3:st=51:t=in[s32];[s31][s32]overlay[s33];[18]fade=alpha=1:d=3:st=54:t=in[s34];[s33][s34]overlay[s35];[19]fade=alpha=1:d=3:st=57:t=in[s36];[s35][s36]overlay[s37];[20]fade=alpha=1:d=3:st=60:t=in[s38];[s37][s38]overlay[s39]" "-map" "[s39]" "-crf" "32" "-r" "15" "-t" "63" "-y" "images.webm"
</p>

<p>
Next, I faded the masks together. These ones faded in and out so that only one mask was active at a time.
</p>

<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> ffmpeg
<span class="org-keyword">import</span> glob
<span class="org-variable-name">files</span> = glob.glob(<span class="org-string">"images/processed-masks/*.JPG"</span>)
files.sort()
<span class="org-variable-name">files</span> = files[:-2]  <span class="org-comment-delimiter"># </span><span class="org-comment">Omit the last two, where I'm just turning off the lights</span>
<span class="org-variable-name">fps</span> = 15
<span class="org-variable-name">crf</span> = 32
<span class="org-variable-name">out</span> = ffmpeg.<span class="org-builtin">input</span>(<span class="org-string">'color=black:s=1280x720'</span>, f=<span class="org-string">'lavfi'</span>, r=fps)
<span class="org-variable-name">duration</span> = 3
<span class="org-keyword">for</span> i <span class="org-keyword">in</span> <span class="org-builtin">range</span>(0, <span class="org-builtin">len</span>(files)):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">out</span> = ffmpeg.<span class="org-builtin">filter</span>([out, ffmpeg.<span class="org-builtin">input</span>(files[i], loop=1, r=fps).<span class="org-builtin">filter</span>(<span class="org-string">'fade'</span>, t=<span class="org-string">'in'</span>, d=1, st=(i + 1)*duration, alpha=1).<span class="org-builtin">filter</span>(<span class="org-string">'fade'</span>, t=<span class="org-string">'out'</span>, st=(i + 2)*duration - 1)], <span class="org-string">'overlay'</span>)
<span class="org-variable-name">args</span> = out.output(<span class="org-string">'processed-masks.webm'</span>, t=<span class="org-builtin">len</span>(files) * duration, r=fps, y=<span class="org-constant">None</span>, crf=crf).<span class="org-builtin">compile</span>()
<span class="org-builtin">print</span>(<span class="org-string">' '</span>.join(f<span class="org-string">'"</span>{item}<span class="org-string">"'</span> <span class="org-keyword">for</span> item <span class="org-keyword">in</span> args))
</pre>
</div>

<p>
"ffmpeg" "-f" "lavfi" "-r" "15" "-i" "color=<span style="color:black;">s=1280x720</span>" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2318.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2319.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2320.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2321.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2322.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2323.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2324.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2325.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2326.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2327.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2328.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2329.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2330.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2331.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2332.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2333.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2334.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2335.JPG" "-filter_complex" "[1]fade=alpha=1:d=1:st=3:t=in[s0];[s0]fade=st=5:t=out[s1];[0][s1]overlay[s2];[2]fade=alpha=1:d=1:st=6:t=in[s3];[s3]fade=st=8:t=out[s4];[s2][s4]overlay[s5];[3]fade=alpha=1:d=1:st=9:t=in[s6];[s6]fade=st=11:t=out[s7];[s5][s7]overlay[s8];[4]fade=alpha=1:d=1:st=12:t=in[s9];[s9]fade=st=14:t=out[s10];[s8][s10]overlay[s11];[5]fade=alpha=1:d=1:st=15:t=in[s12];[s12]fade=st=17:t=out[s13];[s11][s13]overlay[s14];[6]fade=alpha=1:d=1:st=18:t=in[s15];[s15]fade=st=20:t=out[s16];[s14][s16]overlay[s17];[7]fade=alpha=1:d=1:st=21:t=in[s18];[s18]fade=st=23:t=out[s19];[s17][s19]overlay[s20];[8]fade=alpha=1:d=1:st=24:t=in[s21];[s21]fade=st=26:t=out[s22];[s20][s22]overlay[s23];[9]fade=alpha=1:d=1:st=27:t=in[s24];[s24]fade=st=29:t=out[s25];[s23][s25]overlay[s26];[10]fade=alpha=1:d=1:st=30:t=in[s27];[s27]fade=st=32:t=out[s28];[s26][s28]overlay[s29];[11]fade=alpha=1:d=1:st=33:t=in[s30];[s30]fade=st=35:t=out[s31];[s29][s31]overlay[s32];[12]fade=alpha=1:d=1:st=36:t=in[s33];[s33]fade=st=38:t=out[s34];[s32][s34]overlay[s35];[13]fade=alpha=1:d=1:st=39:t=in[s36];[s36]fade=st=41:t=out[s37];[s35][s37]overlay[s38];[14]fade=alpha=1:d=1:st=42:t=in[s39];[s39]fade=st=44:t=out[s40];[s38][s40]overlay[s41];[15]fade=alpha=1:d=1:st=45:t=in[s42];[s42]fade=st=47:t=out[s43];[s41][s43]overlay[s44];[16]fade=alpha=1:d=1:st=48:t=in[s45];[s45]fade=st=50:t=out[s46];[s44][s46]overlay[s47];[17]fade=alpha=1:d=1:st=51:t=in[s48];[s48]fade=st=53:t=out[s49];[s47][s49]overlay[s50];[18]fade=alpha=1:d=1:st=54:t=in[s51];[s51]fade=st=56:t=out[s52];[s50][s52]overlay[s53]" "-map" "[s53]" "-crf" "32" "-r" "15" "-t" "54" "-y" "processed-masks.webm"
</p>

<p>
I ended up using <a href="https://www.youtube.com/watch?v=cn1zf9nFUsI">this particle glitter video</a> because the gifts were small, so I wanted a video that was dense with sparkly things. I also wanted the sparkles to be more concentrated on the area where the gifts were, so I resized it and positioned it.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -f lavfi -i <span class="org-variable-name">color</span>=black:<span class="org-variable-name">s</span>=1280x720 -i sparkles4.webm -ss 13 -filter_complex <span class="org-string">"[1:v]scale=700:392[sparkles];[0:v][sparkles]overlay=x=582:y=194,setpts=(PTS-STARTPTS)*1.05[out]"</span> -map <span class="org-string">"[out]"</span> -r 15 -t 53 -shortest sparkles-trimmed.webm
ffmpeg -y -stream_loop 2 -i sparkles-trimmed.webm -t 57 sparkles-looped.webm              
</pre>
</div>

<p>
Lastly, I combined the videos with the sparkles.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i images.webm -i sparkles-looped.webm -i processed-masks.webm -filter_complex <span class="org-string">"[1:v]chromakey=0x0a9d06:0.1:0.2,format=rgba,alphaextract[ckalpha];[ckalpha][2:v]blend=all_mode=and,format=rgba[maskedalpha];[1:v][maskedalpha]alphamerge[masked];[masked]fade=t=out:st=57:d=1:alpha=1[maskedfaded];[0:v][maskedfaded]overlay[combined];[combined]tpad=start_mode=clone:start_duration=4:stop_mode=clone:stop_duration=4[out]"</span> -map <span class="org-string">"[out]"</span> -r 15 -crf 32 output.webm
</pre>
</div>

<p>
After many iterations and a very late night, I got (roughly) the video I wanted, which I'm not posting here because of reasons. But it worked, yay! Now I don't have to manually place stars frame-by-frame in Krita, and I can just have all that magic happen semi-automatically.
</p>
</div>
</div>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2022%2F12%2Ffiguring-out-how-to-use-ffmpeg-to-mask-a-chroma-keyed-video%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Re-encoding the EmacsConf videos with FFmpeg and GNU Parallel</title>
		<link>https://sachachua.com/blog/2021/12/re-encoding-the-emacsconf-videos-with-ffmpeg-and-gnu-parallel/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 23 Dec 2021 07:30:12 GMT</pubDate>
    <category>geek</category>
<category>linux</category>
<category>emacsconf</category>
<category>ffmpeg</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2021/12/re-encoding-the-emacsconf-videos-with-ffmpeg-and-gnu-parallel/</guid>
		<description><![CDATA[<p>
It turns out that using <code>-crf 56</code> compressed the EmacsConf a little
too aggressively, losing too much information in the video. We wanted
to reencode everything, maybe going back to the default value of <code>-crf
32</code>. My laptop would have taken a long time to do all of those videos.
Fortunately, one of the other volunteers shared a VM on a machine with
12 cores, and I had access to a few other systems. It was a good
opportunity to learn how to use <a href="https://www.gnu.org/software/parallel/">GNU Parallel</a> to send jobs to different
machines and retrieve the results.
</p>

<p>
First, I updated the compression script, <code>compress-video-low.sh</code>:
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="org-variable-name">Q</span>=$<span class="org-variable-name">1</span>
<span class="org-variable-name">WIDTH</span>=1280
<span class="org-variable-name">HEIGHT</span>=720
<span class="org-variable-name">AUDIO_RATE</span>=48000
<span class="org-variable-name">VIDEO_FILTER</span>=<span class="org-string">"scale=w=${WIDTH}:h=${HEIGHT}:force_original_aspect_ratio=1,pad=${WIDTH}:${HEIGHT}:(ow-iw)/2:(oh-ih)/2,fps=25,colorspace=all=bt709:iall=bt601-6-625:fast=1"</span>
<span class="org-variable-name">FILE</span>=$<span class="org-variable-name">2</span>
<span class="org-variable-name">SUFFIX</span>=$<span class="org-variable-name">Q</span>
<span class="org-builtin">shift</span>
<span class="org-builtin">shift</span>
ffmpeg -y -i <span class="org-string">"$FILE"</span>  -pixel_format yuv420p -vf $<span class="org-variable-name">VIDEO_FILTER</span> -colorspace 1 -color_primaries 1 -color_trc 1 -c:v libvpx-vp9 -b:v 0 -crf $<span class="org-variable-name">Q</span> -aq-mode 2 -tile-columns 0 -tile-rows 0 -frame-parallel 0 -cpu-used 8 -auto-alt-ref 1 -lag-in-frames 25 -g 240 -pass 1 -f webm -an -threads 8 /dev/null &amp;&amp;
<span class="org-keyword">if</span> [[ $<span class="org-variable-name">FILE</span> =~ <span class="org-string">"webm"</span> ]]; <span class="org-keyword">then</span>
    ffmpeg -y -i <span class="org-string">"$FILE"</span> $<span class="org-variable-name">*</span>  -pixel_format yuv420p -vf $<span class="org-variable-name">VIDEO_FILTER</span> -colorspace 1 -color_primaries 1 -color_trc 1 -c:v libvpx-vp9 -b:v 0 -crf $<span class="org-variable-name">Q</span> -tile-columns 2 -tile-rows 2 -frame-parallel 0 -cpu-used -5 -auto-alt-ref 1 -lag-in-frames 25 -pass 2 -g 240 -ac 2 -threads 8 -c:a copy <span class="org-string">"${FILE%.*}&#45;&#45;compressed$SUFFIX.webm"</span>
<span class="org-keyword">else</span>
    ffmpeg -y -i <span class="org-string">"$FILE"</span> $<span class="org-variable-name">*</span>  -pixel_format yuv420p -vf $<span class="org-variable-name">VIDEO_FILTER</span> -colorspace 1 -color_primaries 1 -color_trc 1 -c:v libvpx-vp9 -b:v 0 -crf $<span class="org-variable-name">Q</span> -tile-columns 2 -tile-rows 2 -frame-parallel 0 -cpu-used -5 -auto-alt-ref 1 -lag-in-frames 25 -pass 2 -g 240 -ac 2 -threads 8 -c:a libvorbis <span class="org-string">"${FILE%.*}&#45;&#45;compressed$SUFFIX.webm"</span>
<span class="org-keyword">fi</span>
</pre>
</div>

<p>
I made an <code>originals.txt</code> file with all the original filenames. It looked like this:
</p>

<pre class="example" id="orge717a1c">
emacsconf-2020-frownies&#45;&#45;the-true-frownies-are-the-friends-we-made-along-the-way-an-anecdote-of-emacs-s-malleability&#45;&#45;case-duckworth.mkv
emacsconf-2021-montessori&#45;&#45;emacs-and-montessori-philosophy&#45;&#45;grant-shangreaux.webm
emacsconf-2021-pattern&#45;&#45;emacs-as-design-pattern-learning&#45;&#45;greta-goetz.mp4
...
</pre>

<p>
I set up a <code>~/.parallel/emacsconf</code> profile with something like this so
that I could use three computers and my laptop, sending one job each
and displaying progress:
</p>

<p>
<code>&#45;&#45;sshlogin computer1 &#45;&#45;sshlogin computer2 &#45;&#45;sshlogin computer3 &#45;&#45;sshlogin : -j 1 &#45;&#45;progress &#45;&#45;verbose &#45;&#45;joblog parallel.log</code>
</p>

<p>
I already had SSH key-based authentication set up so that I could connect to the three remote computers.
</p>

<p>
Then I spread the jobs over four computers with the following command:
</p>

<div class="org-src-container">
<pre class="src src-sh">cat originals.txt | parallel -J emacsconf <span class="org-sh-escaped-newline">\</span>
                             &#45;&#45;transferfile {} <span class="org-sh-escaped-newline">\</span>
                             &#45;&#45;return <span class="org-string">'{=$_ =~ s/\..*?$/&#45;&#45;compressed32.webm/=}'</span> <span class="org-sh-escaped-newline">\</span>
                             &#45;&#45;cleanup <span class="org-sh-escaped-newline">\</span>
                             &#45;&#45;basefile compress-video-low.sh <span class="org-sh-escaped-newline">\</span>
                             bash compress-video-low.sh 32 {}
</pre>
</div>

<p>
It copied each file over to the computer it was assigned to, processed
the file, and then copied the file back.
</p>

<p>
It was also helpful to occasionally do <code>echo 'killall -9 ffmpeg' |
parallel -J emacsconf -j 1 &#45;&#45;onall</code> if I cancelled a run.
</p>

<p>
It still took a long time, but less than it would have if any one
computer had to crunch through everything on its own.
</p>

<p>
This was much better than my previous way of doing things, which
involved copying the files over, running ffmpeg commands, copying the
files back, and getting somewhat confused about which directory I was
in and which file I assigned where and what to do about
incompletely-encoded files.
</p>

<p>
I sometimes ran into problems with incompletely-encoded files because
I'd cancelled the FFmpeg process. Even though <code>ffprobe</code> said the files
were long, they were missing a large chunk of video at the end. I
added a <code>compile-media-verify-video-frames</code> function to
<a href="https://github.com/sachac/compile-media/blob/main/compile-media.el">compile-media.el</a> so that I could get the last few seconds of frames,
compare them against the duration, and report an error if there was a
big gap.
</p>

<p>
Then I changed <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-publish.el">emacsconf-publish.el</a> to use the new filenames, and I
regenerated all the pages. For EmacsConf 2020, I used some Emacs Lisp
to update the files. I'm not particularly fond of wrangling video files (lots of waiting, high chance of error), but I'm glad I got the computers to work together.
</p>

<p>You can <a href="https://sachachua.com/blog/2021/12/re-encoding-the-emacsconf-videos-with-ffmpeg-and-gnu-parallel/#comment">view 2 comments</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2021%2F12%2Fre-encoding-the-emacsconf-videos-with-ffmpeg-and-gnu-parallel%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Adding an overlay to my webcam via OBS 26.1</title>
		<link>https://sachachua.com/blog/2021/01/adding-an-overlay-to-my-webcam-via-obs-26-1/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sun, 03 Jan 2021 05:08:00 GMT</pubDate>
    <category>geek</category>
<category>linux</category>
		<guid isPermaLink="false">https://sachachua.com/blog/?p=29657</guid>
		<description><![CDATA[<p>A- likes to change her name roughly every two months, depending on whatever things she's focusing on. In 2020, she went through six names, giving us plenty of mental exercise and amusement. Pretend play is wonderful and she picks up all sorts of interesting attributes along the way, so we're totally fine with letting her pretend all the time instead of limiting it to specific times.</p>
<p>A-&#8216;s been going to virtual kindergarten. So far, her teachers and classmates have been cool with the name changes. They met her in her Stephanie phase, and they shifted over to Elizabeth without batting an eye. A-&#8216;s been experimenting with a new name, though, so I thought I'd try to figure out a way to make the teachers' lives a little easier. We use Google Meet to connect to class. A- likes to log in as me because then we're alphabetically sorted close to one of her friends in class, the high-tech equivalent of wanting to sit with your friends. So the name that's automatically displayed when she's speaking is no help either.</p>
<p>It turns out that OBS (Open Broadcast Studio) has a virtual webcam feature in version 26.1, and it works for MacOS X and Linux. I followed the instructions for <a href="https://obsproject.com/wiki/install-instructions">installing OBS 26.1</a> on Ubuntu. To enable the virtual webcam device on Linux, <a href="https://blog.jbrains.ca/permalink/using-obs-studio-as-a-virtual-cam-on-linux">I installed v4l2loopback-dkms</a>. I was initially mystified when I got the error <code>could not insert 'v4l2loopback': Operation not permitted</code>. That was because I have Secure Boot on my laptop, so I just needed to reboot, choose <code>Enroll MOK</code> from the boot menu, and put in the password that I specified during the setup process. After I did that, clicking on the <b>Start Virtual Camera</b> button in OBS worked. I tested it in Google Meet and the image was properly displayed. I don't know if we'll need it, but it's handy to have in my back pocket in case A- decides to change her name again.</p>
<p>Yay Linux and free software!</p>

<p>You can <a href="https://sachachua.com/blog/2021/01/adding-an-overlay-to-my-webcam-via-obs-26-1/#comment">view 1 comment</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2021%2F01%2Fadding-an-overlay-to-my-webcam-via-obs-26-1%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Compiling autotrace against GraphicsMagick instead of ImageMagick</title>
		<link>https://sachachua.com/blog/2020/05/compiling-autotrace-against-graphicsmagick-instead-of-imagemagick/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 22 May 2020 03:59:00 GMT</pubDate>
    <category>geek</category>
<category>linux</category>
		<guid isPermaLink="false">https://sachachua.com/blog/?p=29565</guid>
		<description><![CDATA[<p>In an extravagant instance of yak-shaving, I found myself trying to compile autotrace so that I could use it to trace my handwriting samples so that I could make a font with FontForge so that I could make worksheets for A- without worrying about finding a handwriting font with the right features and open font licensing.</p>
<p>AutoTrace had been written against ImageMagick 5, but this was no longer easily available. ImageMagick 7 greatly changed the API. Fortunately, GraphicsMagick kept the ImageMagick 5 API. I eventually figured out enough about autoconf and C (neither of which I had really worked with much before) to switch the library paths out while attempting to preserve backward compatibility. I <b>think</b> I got it, but I can't really tell aside from the fact that compiling it with GraphicsMagick makes the included tests run. Yay!</p>
<p>At first I tried <code>AC_DEFINE</code>-ing twice in the same package check, but it turns out only the last one sticks, so I moved the other AC_DEFINE to a different condition.</p>
<p>Anyway, here's the <a href="https://github.com/autotrace/autotrace/pull/35">pull request</a>. Whee! I'm coding!</p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2020%2F05%2Fcompiling-autotrace-against-graphicsmagick-instead-of-imagemagick%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Learning more about Docker</title>
		<link>https://sachachua.com/blog/2019/01/learning-more-about-docker/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 03 Jan 2019 05:01:00 GMT</pubDate>
    <category>geek</category>
<category>linux</category>
		<guid isPermaLink="false">https://sachachua.com/blog/?p=29353</guid>
		<description><![CDATA[<p>I&#8217;ve been mostly heads-down on parenting for a few years now. <a href="https://sachachua.com/blog/2018/06/notes-on-the-babysitting-experiment/">A- wasn&#8217;t keen on babysitters</a>, so my computing time consisted of a couple of hours during the graveyard shift, after our little night-owl finally settled into bed. I felt like I was treading water: keep <a href="https://sachachua.com/blog/emacs-news">Emacs News</a> going, check in and do some consulting once in a while so that the relationship doesn&#8217;t go cold, do my <a href="https://sachachua.com/blog/category/weekly">weekly reviews</a>, try to automate things here and there.</p>
<p>I definitely felt the gaps between the quick-and-dirty coding I did and the best practices I saw elsewhere. I felt a little anxious about not having development environments and production deployment processes for my personal projects. Whenever I messed up my blog or my web-based tracker, I stayed up extra late to fix things, coding while tired and sleepy and occasionally interrupted by A- needing extra snuggling. I updated whenever I felt it was necessary for security, but the risk discouraged me from trying to make things better.</p>
<p>Lately, though, I feel like I&#8217;ve been able to actually have some more focused time to learn new things. A- is a little more used to a bedtime routine, and I no longer have to reserve as much energy and patience for dealing with tantrums. She still sleeps really late, but it&#8217;s manageable. And besides, I&#8217;d tracked the time I spent playing a game on my phone, so I knew I had a little discretionary time I could use more effectively.</p>
<p><a href="https://www.docker.com/">Docker</a> is one of the tools on my to-learn list. I think it will help a lot to have environments that I can experiment with and recreate whenever I want. I tried Vagrant before, but Docker feels a lot lighter-weight.</p>
<p>I started by moving my sketch viewer into a Docker container. It&#8217;s a basic Node server with read-only access to my sketches, so that was mostly a matter of changing it to be configured via environment variables and mounting the sketches as a volume. I added <code>dockerfile-mode</code> to my Emacs, made a <code>Dockerfile</code> and a <code>.dockerignore</code> file following the <a href="https://nodejs.org/en/docs/guides/nodejs-docker-webapp/">tutorial for Dockerizing a Node.js web app</a>, tried it out on my laptop, and pushed the image to my private Docker hub so that I could pull the image on my server. It turned out that Linode&#8217;s kernel had overlay built in instead of compiled as a module, so I followed <a href="https://www.linode.com/community/questions/17114/docker-wont-start-using-the-latest-linode-kernel">this tip</a> to fix it.</p>
<pre class="example">cat &lt;&lt; EOF &gt; /etc/systemd/system/containerd.service.d/override.conf
[Service]
ExecStartPre=
EOF
</pre>
<p>I also needed to uninstall my old docker.io and docker-compose, add the <a href="https://docs.docker.com/install/linux/docker-ce/ubuntu/">Docker PPA</a>, and install docker-ce in order to get <code>docker login</code> to work properly on my server.</p>
<p>The next step was to move my web interface for tracking &#8211; not Quantified Awesome, but the <a href="https://sachachua.com/blog/2017/06/quick-notes-on-my-current-interface-for-time-tracking/">button-filled webpage</a> I&#8217;ve been using on my phone. I used lots of environment variables for passwords and tokens, so I switched to using <code>&#45;&#45;env-file</code> file instead.</p>
<p>In order to move Quantified Awesome or my blog into Docker, I needed a <a href="https://docs.docker.com/samples/library/mysql/#docker-secrets">MySQL container</a> that could load my backups. <code>docker-compose.yml</code> Loading the SQL was just a matter of mounting the backup files in <code>/docker-entrypoint-initdb.d</code>, and mounting a directory as <code>/var/lib/mysql</code> should help with data persistence. If I added a script that created a user and granted access from <code>'%'</code>, I could access the MySQL inside the Docker container from my laptop. I didn&#8217;t want my MySQL container to be publicly exposed on my server, though. It turned out that Docker bypassed <code>ufw</code> by setting <code>iptables</code> rules directly, so I followed the <a href="https://stackoverflow.com/questions/30383845/what-is-the-best-practice-of-docker-ufw-under-ubuntu">other instructions in this Stackoverflow answer</a> and added these to the end of my <code>/etc/ufw/after.rules</code>:</p>
<pre class="example"># BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp &#45;&#45;tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp &#45;&#45;tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp &#45;&#45;tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp &#45;&#45;dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp &#45;&#45;dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp &#45;&#45;dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER
</pre>
<p><a href="https://github.com/moby/moby/issues/4737#issuecomment-419705925">There&#8217;s more discussion on docker and ufw</a>, but I don&#8217;t quite have the brainspace right now to fully understand it.</p>
<p>Anyway. Progress. <a href="http://sketches.sachachua.com/">sketches.sachachua.com</a> is in a Docker container, and so is my button-based time tracker. I have a Docker container that I can use to load SQL backups, and I can connect to it for testing. The next step would probably be to try moving Quantified Awesome into a Docker container that talks to my MySQL container. If I can get that working, then I can try moving my blog into a container too.</p>
<p>Yesterday was for sleeping. Today I wanted to clean up my notes and post them, since I&#8217;ll forget too much if I keep going. More coding will have to wait for tomorrow–or maybe the day after, if I use some time for consulting instead. But slow progress is still progress, and it&#8217;s nice to feel like more of a geek again.</p>

<p>You can <a href="https://sachachua.com/blog/2019/01/learning-more-about-docker/#comment">view 2 comments</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2019%2F01%2Flearning-more-about-docker%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Experimenting with adding labels to photos</title>
		<link>https://sachachua.com/blog/2018/06/experimenting-with-adding-labels-to-photos/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sat, 16 Jun 2018 03:45:00 GMT</pubDate>
    <category>geek</category>
<category>linux</category>
		<guid isPermaLink="false">https://sachachua.com/blog/?p=29240</guid>
		<description><![CDATA[<p>A-&#8216;s gotten really interested in letters lately, so I'm looking for ways to add more personally relevant text and images to her life. She often flips through the 4&#8243;x6&#8243; photos I got printed. Since they're photos, they're sturdier and can stand up to a little bending. I wanted to experiment with printing more pictures and labeling them with text, building on the script I used to <a href="https://sachachua.com/blog/2018/03/labeling-toy-storage-bins-with-photos-and-text-using-imagemagick-and-org-babel/">label our toy storage bins</a>. Here's a sample:</p>
<p><a href="https://sachachua.com/blog/wp-content/uploads/2018/06/2018-06-14-16-12-14-★★★-A-gently-lifted-the-flaps-in-the-book.-🏷fine-motor-3.jpg"><img loading="lazy" class="alignnone size-medium wp-image-29242" src="https://sachachua.com/blog/wp-content/uploads/2018/06/2018-06-14-16-12-14-★★★-A-gently-lifted-the-flaps-in-the-book.-🏷fine-motor-3-640x427.jpg" alt="2018-06-14-16-12-14 ★★★ A- gently lifted the flaps in the book. 🏷fine-motor" width="640" height="427" srcset="https://sachachua.com/blog/wp-content/uploads/2018/06/2018-06-14-16-12-14-★★★-A-gently-lifted-the-flaps-in-the-book.-🏷fine-motor-3-640x427.jpg 640w, https://sachachua.com/blog/wp-content/uploads/2018/06/2018-06-14-16-12-14-★★★-A-gently-lifted-the-flaps-in-the-book.-🏷fine-motor-3-280x187.jpg 280w, https://sachachua.com/blog/wp-content/uploads/2018/06/2018-06-14-16-12-14-★★★-A-gently-lifted-the-flaps-in-the-book.-🏷fine-motor-3-768x512.jpg 768w" sizes="(max-width: 640px) 100vw, 640px"></a></p>
<p>(Name still elided in the sample until I figure out what we want to do with names and stuff. I'll use <code>NAME_REPLACEMENTS</code> to put her name into the printed ones, though, since kids tend to learn how to read their names first.)</p>
<p>I haven't printed these out yet to see how legible they are, but a quick on-screen check looks promising.</p>
<p><code>prepare-for-printing:</code></p>
<div class="org-src-container">
<pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/bin/</span><span class="org-keyword">bash</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">Add label from ImageDescription EXIF tag to bottom of photo</span>

<span class="org-variable-name">destination</span>=~/cloud/print
<span class="org-variable-name">description_field</span>=ImageDescription
<span class="org-variable-name">font</span>=Alegreya-Regular
<span class="org-variable-name">SAVEIFS</span>=$<span class="org-variable-name">IFS</span>
<span class="org-variable-name">IFS</span>=$(<span class="org-builtin">echo</span> -en <span class="org-string">"\n\b"</span>)
<span class="org-variable-name">border</span>=50
<span class="org-variable-name">output_width_inches</span>=6
<span class="org-variable-name">output_height_inches</span>=4

<span class="org-comment-delimiter"># </span><span class="org-comment">NAME_REPLACEMENTS is an environment variable that's a sed expression</span>
<span class="org-comment-delimiter"># </span><span class="org-comment">for any name replacements, like s/A-/Actual name goes here/g</span>

<span class="org-keyword">for</span> file<span class="org-keyword"> in</span> $<span class="org-variable-name">*</span>; <span class="org-keyword">do</span>
  <span class="org-variable-name">description</span>=$(exiftool -s -s -s -$<span class="org-variable-name">description_field</span> <span class="org-string">"$file"</span> <span class="org-sh-escaped-newline">\</span>
     | sed <span class="org-string">"s/\"/\\\"/g;s/'/\\\'/g;$NAME_REPLACEMENTS"</span>)
  <span class="org-variable-name">date</span>=$(exiftool -s -s -s -DateTimeOriginal <span class="org-string">"$file"</span> <span class="org-sh-escaped-newline">\</span>
     | cut -c 1-10 | sed s/:/-/g)
  <span class="org-variable-name">width</span>=$(identify -format <span class="org-string">"%w"</span> <span class="org-string">"$file"</span>)
  <span class="org-variable-name">height</span>=$(identify -format <span class="org-string">"%h"</span> <span class="org-string">"$file"</span>)
  <span class="org-variable-name">largest</span>=$(( $<span class="org-variable-name">width</span> &gt; $<span class="org-variable-name">height</span> ? $<span class="org-variable-name">width</span> : $<span class="org-variable-name">height</span> ))
  <span class="org-variable-name">pointsize</span>=12
  <span class="org-variable-name">density</span>=$(( $<span class="org-variable-name">largest</span> / $<span class="org-variable-name">output_width_inches</span> ))
  <span class="org-variable-name">correct_height</span>=$(( $<span class="org-variable-name">output_height_inches</span> * $<span class="org-variable-name">density</span> ))
  <span class="org-variable-name">captionwidth</span>=$(( $<span class="org-variable-name">width</span> - $<span class="org-variable-name">border</span> * 2 ))
  convert <span class="org-string">"$file"</span> -density $<span class="org-variable-name">density</span> -units PixelsPerInch <span class="org-sh-escaped-newline">\</span>
    -gravity North -extent ${<span class="org-variable-name">width</span>}x${<span class="org-variable-name">correct_height</span>} <span class="org-sh-escaped-newline">\</span>
    -strip <span class="org-string">\(</span> -undercolor white -background white <span class="org-sh-escaped-newline">\</span>
    -fill black -font <span class="org-string">"$font"</span> -bordercolor White <span class="org-sh-escaped-newline">\</span>
    -gravity SouthWest -border $<span class="org-variable-name">border</span> -pointsize $<span class="org-variable-name">pointsize</span> <span class="org-sh-escaped-newline">\</span>
    -size ${<span class="org-variable-name">captionwidth</span>}x  caption:<span class="org-string">"$date $description"</span> <span class="org-string">\)</span> <span class="org-sh-escaped-newline">\</span>
    -composite <span class="org-string">"$destination/$file"</span>
<span class="org-keyword">done</span>
<span class="org-variable-name">IFS</span>=$<span class="org-variable-name">SAVEIFS</span>
gwenview $<span class="org-variable-name">destination</span>
</pre>
</div>
<p>Here's my current <code>rename-based-on-exif</code>, too. I modified it to use the <code>ImageDescription</code> or the <code>UserComment</code> field, and I switched to using Unicode stars and labels instead of # to minimize problems with exporting to HTML.</p>
<div class="org-src-container">
<pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/bin/</span><span class="org-keyword">bash</span>

<span class="org-variable-name">date</span>=<span class="org-string">"\${DateTimeOriginal;s/[ :]/-/g}"</span>
<span class="org-variable-name">rating</span>=<span class="org-string">"\${Rating;s/([1-5])/'★' x \$1/e}"</span>
<span class="org-variable-name">tags</span>=<span class="org-string">"\${Subject;s/^/🏷/;s/, / 🏷/g}"</span>
<span class="org-variable-name">field</span>=FileName  <span class="org-comment-delimiter"># </span><span class="org-comment">TestName for testing</span>
exiftool -m -<span class="org-string">"$field&lt;$date $rating \${ImageDescription} $tags.%e"</span> \
         -<span class="org-string">"$field&lt;$date $rating \${UserComment} $tags.%e"</span> <span class="org-string">"$@"</span></pre>
</div>
<p>In order to upload my fancy-shmancy Unicode-filenamed files, I also had to convert my WordPress database from utf8 to utf8mb4. <a href="https://florianbrinkmann.com/en/3457/switch-wordpress-from-utf8-to-utf8mb4-retrospectively/">This upgrade plugin</a> was very helpful.</p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2018%2F06%2Fexperimenting-with-adding-labels-to-photos%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Oops report: Moving from i386 to amd64 on my server</title>
		<link>https://sachachua.com/blog/2018/03/oops-report-moving-from-i386-to-amd64-on-my-server/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sun, 25 Mar 2018 21:44:00 GMT</pubDate>
    <category>geek</category>
<category>linux</category>
		<guid isPermaLink="false">https://sachachua.com/blog/?p=29191</guid>
		<description><![CDATA[<p> I was trying to install Docker on my Linode virtual private server so that I could experiment with containers. I had a problem with the error &#8220;no supported platform found in manifest list.&#8221; Eventually, I realized that <code>dpkg &#45;&#45;print-architecture</code> showed that my Ubuntu package architecture was i386 even though my server was 64-bit. That was probably due to upgrading in-place through the years, starting with a 32-bit version of Ubuntu 10. </p>
<p> I tried <code>dpkg &#45;&#45;add-architecture amd64</code>, which let me install the <code>docker-ce</code> package from the Docker repository. Unfortunately, I didn't review it carefully enough (the perils of SSHing from my cellphone), and installing that removed a bunch of other i386 packages like sudo, ssh, and screen. Ooops! </p>
<p> Even though we've been working on weaning lately, I decided that letting A- nurse a long time in her sleep might give me a little time to try to fix things. I used Linode's web-based console to try to log in. I forgot the root password, so I used their tool for resetting the root password. After I got that sorted out, though, I found that I couldn't resolve network resources. I'd broken the system badly enough that I needed to use another rescue tool to mount my drives, chroot to them, and install stuff from there. I was still getting stuck. I needed more focused time. </p>
<p> Fortunately, I'd broken my server during the weekend, so W- was around to take care of A- while I tried to figure things out. I had enough free space to create another root partition and install Ubuntu 16, which was a straightforward process with Linode's Deploy Image tool. </p>
<p> I spent a few hours trying to figure out if I could set everything up in Docker containers from the start. I got the databases working, but I kept getting stymied by annoying WordPress redirection issues even after setting <code>home</code> and <code>siteurl</code> in the database and defining them in my config file. I tried adding Nginx reverse proxying to the mix, and it got even more tangled. </p>
<p> Eventually, I gave up and went back to running the services directly on my server. Because I did the new install in a separate volume, it was easy to mount the old volume and copy or symlink my configuration files. </p>
<p> Just in case I need to do this again, here are the packages that <code>apt</code> says I installed: </p>
<ul class="org-ul">
<li>General:
<ul class="org-ul">
<li>screen</li>
<li>apt-transport-https</li>
<li>ca-certificates</li>
<li>curl</li>
<li>dirmngr</li>
<li>gnupg</li>
<li>software-properties-common</li>
<li>borgbackup</li>
</ul>
</li>
<li>For the blog:
<ul class="org-ul">
<li>mysql-server</li>
<li>php-fpm</li>
<li>php-mysql</li>
<li>php-xml</li>
</ul>
</li>
<li>For Quantified Awesome:
<ul class="org-ul">
<li>ruby-bundler</li>
<li>ruby-dev</li>
</ul>
</li>
<li>For experimenting:
<ul class="org-ul">
<li>docker-compose</li>
</ul>
</li>
<li>For compiling Emacs:
<ul class="org-ul">
<li>make</li>
<li>gcc</li>
<li>g++</li>
<li>zlib1g-dev</li>
<li>libmysqlclient-dev</li>
<li>autoconf</li>
<li>texinfo</li>
<li>gnutls-dev</li>
<li>ncurses-dev</li>
</ul>
</li>
<li>From external repositories:
<ul class="org-ul">
<li><a href="https://docs.docker.com/install/linux/docker-ce/ubuntu/">docker-ce</a></li>
<li><a href="https://www.phusionpassenger.com/library/install/nginx/install/oss/xenial/">nginx-extras passenger</a></li>
<li><a href="https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions">nodejs</a></li>
<li><a href="https://apt.syncthing.net/">syncthing</a></li>
</ul>
</li>
</ul>
<p> I got the list by running: </p>
<pre class="example">
zgrep 'Commandline: apt' /var/log/apt/history.log /var/log/apt/history.log.*.gz
</pre>
<p> I saved my selections with <code>dpkg &#45;&#45;get-selections</code> so that I can load them with <code>dpkg &#45;&#45;set-selections &lt;&lt; ...; apt-get dselect-upgrade</code> if I need to do this again. </p>
<p> Symbolic links to old volume: </p>
<ul class="org-ul">
<li>/var/www</li>
<li>/usr/local</li>
<li>/home/sacha</li>
<li>/var/lib/mysql (after installing)</li>
</ul>
<p> Copied after installing &#8211; I'll probably want to tidy this up: </p>
<ul class="org-ul">
<li>/etc/nginx/sites-available</li>
<li>/etc/nginx/sites-enabled</li>
</ul>
<p> Lessons learned: </p>
<ul class="org-ul">
<li>Actually check the list of packages to remove.</li>
<li>Consider fresh installs for major upgrades.</li>
</ul>
<p> When things settle down, I should probably look into organizing one of the volumes as a proper data volume so that I can cleanly reinstall the root partition whenever I want to. </p>
<p> I also want to explore Docker again &#8211; maybe once I've wrapped my mind around how Docker, Nginx, WordPress, Passenger, virtual hosts, and subdirectories all fit together. Still, I'm glad I got my site up and running again! </p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2018%2F03%2Foops-report-moving-from-i386-to-amd64-on-my-server%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item>
	</channel>
</rss>