<?xml version="1.0" encoding="UTF-8"?><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/"
	xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" xmlns:media="http://search.yahoo.com/mrss/"
	>

<channel>
	<title>Kristina Chodorow&#039;s Blog</title>
	<atom:link href="https://kchodorow.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://kchodorow.com</link>
	<description>Snail in a Turtleneck</description>
	<lastBuildDate>Mon, 03 Feb 2025 14:47:23 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>http://wordpress.com/</generator>

<image>
	<url>https://kchodorow.com/wp-content/uploads/2019/07/cropped-snail.png?w=32</url>
	<title>Kristina Chodorow&#039;s Blog</title>
	<link>https://kchodorow.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<cloud domain='kchodorow.com' port='80' path='/?rsscloud=notify' registerProcedure='' protocol='http-post' />
<atom:link rel="search" type="application/opensearchdescription+xml" href="https://kchodorow.com/osd.xml" title="Kristina Chodorow&#039;s Blog" />
	<atom:link rel='hub' href='https://kchodorow.com/?pushpress=hub'/>
	<item>
		<title>How to sync Stripe transactions to Quickbooks</title>
		<link>https://kchodorow.com/2025/02/03/how-to-sync-stripe-transactions-to-quickbooks/</link>
					<comments>https://kchodorow.com/2025/02/03/how-to-sync-stripe-transactions-to-quickbooks/#respond</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Mon, 03 Feb 2025 14:47:23 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[accounting]]></category>
		<category><![CDATA[bookkeeping]]></category>
		<category><![CDATA[business]]></category>
		<category><![CDATA[finance]]></category>
		<category><![CDATA[financial-management]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=4000</guid>

					<description><![CDATA[When I connected my business bank account to Quickbooks, I quickly realized that the revenue numbers were wrong. Stripe takes a bite out of the transactions before sending it to my bank, so if I invoiced someone for $500, Stipe would charge a $15 fee and then my bank would see $485 as the revenue.<a class="more-link" href="https://kchodorow.com/2025/02/03/how-to-sync-stripe-transactions-to-quickbooks/">Continue reading <span class="screen-reader-text">"How to sync Stripe transactions to&#160;Quickbooks"</span></a>]]></description>
										<content:encoded><![CDATA[
<p>When I connected my business bank account to Quickbooks, I quickly realized that the revenue numbers were wrong. Stripe takes a bite out of the transactions before sending it to my bank, so if I invoiced someone for $500, Stipe would charge a $15 fee and then my bank would see $485 as the revenue. This would be copied to Quickbooks, so my sales would look like $485, not $500.</p>



<figure class="wp-block-image size-large"><img width="500" height="291" data-attachment-id="4015" data-permalink="https://kchodorow.com/2025/02/03/how-to-sync-stripe-transactions-to-quickbooks/unacceptable/" data-orig-file="https://kchodorow.com/wp-content/uploads/2025/01/unacceptable.gif" data-orig-size="500,291" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="unacceptable" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2025/01/unacceptable.gif?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2025/01/unacceptable.gif?w=500" src="https://kchodorow.com/wp-content/uploads/2025/01/unacceptable.gif?w=500" alt="" class="wp-image-4015" /></figure>



<p>However, there&#8217;s no native Stripe-&gt;Quickbooks connector. You can either install a third party extension (ಠ_ಠ) or download a CSV of transactions from Stripe and upload a CSV of transactions into Quickbooks.</p>



<p>I decided to try going the CSV route. Goals:</p>



<ul class="wp-block-list">
<li>Record the absolute sales amount, before any fees. E.g., $500</li>



<li>Record the fee for the transaction. E.g., $500 * 2.9% -&gt; -$14.50</li>



<li>Record the refunds for that fees (since we have some fee-free processing for the first $N of purchases). E.g., +$14.50</li>



<li>Record the fee for using the pleasure of Stripe billing E.g., $500 * .7% -&gt; $-3.50</li>



<li>Record the taxes on the fees Stripe charges (thanks Stripe). E.g., $3.50 * 9% = -$.32</li>
</ul>



<p>Which nets out to $500 &#8211; $14.50 + $14.50 &#8211; $3.50 &#8211; $.32 = $496.18. And, of course, this is a single payment! Customers obviously aren&#8217;t perfectly synced on billing payouts, so Stripe extrudes money into our business bank account like a poorly-filled sausage of payment blops of weird amounts. This gets very complicated and annoying to untangle on the bank account side, so I&#8217;d like it to be really clearly laid out as Stripe transactions.</p>



<h2 class="wp-block-heading">The Stripe Side</h2>



<p>We want to get all of the Stripe transactions in some sensible, readable form. To do so, go to Stripe&#8217;s <a href="https://dashboard.stripe.com/reports/balance">Balance summary</a> page and set the time frame for what you want (e.g., last month, last year, etc.). Then scroll down to &#8220;Balance change from activity&#8221; and hit &#8220;Download.&#8221; <strong>Do the same for &#8220;Payouts&#8221;</strong>. Keep the defaults. You should end up with two CSVs downloaded. (If you like this system, you can subscribe at the top of the page to have Stripe email you the reports each month.)</p>



<p>However, Quickbooks does not like the format Stripe presents these amounts in, so I put together a script to munge the CSV into a QBO-friendly form, which you can <a href="https://colab.research.google.com/drive/1p5kyPbf75h_CDd-KUsdG_aVAfNeBf4Qn?usp=sharing">see/copy/run on Colab</a>. If you don&#8217;t really care about the mechanics you can run the whole colab, download the output (stripe_2024-12.csv) and skip down to &#8220;The Quickbooks side&#8221; section of this post. </p>



<p>If you want to understand what&#8217;s going on: first let&#8217;s consider the balance change CSV (named something like Itemized_balance_change_from_activity_USD_<em>daterange</em>.csv). Stripe creates a table of the form (extra columns excluded for simplicity):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td><strong>Description</strong></td><td><strong>Created</strong></td><td><strong>Gross</strong></td><td><strong>Fee</strong></td></tr><tr><td>Llama Brush</td><td>2025-01-30</td><td>$30</td><td>$3</td></tr><tr><td>Stripe Billing Usage Fee</td><td>2025-01-29</td><td>-$10</td><td>$1</td></tr></tbody></table></figure>



<p>In QBO, we&#8217;d like to split gross and fee into two separate rows to look like:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td><strong>Description</strong></td><td><strong>Date</strong></td><td><strong>Amount</strong></td></tr><tr><td>Llama Brush</td><td>2025-01-30</td><td>$30</td></tr><tr><td>Llama Brush Fee</td><td>2025-01-30</td><td>-$3</td></tr><tr><td>Stripe Billing Usage Fee</td><td>2025-01-29</td><td>-$10</td></tr><tr><td>Stripe Billing Usage Fee Tax</td><td>2025-01-29</td><td>-$1</td></tr></tbody></table></figure>



<p>This takes a few steps: first, we want to make all of the fees negative. Then we want to split each row into two rows: one row for its actual amount and one row for its fees. Then we want everything to be named the way QBO expects (not necessary, but involves less clicks on import) and remove $0 rows.</p>



<p>Making the fees negative is easy, we just multiply that column by -1:</p>



<pre class="wp-block-code"><code>df = df.assign(fee=df.fee * -1)</code></pre>



<p>Splitting each row into two rows is a little more complex. We can use Pandas&#8217;s <a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.stack.html">stack</a> function, but it discards all of the other columns so we are going to &#8220;store&#8221; the other columns we want (<code>created</code> and <code>description</code>) in the index:</p>



<pre class="wp-block-code"><code>df = df.set_index(&#091;'created', 'description'])
# This makes the stacked index have the name "amount_type"
# (instead of "level_2").
df.columns.name = 'amount_type'
df = df&#091;&#091;'gross', 'fee']].stack()
# This names the stacked column "amount" (instead of "0").
df.name = 'amount'</code></pre>



<p>Now we have a dataframe with three levels of index and one coumn. </p>



<figure class="wp-block-image size-large"><img width="1024" height="355" data-attachment-id="4009" data-permalink="https://kchodorow.com/2025/02/03/how-to-sync-stripe-transactions-to-quickbooks/screenshot-2025-01-30-at-11-22-21-am/" data-orig-file="https://kchodorow.com/wp-content/uploads/2025/01/screenshot-2025-01-30-at-11.22.21e280afam.png" data-orig-size="1306,454" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Screenshot 2025-01-30 at 11.22.21 AM" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2025/01/screenshot-2025-01-30-at-11.22.21e280afam.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2025/01/screenshot-2025-01-30-at-11.22.21e280afam.png?w=750" src="https://kchodorow.com/wp-content/uploads/2025/01/screenshot-2025-01-30-at-11.22.21e280afam.png?w=1024" alt="" class="wp-image-4009" /></figure>



<p>Reset the index to get it back to &#8220;normal&#8221; data and one index. </p>



<pre class="wp-block-code"><code>df = df.reset_index()</code></pre>



<p>Then I&#8217;d like each &#8220;fee&#8221; description to be suffixed with &#8220;fee&#8221; and each tax description to be suffixed with tax:</p>



<pre class="wp-block-code"><code># The usage fee fee is actually tax (I asked support because I 
# was so annoyed).
tax_mask = (df.amount_type == 'fee') &amp; df.description.str.contains('Usage Fee')
# Any fee that isn't a tax.
fee_mask = (df.amount_type == 'fee') &amp; (~tax_mask)
df.loc&#091;tax_mask, 'description'] = df&#091;'description'] + ' tax'
df.loc&#091;fee_mask, 'description'] = df&#091;'description'] + ' fee'</code></pre>



<p>Finally, let&#8217;s do some housekeeping. Drop the &#8220;amount_type&#8221; column, rename &#8220;created&#8221; to &#8220;date&#8221;, and drop any rows that are $0 (Quickbooks doesn&#8217;t understand $0):</p>



<pre class="wp-block-code"><code>df = df.drop(columns=&#091;'amount_type']).rename(columns={'created': 'date'})
df = df&#091;df.amount != 0]</code></pre>



<p>If we create a &#8220;Stripe&#8221; account in Quickbooks and upload this, it will have all of the income and fees we generated on Stripe. However, Quickbooks will entirely refuse to match this up with the Stripe payouts in our bank account, since the bank payouts are batched and won&#8217;t match up with <strong>any</strong> of the amounts in our current CSV. Thus, it&#8217;ll look like we earned this income twice! (Once in Stripe and once in our bank account, minus fees.) This is where our second &#8220;Payouts&#8221; CSV comes in.</p>



<p>Load it into a second dataframe. This one is much simpler, so we&#8217;ll just rename and pull out the columns we want, then concatenate it with our transactions dataframe.</p>



<pre class="wp-block-code"><code>payouts = pd.read_csv('...')
payouts = (
    payouts
    .rename(columns={'effective_at': 'date', 'gross': 'amount'})
    .assign(
        amount=lambda x: x.amount * -1,
        description=lambda x: 'Payout ' + x.payout_id
    )
    &#091;&#091;'date', 'amount', 'description']]
)
df = pd.concat(&#091;df, payouts]).sort_values('date')</code></pre>



<p>Last up, store it in a CSV:</p>



<pre class="wp-block-code"><code>df.to_csv('stripe_2024-12.csv', index=False, header=True)</code></pre>



<h2 class="wp-block-heading">The Quickbooks Side</h2>



<p>Now go to your Quickbooks account and create a Stripe &#8220;bank&#8221; account if one doesn&#8217;t exist already. Then under Transactions -&gt; Bank Transactions select the arrow next to &#8220;Link Account&#8221; and select the &#8220;Upload File&#8221; option.</p>



<figure class="wp-block-image size-large"><img width="688" height="562" data-attachment-id="4016" data-permalink="https://kchodorow.com/2025/02/03/how-to-sync-stripe-transactions-to-quickbooks/screenshot-2025-02-01-at-11-42-49-am/" data-orig-file="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.42.49e280afam.png" data-orig-size="688,562" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Screenshot 2025-02-01 at 11.42.49 AM" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.42.49e280afam.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.42.49e280afam.png?w=688" src="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.42.49e280afam.png?w=688" alt="" class="wp-image-4016" /></figure>



<p>Under &#8220;Manually upload your transactions,&#8221; select the stripe_2024-12.csv (or whatever) file you created above and hit &#8220;Continue.&#8221; In &#8220;Which account are these transactions from?&#8221; select the &#8220;Stripe&#8221; account.</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="449" data-attachment-id="4018" data-permalink="https://kchodorow.com/2025/02/03/how-to-sync-stripe-transactions-to-quickbooks/screenshot-2025-02-01-at-11-46-18-am/" data-orig-file="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.46.18e280afam.png" data-orig-size="2010,882" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Screenshot 2025-02-01 at 11.46.18 AM" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.46.18e280afam.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.46.18e280afam.png?w=750" src="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.46.18e280afam.png?w=1024" alt="" class="wp-image-4018" /></figure>



<p>On the next page (&#8220;Let&#8217;s set up your file in QuickBooks&#8221;), most of the defaults are fine, but for &#8220;What’s the date format used in your file?&#8221; select the last option: yyyy-MM-dd. Hit &#8220;Continue.&#8221;</p>



<p>It&#8217;ll give you a preview of what it&#8217;s importing at that point. If it looks okay, hit the top checkbox to select them all and hit Continue. Then confirm yes, you really do want to import these.</p>



<p>Finally, you&#8217;ll get back to the transaction categorization screen! Quickbooks should, at this point, automatically match up your Stripe payouts to you bank account.</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="75" data-attachment-id="4020" data-permalink="https://kchodorow.com/2025/02/03/how-to-sync-stripe-transactions-to-quickbooks/screenshot-2025-02-01-at-11-50-29-am/" data-orig-file="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.50.29e280afam.png" data-orig-size="2236,164" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Screenshot 2025-02-01 at 11.50.29 AM" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.50.29e280afam.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.50.29e280afam.png?w=750" src="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.50.29e280afam.png?w=1024" alt="" class="wp-image-4020" /></figure>



<p>This gives me, finally, the view I want: actual top-line revenue, service fees, taxes paid, and payouts to the bank account.</p>



<p>Hopefully this will be helpful for other programmers dealing with Stripe + Quickbooks! However, I&#8217;m a newbie at Quickbooks, so let me know if you see any improvements I could make.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2025/02/03/how-to-sync-stripe-transactions-to-quickbooks/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		
		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>

		<media:content url="https://kchodorow.com/wp-content/uploads/2025/01/unacceptable.gif?w=500" medium="image" />

		<media:content url="https://kchodorow.com/wp-content/uploads/2025/01/screenshot-2025-01-30-at-11.22.21e280afam.png?w=1024" medium="image" />

		<media:content url="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.42.49e280afam.png?w=688" medium="image" />

		<media:content url="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.46.18e280afam.png?w=1024" medium="image" />

		<media:content url="https://kchodorow.com/wp-content/uploads/2025/02/screenshot-2025-02-01-at-11.50.29e280afam.png?w=1024" medium="image" />
	</item>
		<item>
		<title>Pixelating images in Python</title>
		<link>https://kchodorow.com/2024/04/10/pixelating-images-in-python/</link>
					<comments>https://kchodorow.com/2024/04/10/pixelating-images-in-python/#respond</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Wed, 10 Apr 2024 17:30:11 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[machine-learning]]></category>
		<category><![CDATA[opencv]]></category>
		<category><![CDATA[pixel art]]></category>
		<category><![CDATA[python]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=3966</guid>

					<description><![CDATA[Generating pixel art with AI has mixed results, but I&#8217;ve found you can improve the output with some basic heuristics. Last year I wrote several libraries for this and then stuffed them in a drawer, but since there&#8217;s been some interest in the topic: here&#8217;s how to make AI-generated pixel art a bit better with<a class="more-link" href="https://kchodorow.com/2024/04/10/pixelating-images-in-python/">Continue reading <span class="screen-reader-text">"Pixelating images in&#160;Python"</span></a>]]></description>
										<content:encoded><![CDATA[
<p>Generating pixel art with AI has mixed results, but I&#8217;ve found you can improve the output with some basic heuristics. Last year I wrote several libraries for this and then stuffed them in a drawer, but since there&#8217;s been some interest in the topic: here&#8217;s how to make AI-generated pixel art a bit better with good ol&#8217; image processing.</p>



<p>I&#8217;m going to use Python and, if you&#8217;d prefer not to write it yourself, I created a <a href="https://colab.research.google.com/drive/1k6C_anrDXb9odsHZdUAaJMC6q9nnE6jf#scrollTo=cj6zqpOMonr_">public colab</a> that you can just step through.</p>



<p>We&#8217;re going to use OpenCV for image processing, which has an <em>interesting</em> Python API (it&#8217;s actually a C++ library and it shows).</p>



<pre class="wp-block-code"><code># If you're using colab, no need to install anything. If you're 
# running locally, run:
# $ pip install opencv-python-headless
# You'll also need numpy.
import cv2 as cv

img = cv.imread(path_to_your_jpg, cv.IMREAD_UNCHANGED) 
print(img.shape)</code></pre>



<p>This should show the dimensions of the image you uploaded. (If you get an exception about not being able to access shape on None, you probably didn&#8217;t put in the right path. Try using the absolute path to the file.)</p>



<p>I&#8217;m using the Wikipedia image for <a href="https://en.wikipedia.org/wiki/Neuschwanstein_Castle">Neuschwanstein Castle</a>:</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="500" height="374" data-attachment-id="3980" data-permalink="https://kchodorow.com/2024/04/10/pixelating-images-in-python/schloss_neuschwanstein_2013/" data-orig-file="https://kchodorow.com/wp-content/uploads/2024/04/schloss_neuschwanstein_2013.jpg" data-orig-size="500,374" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;Thomas Wolf - TW PHOTOMEDIA&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;(c) Thomas Wolf (www.foto-tw.de)&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="schloss_neuschwanstein_2013" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2024/04/schloss_neuschwanstein_2013.jpg?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2024/04/schloss_neuschwanstein_2013.jpg?w=500" src="https://kchodorow.com/wp-content/uploads/2024/04/schloss_neuschwanstein_2013.jpg?w=500" alt="" class="wp-image-3980" srcset="https://kchodorow.com/wp-content/uploads/2024/04/schloss_neuschwanstein_2013.jpg 500w, https://kchodorow.com/wp-content/uploads/2024/04/schloss_neuschwanstein_2013.jpg?w=150 150w, https://kchodorow.com/wp-content/uploads/2024/04/schloss_neuschwanstein_2013.jpg?w=300 300w" sizes="(max-width: 500px) 100vw, 500px" /></figure>



<p>Let&#8217;s see how pixel-y we can get it!</p>



<p><code>img</code> is basically a three dimensional array (height x width x color). That is, you can access any given pixel&#8217;s color by looking at its (y, x) coordinate:</p>



<pre class="wp-block-code"><code>&gt; img&#091;123]&#091;45]
array(&#091;243, 218, 176], dtype=uint8)</code></pre>



<p>You can see the image by just running &#8220;img&#8221; in colab. However, you&#8217;ll notice that the colors are off and it looks slightly post-apocalyptic:</p>



<pre class="wp-block-code"><code># If you run:
img
# you get:</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" width="500" height="374" data-attachment-id="3978" data-permalink="https://kchodorow.com/2024/04/10/pixelating-images-in-python/orangesky/" data-orig-file="https://kchodorow.com/wp-content/uploads/2024/04/orangesky.png" data-orig-size="500,374" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="orangesky" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2024/04/orangesky.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2024/04/orangesky.png?w=500" src="https://kchodorow.com/wp-content/uploads/2024/04/orangesky.png?w=500" alt="" class="wp-image-3978" srcset="https://kchodorow.com/wp-content/uploads/2024/04/orangesky.png 500w, https://kchodorow.com/wp-content/uploads/2024/04/orangesky.png?w=150 150w, https://kchodorow.com/wp-content/uploads/2024/04/orangesky.png?w=300 300w" sizes="(max-width: 500px) 100vw, 500px" /></figure>



<p>This is because OpenCV assumes that pixel color is ordered blue-green-red, not RGB, so it&#8217;s swapping reds and blues.  (This is apparently a historical accident based on how <a href="https://stackoverflow.com/a/33787594/4243">camera manufacturers did things</a>.) We can swap the colors back to &#8220;normal&#8221; by using cv&#8217;s color transformation function:</p>



<pre class="wp-block-code"><code>img = cv.cvtColor(img, cv.COLOR_BGR2RGB)
# Display the image again. Should look normal now, color-wise.
img</code></pre>



<p>Okay, now we can actually get into image processing! First, we&#8217;re going to handle the issue where AI generated images tend to use too many colors for pixel art. I found seven colors was a good sweet spot for pleasingly-retro-but-still-has-nuance, but you can experiment. More colors looked &#8220;too detailed&#8221; and fewer colors started losing shapes that I wanted. (Heuristically determining a good number of colors from the image&#8217;s size and complexity is left as an exercise to the reader.)</p>



<p>So you have seven buckets. For each pixel in the image, which color bucket should you put it in? And what should the colors be? Luckily, we don&#8217;t have to figure this out ourselves: an algorithm called <a href="https://scikit-learn.org/stable/modules/clustering.html#k-means">k-means clustering</a> figures out what sensible buckets would be, based on the data, and clusters our data into these buckets.</p>



<p>However, first we need to munge our data a little. As mentioned above, <code>img</code> is currently a three-dimensional array (so something like 100 x 200 x 3). We just want to pass the clustering algorithm a list of colors, so we want to flatten our 3D array into a flat list of 20,000 x 3 (100*200=20,000):</p>



<pre class="wp-block-code"><code>import numpy as np

# Magic incantation to flatten the array.
pixels = img.reshape((-1, 3))
# Values are currently uint8 type. Clustering only works on 
# floats, so convert.
pixels = np.float32(pixels)</code></pre>



<p>Now we actually call the <a href="https://docs.opencv.org/3.4/d1/d5c/tutorial_py_kmeans_opencv.html">k-means function</a>. I&#8217;ve attempted to give variables sensible names, but OpenCV is not making this easy to understand:</p>



<pre class="wp-block-code"><code>num_colors = 7
termination_criteria = cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER
num_iter = 10
epsilon = 1.0
criteria = (termination_criteria, num_iter, epsilon)

correctness, clusters, centers = cv.kmeans(
    data=pixels, K=num_colors, bestLabels=None, criteria=criteria, attempts=10,
    flags=cv.KMEANS_RANDOM_CENTERS)</code></pre>



<p>The important data returned is the centers (i.e., the color assigned to each bucket) and the clusters (a mapping of each pixel to the bucket it belongs in). We can draw the chosen colors as a nice palette:</p>



<pre class="wp-block-code"><code>palette = np.zeros((100, 100 * len(centers), 3), np.uint8)

for i, color in enumerate(centers):
  cv.rectangle(
      palette, (100 * i, 0), (100 * (i + 1), 100), color.tolist(), -1)
palette</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" width="700" height="100" data-attachment-id="3986" data-permalink="https://kchodorow.com/2024/04/10/pixelating-images-in-python/palette/" data-orig-file="https://kchodorow.com/wp-content/uploads/2024/04/palette.png" data-orig-size="700,100" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="palette" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2024/04/palette.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2024/04/palette.png?w=700" src="https://kchodorow.com/wp-content/uploads/2024/04/palette.png?w=700" alt="" class="wp-image-3986" srcset="https://kchodorow.com/wp-content/uploads/2024/04/palette.png 700w, https://kchodorow.com/wp-content/uploads/2024/04/palette.png?w=150 150w, https://kchodorow.com/wp-content/uploads/2024/04/palette.png?w=300 300w" sizes="(max-width: 700px) 100vw, 700px" /></figure>



<p>Then we can do some *magic* (of the matrix variety) and mush our mapping of pixel-to-bucket back into a height x width x color array:</p>



<pre class="wp-block-code"><code># Map each pixel to the correct color for its bucket.
bucketed_img = centers&#091;clusters.flatten()]
# Reshape into the original image's shape.
bucketed_img.reshape((img.shape))
# Let's see what we got:
bucketed_img</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" width="500" height="374" data-attachment-id="3989" data-permalink="https://kchodorow.com/2024/04/10/pixelating-images-in-python/bucketed/" data-orig-file="https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png" data-orig-size="500,374" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="bucketed" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png?w=500" src="https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png?w=500" alt="" class="wp-image-3989" srcset="https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png 500w, https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png?w=150 150w, https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png?w=300 300w" sizes="(max-width: 500px) 100vw, 500px" /></figure>



<p>Nice! That&#8217;s already looking more like pixel art. Look at that sky &#8220;gradient&#8221; with those clouds. There&#8217;s plenty more we can do: the palette has several very similar colors and there are a bunch of &#8220;compression artifacts&#8221; from removing colors (e.g., the tip of the tallest steeple), some of which I&#8217;ll cover fixing in my next post. But regardless, with a few lines of code, you can turn any image into &#8220;pixel art&#8221; (of dubious quality).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2024/04/10/pixelating-images-in-python/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		
		<media:thumbnail url="https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png" />
		<media:content url="https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png" medium="image">
			<media:title type="html">bucketed</media:title>
		</media:content>

		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>

		<media:content url="https://kchodorow.com/wp-content/uploads/2024/04/schloss_neuschwanstein_2013.jpg?w=500" medium="image" />

		<media:content url="https://kchodorow.com/wp-content/uploads/2024/04/orangesky.png?w=500" medium="image" />

		<media:content url="https://kchodorow.com/wp-content/uploads/2024/04/palette.png?w=700" medium="image" />

		<media:content url="https://kchodorow.com/wp-content/uploads/2024/04/bucketed.png?w=500" medium="image" />
	</item>
		<item>
		<title>Login via command line</title>
		<link>https://kchodorow.com/2023/01/09/login-via-command-line/</link>
					<comments>https://kchodorow.com/2023/01/09/login-via-command-line/#respond</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Tue, 10 Jan 2023 04:12:48 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=3952</guid>

					<description><![CDATA[I&#8217;m working on a command-line tool that will require user login, so I wanted to have the flow that all the snazzy command-line clis use: pop up a browser window and ask you to login with &#60;known provider&#62;, then pass back something to the command line. Unfortunately, I had no idea what this type of<a class="more-link" href="https://kchodorow.com/2023/01/09/login-via-command-line/">Continue reading <span class="screen-reader-text">"Login via command&#160;line"</span></a>]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;m working on a command-line tool that will require user login, so I wanted to have the flow that all the snazzy command-line clis use: pop up a browser window and ask you to login with &lt;known provider&gt;, then pass back something to the command line. Unfortunately, I had no idea what this type of login was called or how to do it. </p>



<p><a href="https://hasinthaindrajee.medium.com/browser-sso-for-cli-applications-b0be743fa656">This article</a> was great, and went through enough of the flow that I got the idea and finished it up on my own. The (kind of ridiculous) flow is:</p>



<ul class="wp-block-list">
<li>Start a local webserver.</li>



<li>Open a browser pointing to the platform you want to use to login.</li>



<li>&#8230;passing the local webserver&#8217;s address in as the redirect for post-login.</li>



<li>Receive the response on the webserver and parse it.</li>
</ul>



<p>I&#8217;m using Python, so in more detail: first we start a local webserver. I&#8217;m doing this in a separate thread, because I need to do some other work while the webserver is handling stuff.</p>



<pre class="wp-block-code"><code>import http.server
import threading

class LoginManager:
  def __init__(self):
    self._server = None
    self._port = 0

  def start_web_server(self):
    """Kick off a thread for the local webserver."""
    th = threading.Thread(target=self._start_local_server)
    th.start()

  def _start_local_server(self):
    self._server = http.server.HTTPServer(('localhost', 0), Handler)
    self._port = self._server.server_port
    print(f'Serving on port {self._port}')
    self._server.serve_forever()</code></pre>



<p><code>_start_local_server</code> is the interesting part here. I don&#8217;t want to risk bumping into a port conflict (imagine how confusing it would be to not be able to log into a website because you happened to be running some emulator), so I&#8217;m going to make the OS give us an open port. Also, we only want the server to listen to localhost (no outside traffic). The pair <code>('localhost', 0)</code> is the host and port, which binds the server to only accept requests to localhost and says &#8220;give me an open port.&#8221;</p>



<p>Because we&#8217;re not specifying a port, we then have to figure out what port we&#8217;re using. So I immediately ask the server what port was chosen (and then print it, for my own debugging).</p>



<p>Next up, we need to open a browser.</p>



<pre class="wp-block-code"><code>  def open_browser(self):
    """Opens the browser to the login page."""
    # Waits for the server to start.
    while self._server is None:
      time.sleep(1)
    url = self.create_login_url()
    system = platform.system()
    if system == 'Darwin:
      cmd = &#091;'open', url]
    elif system == 'Linux':
      cmd = &#091;'xdg-open', url]
    elif system == 'Windows':
      cmd = &#091;'cmd', '/c', 'start', url.replace('&amp;', '^&amp;')]
    else:
      raise RuntimeError(f'Unsupported system: {system}')
    subprocess.run(cmd, check=True)</code></pre>



<p>This is just copied from the article I linked above. I&#8217;m on Darwin and it works great, YMMV.</p>



<p>The URL is returned from <code>create_login_url</code>. This is where the article leaves us to our own devices. My default device is &#8220;Google probably has a free service that does this,&#8221; which <a href="https://developers.google.com/identity/openid-connect/openid-connect#python">seems to be true</a> for this case. I created a new client <a href="https://console.cloud.google.com/apis/credentials">credential</a> under &#8220;OAuth 2.0 Client IDs&#8221;</p>



<p>In the client&#8217;s configuration you have to specify &#8220;Authorized JavaScript origins&#8221; and &#8220;Authorized redirect URIs.&#8221; We want URIs that match <a href="http://localhost:N">http://localhost:N</a>, where N is going to change each run. However, the ? sternly warns you against URIs containing wildcards, so how do we specify N? The answer is: don&#8217;t. Turns out this is a prefix match, so put &#8220;<a href="http://localhost&#038;#8221" rel="nofollow">http://localhost&#038;#8221</a>; in the questionable-named &#8220;URIs 1&#8221; for each section. This does mean that you have to serve the redirect from root (e.g., you have to redirect to localhost:12345, not localhost:12345/login-success-page), but this is just a scratch server for handling this one request, so that shouldn&#8217;t be a huge deal.</p>



<p>Armed with this configuration, we can now implement the URL gen function:</p>



<pre class="wp-block-code"><code>  def create_login_url(self) -&gt; str:
    """Generate the login URL."""
    nonce = hashlib.sha256(os.urandom(1024)).hexdigest()
    return (
      'https://accounts.google.com/o/oauth2/v2/auth?'
      'response_type=code&amp;'
      f'client_id={_CLIENT_ID}&amp;'
      'scope=openid%20email&amp;'
      f'redirect_uri=http%3A//localhost:{self._port}&amp;'
      f'nonce={nonce}')</code></pre>



<p>Another stern warning that we&#8217;re ignoring is that the docs &#8220;highly recommend&#8221; <a href="https://developers.google.com/identity/openid-connect/openid-connect#createxsrftoken">passing a &#8220;state&#8221; parameter</a>. The docs assume you&#8217;re using this flow to have users log into your website, so your server has to be cautious that it&#8217;s getting a response from your actual user, not a man-in-the-middle attacker. However, we are running this direct from command line to Google, so using the state doesn&#8217;t make a lot of sense.</p>



<p>The final piece is to actually handle that redirect request from the browser. The browser passes back the ID token as a base64-encoded cookie, so we can use Python&#8217;s built-in libraries to extract it:</p>



<pre class="wp-block-code"><code>class Handler(http.server.SimpleHTTPRequestHandler):
  """Handle the response from accounts.google.com."""

  # Sketchy static variable to hold response.
  info = None

  def do_GET(self):
    c = http.cookies.SimpleCookie(self.headers.get('Cookie'))
    jwt = c&#091;'idToken'].value
    if not jwt:
      # If the server gets a non-login request.
      return
    # Google's cookie comes in the format: "&#091;header].&#091;idToken]."
    # where &#091;header] and &#091;idToken] are base64 encoded. However,
    # "." isn't a base64 thing, so we have to split up the 
    # cookie before decoding.
    pieces = jwt.split('.')
    info = None
    for piece in pieces:
      # The base64 might not have enough padding for Python's
      # decoder to roll with (JS is fine with it, but Python
      # needs a couple of extra trailing =s).
      i = base64.b64decode(f'{piece}==').decode('utf-8')
      info = json.loads(i)
      if is_header(info):
        continue
      # Otherwise, "info" is the value we want! Actually do
      # something with it here:
      do_something_with(info)
      break
    self.wfile.write(b'All set, feel free to close this tab')

def is_header(info: dict&#091;str, Any]) -&gt; bool:
  return 'alg' in info and 'typ' in info</code></pre>



<p>This is full of gross little implementation details. I&#8217;ve tried to comment on them above.  <code>info</code> looks something like:</p>



<pre class="wp-block-code"><code>{
  'name': 'Alice Doe', 
  'email': 'adoe@example.com', 
  'email_verified': True, 
  'auth_time': 1659727260, 
  'user_id': '...', 
  'firebase': {
    'identities': {
      'email': &#091;'adoe@example.com'], 
      'google.com': &#091;'&lt;long string&gt;']
    }, 
    'sign_in_provider': 'google.com'
  }, 
  'iat': 1659727260, 
  'exp': 1659730860, 
  'aud': 'lien-288519', 
  'iss': 'https://securetoken.google.com/&lt;your project&gt;', 
  'sub': '...'
}</code></pre>



<p>Then we just have to put this all together:</p>



<pre class="wp-block-code"><code>def main(argv):
  m = LoginManager()
  m.start_web_server()
  m.open_browser()
  # Probably do something with info here, and shutdown the 
  # server.

if __name__ == '__main__':
  app.run(main)</code></pre>



<p>Now when we run, it&#8217;ll create a server, pop open a browser, wait for us to log in, then redirect back to the server we just started, show a polite message to the user in the browser, and we can do something with the user&#8217;s token.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2023/01/09/login-via-command-line/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		
		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>
	</item>
		<item>
		<title>Generating Voronoi cells in Python</title>
		<link>https://kchodorow.com/2022/10/13/generating-voronoi-cells-in-python/</link>
					<comments>https://kchodorow.com/2022/10/13/generating-voronoi-cells-in-python/#respond</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Thu, 13 Oct 2022 14:32:11 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=3927</guid>

					<description><![CDATA[Voronoi cells are basically the shape you see soap suds make. They have a lot of cool properties that I wanted to use for image generation, but I didn&#8217;t want to have to figure out the math myself. I found this really excellent tutorial on generating Voronoi cells, which goes into some interesting history about<a class="more-link" href="https://kchodorow.com/2022/10/13/generating-voronoi-cells-in-python/">Continue reading <span class="screen-reader-text">"Generating Voronoi cells in&#160;Python"</span></a>]]></description>
										<content:encoded><![CDATA[
<p>Voronoi cells are basically the shape you see soap suds make. They have a lot of cool properties that I wanted to use for <a href="https://linseed.io">image generation</a>, but I didn&#8217;t want to have to figure out the math myself. I found this <a href="https://learnopencv.com/delaunay-triangulation-and-voronoi-diagram-using-opencv-c-python/">really excellent tutorial</a> on generating Voronoi cells, which goes into some interesting history about them, too!</p>



<p>However, the Python code was a little out-of-date (and I think the author&#8217;s primary language was C++), so I wanted to clean up the example a bit. </p>



<p>It&#8217;s always a little tricky combining numpy and cv2, since numpy is column-major and cv2 is row-major (or maybe visa versa?) so I&#8217;m doing a rectangle instead of a square to make sure the coordinates are all ordered correctly. I started with some initialization:</p>



<pre class="wp-block-code"><code>import cv2
from matplotlib import pyplot as plt
import numpy as np

random.seed(42)
width = 256
height = 128
num_points = 25</code></pre>



<p>Then we can use the Subdiv2D class and add a point for each cell:</p>



<pre class="wp-block-code"><code>subdiv  = cv2.Subdiv2D((0, 0, width, height))

def RandomPoint():
  return (int(random.random() * width), int(random.random() * height))

for i in range(num_points):
  subdiv.insert(RandomPoint())</code></pre>



<p>Then it just spits out the cells!</p>



<pre class="wp-block-code"><code># Note that this is height x width!
img = np.zeros((height, width, 3), dtype=np.uint8)

def RandomColor():
  """Generates a random RGB color."""
  return (
    random.randint(0, 256), 
    random.randint(0, 256), 
    random.randint(0, 256))

# idx is the list of indexes you want to get, &#091;] means all.
facets, centers = subdiv.getVoronoiFacetList(idx=&#091;])
for facet, center in zip(facets, centers):
  # Convert shape coordinates (floats) to int.
  ifacet = np.array(facet, int)

  # Draw the polygon.
  cv2.fillConvexPoly(img, ifacet, RandomColor(), cv2.LINE_AA, 0)

  # Draw a black edge around the polygon.
  cv2.polylines(img, np.array(&#091;ifacet]), True, (0, 0, 0), 1, cv2.LINE_AA, 0)

  # Draw the center point of each cell.
  cv2.circle(
    img, (int(center&#091;0]), int(center&#091;1])), 3, (0, 0, 0), cv2.FILLED, cv2.LINE_AA, 0)</code></pre>



<p>Finally,  write img to a file or just it display with:</p>



<pre class="wp-block-code"><code>plt.imshow(img)</code></pre>



<p>If you use 42 as the seed, you should see exactly:</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="377" height="202" data-attachment-id="3933" data-permalink="https://kchodorow.com/2022/10/13/generating-voronoi-cells-in-python/voronoi/" data-orig-file="https://kchodorow.com/wp-content/uploads/2022/10/voronoi.png" data-orig-size="377,202" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="voronoi" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2022/10/voronoi.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2022/10/voronoi.png?w=377" src="https://kchodorow.com/wp-content/uploads/2022/10/voronoi.png?w=377" alt="" class="wp-image-3933" srcset="https://kchodorow.com/wp-content/uploads/2022/10/voronoi.png 377w, https://kchodorow.com/wp-content/uploads/2022/10/voronoi.png?w=150 150w, https://kchodorow.com/wp-content/uploads/2022/10/voronoi.png?w=300 300w" sizes="(max-width: 377px) 100vw, 377px" /></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2022/10/13/generating-voronoi-cells-in-python/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		
		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>

		<media:content url="https://kchodorow.com/wp-content/uploads/2022/10/voronoi.png?w=377" medium="image" />
	</item>
		<item>
		<title>How to set up Python on Compute Engine</title>
		<link>https://kchodorow.com/2022/09/21/how-to-set-up-python-on-compute-engine/</link>
					<comments>https://kchodorow.com/2022/09/21/how-to-set-up-python-on-compute-engine/#respond</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Wed, 21 Sep 2022 12:50:00 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=3914</guid>

					<description><![CDATA[This is a followup to my previous post on setting up big files on GCP. I ran into similar problems with Python as I did with static files, but my solution was a bit different. The right wayTM of running Python on GCP seems to be via a docker container. However, adding a virtual environment<a class="more-link" href="https://kchodorow.com/2022/09/21/how-to-set-up-python-on-compute-engine/">Continue reading <span class="screen-reader-text">"How to set up Python on Compute&#160;Engine"</span></a>]]></description>
										<content:encoded><![CDATA[
<p>This is a followup to my previous post on <a href="https://kchodorow.com/2022/09/19/how-to-get-big-files-into-compute-engine/">setting up big files on GCP</a>. I ran into similar problems with Python as I did with static files, but my solution was a bit different.</p>



<p>The right way<sup>TM</sup> of running Python on GCP seems to be via a docker container. However, adding a virtual environment to a docker container is painful: for anything more than a small number of dependencies and the docker image becomes too unwieldy to upload. Thus, I decided to keep my virtual environment on a separate disk in GCP and mount it as a volume on container startup. This keeps the Python image svelte and the virtual environment static, both good things! It does mean that they can get out of sync: technically I should probably be setting up some sort of continuous deployment. However, I don&#8217;t want spend the rest of my life setting up ops stuff, so let&#8217;s go with this for now.</p>



<p>To create a separate disk, follow the instructions in the last post for creating and attaching a disk to your GCP instance. Make sure you mark the disk read/write, since we&#8217;re going to install a bunch of packages.</p>



<p>Start up the instance and mount your disk (I&#8217;m calling mine <em>vqgan_models</em>, because sharing is caring).</p>



<p>On your development environment, scp your <em>requirements.txt</em> file over to GCP:</p>



<pre class="wp-block-code"><code>gcloud compute scp requirements.txt vqgan-clip:/mnt/disks/vqgan_models/python_env/requirements.txt</code></pre>



<p>Here&#8217;s where things get a little tricky, so here&#8217;s a high-level view of what we&#8217;re doing:</p>



<ol class="wp-block-list">
<li>Create a &#8220;scratch&#8221; Docker instance.</li>



<li>Add our persistent disk to the container in such a way that it mimics what out prod app will look like.</li>



<li>Install Python dependencies.</li>
</ol>



<p>Virtual environments are <em>not</em> relocatable, so we need to make the virtual environment directory match what prod will look like. For instance, I&#8217;ll be running my python app in /app with a virtual environment /app/.venv. Thus, I am going to mount my persistent disk to /app in the scratch docker container:</p>



<pre class="wp-block-code"><code>docker run -v /mnt/disks/vqgan_models/python_env:/app -it python:3.10-slim bash</code></pre>



<p>This will put you in a bash shell in a python environment container. Everything your create in /app will be saved to the persistent disk.</p>



<p><em>Note: when you want to leave, exit by hitting Ctrl-D! Typing &#8220;exit&#8221; seemed to cause changes in the volume not to actually be written to the persistent disk.</em></p>



<p>Now you can create a virtual environment that will match your production environment:</p>



<pre class="wp-block-code"><code># Shell starts in /
$ cd /app
$ python3 -m venv .venv
$ . .venv/bin/activate
$ pip install -r requirements.txt</code></pre>



<p>Hit Ctrl-D to exit the scratch docker instance. Shut down your instance so you can change your docker volumes. Go to Container -&gt; Change -&gt; Volume mounts and set the Mount path to <em>/app/.venv</em> and the Host path to <em>/mnt/disks/vqgan_models/python_env/.venv</em>.</p>



<p>On you development machine, set up a Dockerfile that copies your source code and then activates your virtual environment before starting your service:</p>



<pre class="wp-block-code"><code>FROM python:3.10-slim
WORKDIR /app
COPY mypkg ./mypkg
CMD . .venv/bin/activate &amp;&amp; python -m mypkg.my_service</code></pre>



<p>Build and push your image:</p>



<pre class="wp-block-code"><code>$ export BACKEND_IMAGE="${REGION}"-docker.pkg.dev/"${PROJECT_ID}"/"${BACKEND_ARTIFACT}"/my-python-app
$ docker build --platform linux/amd64 --tag "${BACKEND_IMAGE}" .
$ docker push "${BACKEND_IMAGE}"</code></pre>



<p>Now start up your GCP instance and make sure it&#8217;s running by checking the docker logs.</p>



<pre class="wp-block-code"><code>$ export CID=$(docker container ls | tail -n 1 | cut -f 1 -d' ')
$ docker logs $CID
...
I0917 01:24:51.654180 139988588971840 my_service.py:119] Ready to serve</code></pre>



<p>Now you can quickly upload new versions of code without hassling with giant Docker containers.</p>



<p><em>Note: I am a newbie at these technologies, so please let me know in the comments if there are better ways of doing this!</em></p>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2022/09/21/how-to-set-up-python-on-compute-engine/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		
		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>
	</item>
		<item>
		<title>How to get big files into Compute Engine</title>
		<link>https://kchodorow.com/2022/09/19/how-to-get-big-files-into-compute-engine/</link>
					<comments>https://kchodorow.com/2022/09/19/how-to-get-big-files-into-compute-engine/#comments</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Mon, 19 Sep 2022 11:55:00 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=3902</guid>

					<description><![CDATA[I&#8217;ve been working with some large models recently and, as a Docker beginner, shoved them all into my Docker image. This worked&#8230; sort of&#8230; until docker push started trying to upload 20GB of data. Google Cloud doesn&#8217;t seem to support service keys for docker auth (even though they claim to! not that I&#8217;m bitter), so<a class="more-link" href="https://kchodorow.com/2022/09/19/how-to-get-big-files-into-compute-engine/">Continue reading <span class="screen-reader-text">"How to get big files into Compute&#160;Engine"</span></a>]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve been working with some large models recently and, as a Docker beginner, shoved them all into my Docker image. This worked&#8230; sort of&#8230; until <code>docker push</code> started trying to upload 20GB of data. Google Cloud doesn&#8217;t seem to support service keys for docker auth (even though they claim to! not that I&#8217;m bitter), so I kept getting authorization errors. Time to figure out docker volumes.</p>



<p>First, I needed to create an additional disk. I essentially followed the directions in <a href="https://cloud.google.com/compute/docs/disks/add-persistent-disk">the docs</a>. Using the console in your compute engine instance, under &#8220;Additional Disks&#8221; select &#8220;Add new disk&#8221; and fill in the size you want. The defaults are probably fine, although it defaults to SSD so you can select Standard if don&#8217;t care about speed.</p>



<p>Save the instance and start it up. Hit the &#8220;SSH&#8221; button once it&#8217;s booted. Then, find your new disk:</p>



<pre class="wp-block-code"><code>$ sudo lsblk
...
sdb         8:16   0   20G  0 disk</code></pre>



<p>Then format the disk:</p>



<pre class="wp-block-code"><code>$ sudo mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb
$ sudo mkdir -p /mnt/disks/vqgan_models
$ sudo mount -o discard,defaults /dev/sdb /mnt/disks/vqgan_models</code></pre>



<p>I then ran a quick test to make sure it&#8217;s actually a writable directory:</p>



<pre class="wp-block-code"><code>$ cd /mnt/disks/vqgan_models/
$ echo "hello world" &gt; test.txt
$ cat test.txt
hello world</code></pre>



<p>Woot! Time to transfer some real data. Following <a href="https://cloud.google.com/compute/docs/instances/transfer-files">the docs</a>, I ran:</p>



<pre class="wp-block-code"><code>gcloud compute scp models/vqgan/model.ckpt vqgan-clip:/mnt/disks/vqgan_models</code></pre>



<p>After a long upload, I realized that I created the disk in the wrong data center. So if this happens to you: stop the VM, edit it to remove the disk (you have to detach the disk from the VM to modify its zone). Then move the disk:</p>



<pre class="wp-block-code"><code>gcloud compute disks move vqgan-models --zone=us-east1-b --destination-zone=us-central1-c</code></pre>



<p>&#8220;zone&#8221; is the source zone and &#8220;destination-zone&#8221; is, more obviously, the destination zone. This probably incurred some cross-data-center-networking cost, but life&#8217;s too short to wait for SCP.</p>



<p>Then I edited my us-central1-c instance to add an existing disk. Annoyingly, it isn&#8217;t mounted on startup. GCP claims that you can add it to your /etc/fstab, but that was destroyed every time I restarted the instance. Thus, I instead went to &#8220;Edit&#8221; -&gt; &#8220;Management&#8221; -&gt; &#8220;Metadata&#8221; -&gt; &#8220;Automation&#8221; -&gt; &#8220;Startup script&#8221; and added the lines:</p>



<pre class="wp-block-code"><code>sudo mkdir -p /mnt/disks/vqgan_models
sudo mount -o discard,defaults /dev/sdb /mnt/disks/vqgan_models</code></pre>



<p>I also managed to make my disk the wrong size. So, if you need to increase the size of your disk, run:</p>



<pre class="wp-block-code"><code>gcloud compute disks resize vqgan-models --size 40 --zone us-central1-c</code></pre>



<p>Then ext4 doesn&#8217;t know about the new, bigger size yet, so SSH into your VM and run:</p>



<pre class="wp-block-code"><code>sudo resize2fs /dev/sdb</code></pre>



<p>Now <code>df -h</code> should show &#8220;40G&#8221; as the size.</p>



<p>Now to actually mount this sucker as a docker volume. Shut the instance back down and go to &#8220;Edit&#8221;. Under &#8220;Container&#8221; select &#8220;Change&#8221; and select &#8220;Add Volume&#8221;. I want <em>/mnt/disks/vqgan_models/pretrained</em> to be mounted as <em>/app/pretrained</em> in the Docker container, so set &#8220;Mount path&#8221; to <em>/app/pretrained</em> and &#8220;Host path&#8221; to <em>/mnt/disks/vqgan_models/pretrained</em>.</p>



<p>Finally, it&#8217;s time to boot this up and try it out! Start the instance, hit the SSH button, find the docker container ID, and use that to check the filesystem in the container:</p>



<pre class="wp-block-code"><code>$ export CID=$(docker container ls | tail -n 1 | cut -f 1 -d' ')
$ docker exec $CID ls /app/pretrained
model.ckpt</code></pre>



<p>Now you can (fairly) easily move big files around and attach them to your docker instances.</p>



<p><em>Note: I am a newbie at all of these tech stacks. If anyone knows a better way to do this, I&#8217;d love to hear about it! Please let me know in the comments.</em></p>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2022/09/19/how-to-get-big-files-into-compute-engine/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		
		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>
	</item>
		<item>
		<title>Using Warp workflows to make the shell easier</title>
		<link>https://kchodorow.com/2022/05/15/using-warp-workflows-to-make-the-shell-easier/</link>
					<comments>https://kchodorow.com/2022/05/15/using-warp-workflows-to-make-the-shell-easier/#respond</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Sun, 15 May 2022 16:10:13 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=3870</guid>

					<description><![CDATA[Disclaimer: GV is an investor in Warp. Whenever I start a new Python project, I have to go look up the syntax for creating a virtual environment. Somehow it can never stick in my brain, but it seems too trivial to add a script for. I&#8217;ve been using Warp as my main shell for a<a class="more-link" href="https://kchodorow.com/2022/05/15/using-warp-workflows-to-make-the-shell-easier/">Continue reading <span class="screen-reader-text">"Using Warp workflows to make the shell&#160;easier"</span></a>]]></description>
										<content:encoded><![CDATA[
<p><em>Disclaimer: GV is an investor in Warp.</em></p>



<p>Whenever I start a new Python project, I have to go look up the syntax for creating a virtual environment. Somehow it can never stick in my brain, but it seems too trivial to add a script for. I&#8217;ve been using <a href="https://www.warp.dev/">Warp</a> as my main shell for a few months now and noticed they they have a feature called &#8220;workflows,&#8221; which seems to make it easy to add a searchable, documented command you frequently use right to the shell.</p>



<p>To add a workflow to the Warp shell, create a <em>~/.warp/workflows</em> directory and add a YAML file describing the workflow:</p>



<pre class="wp-block-code"><code>$ mkdir -p ~/.warp/workflows
$ emacs ~/.warp/workflows/venv.yaml</code></pre>



<p>Then I used one of the <a href="https://github.com/warpdotdev/Workflows/tree/main/specs/python">built-in workflows</a> as a template and modified it to create a virtual environment:</p>



<pre class="wp-block-code"><code>---
name: Create a virtual environment
command: "python3 -m venv {{directory}}"
tags: &#091;"python"]
description: Creates a virtual environment for the current directory.
arguments:
  - name: directory
    description: The directory to contain the virtual environment.
    default_value: .venv
source_url: "https://docs.python.org/3/library/venv.html"
author: kchodorow
author_url: "https://www.kchodorow.com"
shells: &#091;]</code></pre>



<p>I saved the file, typed Ctrl-Shift-R, and typed venv and my nice, documented workflow popped up:</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="672" data-attachment-id="3877" data-permalink="https://kchodorow.com/2022/05/15/using-warp-workflows-to-make-the-shell-easier/screen-shot-2022-05-15-at-11-35-54-am/" data-orig-file="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png" data-orig-size="1482,974" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="screen-shot-2022-05-15-at-11.35.54-am" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png?w=750" src="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png?w=1024" alt="" class="wp-image-3877" srcset="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png?w=1022 1022w, https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png?w=150 150w, https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png?w=300 300w, https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png?w=768 768w, https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png 1482w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>However, I&#8217;d really like this to handle creating or activating it, so I changed the command to:</p>



<pre class="wp-block-code"><code>command: "&#091; -d '{{directory}}' ] &amp;&amp; source '{{directory}}/bin/activate' || python3 -m venv {{directory}}"</code></pre>



<p>Which now yields:</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="303" data-attachment-id="3879" data-permalink="https://kchodorow.com/2022/05/15/using-warp-workflows-to-make-the-shell-easier/screen-shot-2022-05-15-at-11-57-52-am/" data-orig-file="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png" data-orig-size="2064,612" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="screen-shot-2022-05-15-at-11.57.52-am" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png?w=750" src="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png?w=1024" alt="" class="wp-image-3879" srcset="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png?w=1022 1022w, https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png?w=2044 2044w, https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png?w=150 150w, https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png?w=300 300w, https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png?w=768 768w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>So nice.</p>



<p><em>Update: I realized I actually <strong>always</strong> want to activate the virtual environment, but I also want to create it first if it doesn&#8217;t exist. So I updated the command to: <code>! [ -d '{{directory}}' ] &amp;&amp; python3 -m venv {{directory}}; source '{{directory}}/bin/activate'"</code>.</em> This creates the virtual environment if it doesn&#8217;t exist, and then activates it regardless.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2022/05/15/using-warp-workflows-to-make-the-shell-easier/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		
		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>

		<media:content url="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.35.54-am.png?w=1024" medium="image" />

		<media:content url="https://kchodorow.com/wp-content/uploads/2022/05/screen-shot-2022-05-15-at-11.57.52-am.png?w=1024" medium="image" />
	</item>
		<item>
		<title>Why market cap is dumb</title>
		<link>https://kchodorow.com/2022/04/27/why-market-cap-is-dumb/</link>
					<comments>https://kchodorow.com/2022/04/27/why-market-cap-is-dumb/#comments</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Wed, 27 Apr 2022 19:58:06 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=3865</guid>

					<description><![CDATA[When I was a kid, I went to a tag sale &#8220;for kids, by kids&#8221; where kids sold their junk/toys to other kids. I was wandering around and saw a shoebox filled to the brim with marbles. I went over and there was a sign on the box that said, &#8220;25 cents/marble&#8221;. &#8220;How much for<a class="more-link" href="https://kchodorow.com/2022/04/27/why-market-cap-is-dumb/">Continue reading <span class="screen-reader-text">"Why market cap is&#160;dumb"</span></a>]]></description>
										<content:encoded><![CDATA[
<p>When I was a kid, I went to a tag sale &#8220;for kids, by kids&#8221; where kids sold their junk/toys to other kids. I was wandering around and saw a shoebox filled to the brim with marbles. I went over and there was a sign on the box that said, &#8220;25 cents/marble&#8221;. </p>



<p>&#8220;How much for the whole box?&#8221; I asked the kid.</p>



<p>He thought for a second. He was around my age, maybe a little older. &#8220;$5,&#8221; he said.</p>



<p>&#8220;Sold!&#8221; I said quickly, handed him $5, and ran off with my hundreds and hundreds of marbles before he realized how deep a discount he had just given me.</p>



<p>Suppose there were 300 marbles in the box. The box &#8220;should have&#8221; cost 300*25 cents=$75. Obviously no one is going to pay $75 for a box of marbles, which brings us to the basic problem with market cap and the stock market.</p>



<p>The market cap of a company is basically the number of shares it has issued multiplied by price per share. However, if we think of a share as a marble, the market cap is that ridiculously inflated $75.  </p>



<p>How much of this stock are people actually trading? Google, for example, has 723,000,000,000(ish) shares outstanding. Daily trading volume is around 1,500,000. That is .0002% of the outstanding shares.  Translating that into marbles&#8230; that&#8217;s a lot less than one marble.</p>



<p>But let&#8217;s say a couple of people buy individual marbles, and then start trading them between themselves for 25 cents. Someone who hasn&#8217;t seen the kid&#8217;s booth offers a buyer 30 cents for a marble.  Doing some quick math, people realize that marble boy&#8217;s net worth has gone from $75 to $90. &#8220;Hey, that kid just made $15. We should tax him on that.&#8221;</p>



<p>And that&#8217;s why a wealth tax is stupid.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2022/04/27/why-market-cap-is-dumb/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		
		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>
	</item>
		<item>
		<title>Shoulders of Giants</title>
		<link>https://kchodorow.com/2022/02/01/shoulders-of-giants/</link>
					<comments>https://kchodorow.com/2022/02/01/shoulders-of-giants/#comments</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Tue, 01 Feb 2022 14:08:34 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=3856</guid>

					<description><![CDATA[I&#8217;ve been thinking a lot about construction. Taking a very specific part of the process, building the staircase: you find a carpenter and they build the staircase to your measurements. Generally your contractor will find someone with decent experience that they think will do a good job for whatever price you&#8217;re willing to pay and<a class="more-link" href="https://kchodorow.com/2022/02/01/shoulders-of-giants/">Continue reading <span class="screen-reader-text">"Shoulders of Giants"</span></a>]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve been thinking a lot about construction. Taking a very specific part of the process, building the staircase: you find a carpenter and they build the staircase to your measurements. Generally your contractor will find someone with decent experience that they think will do a good job for whatever price you&#8217;re willing to pay and then you get as staircase executed at whatever skill level happens to be available/at that price point.</p>



<p><a href="https://constructionphysics.substack.com/">Construction Physics</a> had an interesting point <a href="https://constructionphysics.substack.com/p/construction-ford-and-a-lever-to">the other day</a>: mass production took off in America because the United States didn&#8217;t have skilled craftsmen the way Europe did. This is also borne out by my Instagram feed, currently: European tradesmen seem to be more artistic and skilled than the Americans in my feed (sorry fellow countrymen). My guess is that Europe&#8217;s aristocracy supported spending 5000 man-hours on a staircase in a way that the United States really couldn&#8217;t compete with. And now maybe a continuing culture that values these skills more? I don&#8217;t really know.</p>



<p>Regardless, I was thinking about how different this is than software engineering. There&#8217;s always someone&#8217;s first staircase they&#8217;ve ever built, which is not going to be as good as the thousandth (I hope!). However, if there&#8217;s a common component in software engineering, someone will have already built it and it will be the product of many engineers&#8217; thousandth try at building user login, logging, whatever. Thus, a junior engineer can use these solid building blocks to create their own first-try-mess on top of. However, that mess will (hopefully!) have a solid foundation.</p>



<p>Open source and APIs are an incredible superpower software engineers have over the physical world. It&#8217;s like installing a staircase that was built by every master-craftsman over the last 500 years. And generally, the best tools are accessible to everyone: Fortune 500 companies can use Stripe/Twilio/Mercurial the same way an individual developer with a hobby project can. At least in the realm of software engineering, it is a golden age of equality.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2022/02/01/shoulders-of-giants/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		
		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>
	</item>
		<item>
		<title>5-minute design: meme generator</title>
		<link>https://kchodorow.com/2022/01/31/5-minute-design-meme-generator/</link>
					<comments>https://kchodorow.com/2022/01/31/5-minute-design-meme-generator/#respond</comments>
		
		<dc:creator><![CDATA[kchodorow]]></dc:creator>
		<pubDate>Mon, 31 Jan 2022 19:23:22 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://kchodorow.com/?p=3847</guid>

					<description><![CDATA[When I&#8217;ve talked to people who&#8217;ve attempted to make meme tools, they say that search is a really hard problem. This sort of makes sense: one person might search &#8220;communism&#8221;, another &#8220;bugs bunny&#8221;, and another &#8220;we&#8221; trying to get this image template: I was thinking about it today, though, and you know what? All of<a class="more-link" href="https://kchodorow.com/2022/01/31/5-minute-design-meme-generator/">Continue reading <span class="screen-reader-text">"5-minute design: meme&#160;generator"</span></a>]]></description>
										<content:encoded><![CDATA[
<p>When I&#8217;ve talked to people who&#8217;ve attempted to make meme tools, they say that search is a really hard problem. This sort of makes sense: one person might search &#8220;communism&#8221;, another &#8220;bugs bunny&#8221;, and another &#8220;we&#8221; trying to get this image template:</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="800" height="450" data-attachment-id="3851" data-permalink="https://kchodorow.com/communist_bugs_bunny_banner/" data-orig-file="https://kchodorow.com/wp-content/uploads/2022/01/communist_bugs_bunny_banner.jpeg" data-orig-size="800,450" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="communist_bugs_bunny_banner" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2022/01/communist_bugs_bunny_banner.jpeg?w=300" data-large-file="https://kchodorow.com/wp-content/uploads/2022/01/communist_bugs_bunny_banner.jpeg?w=750" src="https://kchodorow.com/wp-content/uploads/2022/01/communist_bugs_bunny_banner.jpeg?w=800" alt="" class="wp-image-3851" srcset="https://kchodorow.com/wp-content/uploads/2022/01/communist_bugs_bunny_banner.jpeg 800w, https://kchodorow.com/wp-content/uploads/2022/01/communist_bugs_bunny_banner.jpeg?w=150 150w, https://kchodorow.com/wp-content/uploads/2022/01/communist_bugs_bunny_banner.jpeg?w=300 300w, https://kchodorow.com/wp-content/uploads/2022/01/communist_bugs_bunny_banner.jpeg?w=768 768w" sizes="(max-width: 800px) 100vw, 800px" /><figcaption>See <a href="https://knowyourmeme.com/memes/communist-bugs-bunny">Know Your Meme</a> for context.</figcaption></figure>



<p>I was thinking about it today, though, and you know what? All of those people who&#8217;ve actually tried to build this are wrong. This is a super easy problem.</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="500" height="713" data-attachment-id="3853" data-permalink="https://kchodorow.com/skinner/" data-orig-file="https://kchodorow.com/wp-content/uploads/2022/01/skinner.jpeg" data-orig-size="500,713" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="skinner" data-image-description="" data-image-caption="" data-medium-file="https://kchodorow.com/wp-content/uploads/2022/01/skinner.jpeg?w=210" data-large-file="https://kchodorow.com/wp-content/uploads/2022/01/skinner.jpeg?w=500" src="https://kchodorow.com/wp-content/uploads/2022/01/skinner.jpeg?w=500" alt="" class="wp-image-3853" srcset="https://kchodorow.com/wp-content/uploads/2022/01/skinner.jpeg 500w, https://kchodorow.com/wp-content/uploads/2022/01/skinner.jpeg?w=105 105w, https://kchodorow.com/wp-content/uploads/2022/01/skinner.jpeg?w=210 210w" sizes="(max-width: 500px) 100vw, 500px" /></figure>



<p>How this should work:</p>



<ul class="wp-block-list"><li>A user searches for a meme and doesn&#8217;t find it. Keep a record of the query (say it&#8217;s &#8220;communism&#8221;).</li><li>The same user uploads a meme. Now there is a strong possibility that the query the user did is a good match for the uploaded image, so associate that image with &#8220;communism&#8221; and give that pair a score of 1.</li><li>Now diff that image to others in the database using image recognition to find &#8220;equivalent&#8221; images. For gifs, I&#8217;m not too familiar with gif&#8217;s file format, but I assume something could be done generating frames of image. (There are a lot of assumptions here.)</li><li>Now associate that query with all &#8220;equivalent&#8221; images, plus the new image. Then take all of the query terms associated with the existing images and add them to the new image.</li></ul>



<p>Next time someone searches for &#8220;communism&#8221;, show the meme template uploaded above. If they choose that template, increase the <code>(template, "communism")</code> pair score. Whenever someone searches, show them a mix of high-scoring templates for their search term, plus some prospective templates that are still &#8220;young.&#8221;</p>



<p>In the example above, I assume the user is trustworthy. There&#8217;s also a strong possibility that the user is a bot/malicious actor/both. So that users rep should be tied to whether others use that prospective template/query pair, and that feeds back into how much a user can affect a template&#8217;s score.</p>



<p>Since memes change over time, you probably also want to overlay some decay function, so if you search for &#8220;drake&#8221; you get the latest templates, not ones from years ago.</p>



<p>Now, assuming you have some users, you can set up a &#8220;self-labeling&#8221; system. </p>



<p>Easy peasy.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://kchodorow.com/2022/01/31/5-minute-design-meme-generator/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		
		<media:content url="https://0.gravatar.com/avatar/fc3fcc64636d8f4ecb7e40598ea3f8208d09babdd0a3773c3288a83e28f3476c?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">kchodorow</media:title>
		</media:content>

		<media:content url="https://kchodorow.com/wp-content/uploads/2022/01/communist_bugs_bunny_banner.jpeg?w=800" medium="image" />

		<media:content url="https://kchodorow.com/wp-content/uploads/2022/01/skinner.jpeg?w=500" medium="image" />
	</item>
	</channel>
</rss>
