<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/Content/RSS.xslt" type="text/xsl" media="screen"?>
<rss xmlns:dc="https://purl.org/dc/elements/1.1/" xmlns:atom="https://www.w3.org/2005/Atom" version="2.0">

    <channel>

        <title>Productive Rage</title>
        <link>https://www.productiverage.com/</link>
        <atom:link href="https://www.productiverage.com/feed" rel="self" type="application/rss+xml" />
        <description>Dan's techie ramblings</description>
        <language>en-gb</language>

        <lastBuildDate>Sun, 06 Apr 2025 19:27:00 GMT</lastBuildDate>
        <docs>https://blogs.law.harvard.edu/tech/rss</docs>

        <image>
            <title>Productive Rage</title>
            <url>https://www.productiverage.com/Content/Images/Grouch.jpg</url>
            <width>142</width>
            <height>142</height>
            <link>https://www.productiverage.com/</link>
        </image>

        <xhtml:meta xmlns:xhtml="https://www.w3.org/1999/xhtml" name="robots" content="noindex" />

            <item>
                <title>Hosting a DigitalOcean App Platform app on a custom subdomain (with CORS)</title>
                <link>https://www.productiverage.com/hosting-a-digitalocean-app-platform-app-on-a-custom-subdomain-with-cors</link>
                <guid>https://www.productiverage.com/hosting-a-digitalocean-app-platform-app-on-a-custom-subdomain-with-cors</guid>
                <description>&lt;h3 id=&quot;tldr&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/hosting-a-digitalocean-app-platform-app-on-a-custom-subdomain-with-cors#tldr&quot;&gt;TL;DR&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I host my blog using GitHub Pages (&lt;a href=&quot;https://github.com/ProductiveRage/productiverage.github.io&quot;&gt;repo here&lt;/a&gt;), and have the domain registered through GoDaddy. I wanted to experiment with adding some additional functionality to my static content, using DigitalOcean App Platform (where I can essentially throw a Docker container and have it appear on the internet).&lt;/p&gt;&#xA;&lt;p&gt;I wanted this DigitalOcean-hosted app to be available through a productiverage.com subdomain, and I wanted it to be accessible as an API from JavaScript on the page. SSL* has long been a given, and I hoped that I would hit few (if any) snags with that.&lt;/p&gt;&#xA;&lt;p&gt;There &lt;em&gt;are&lt;/em&gt; instructions out there for doing what I wanted, but I still encountered so many confusions and gotchas, that I figured I&#x27;d try to document the process (along with a few ways to reassure yourself when things look bleak).. even if it&#x27;s only for future-me!&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(Insert pedantic comment about how TLS has replaced SSL, and so we shouldn&#x27;t refer to &amp;quot;SSL&amp;quot; or &amp;quot;SSL certificates&amp;quot; - for the rest of the post, I&#x27;ll be saying &amp;quot;SSL&amp;quot; and hopefully that doesn&#x27;t upset anyone too much despite it not being technically correct!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;digitalocean-app-platform&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/hosting-a-digitalocean-app-platform-app-on-a-custom-subdomain-with-cors#digitalocean-app-platform&quot;&gt;DigitalOcean App Platform&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;So you have something deployed using DigitalOcean&#x27;s App Platform solution. It will have an automatically generated unique url that you can access it on, that is a subdomain of &amp;quot;ondigitalocean.app&amp;quot; (something like. &lt;a href=&quot;https://productiverage-search-58yr4.ondigitalocean.app&quot;&gt;https://productiverage-search-58yr4.ondigitalocean.app&lt;/a&gt;). This will not change (unless you delete your app), and you can always use it to test your application.&lt;/p&gt;&#xA;&lt;p&gt;You want to host the application on a subdomain of a domain that you own (hosted by GoDaddy, in my case).&lt;/p&gt;&#xA;&lt;p&gt;To start the process, go into the application&#x27;s details in DigitalOcean (the initial tab you should see if called &amp;quot;Overview&amp;quot;) and click into the &amp;quot;Settings&amp;quot; tab.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Note: Do &lt;em&gt;not&lt;/em&gt; click into the &amp;quot;Networking&amp;quot; section through the link in the left hand navigation bar (under &amp;quot;Manage), and then into &amp;quot;Domains&amp;quot;&lt;/strong&gt; (some guides that I found online suggested this, and it only resulted in me getting lost and confused - see the section below as to why).&lt;/p&gt;&#xA;&lt;p&gt;This tab has the heading &amp;quot;App Settings&amp;quot; and the second section should be &amp;quot;Domains&amp;quot;, click &amp;quot;Edit&amp;quot; and then the &amp;quot;&lt;strong&gt;&#x2B;Add Domain&lt;/strong&gt;&amp;quot; button.&lt;/p&gt;&#xA;&lt;p&gt;Here, enter the subdomain that you want to use for your application. Again, the auto-assigned ondigitalocean.app subdomain will never go away, and you can add &lt;em&gt;multiple&lt;/em&gt; custom domains if you want (though I only needed a single one).&lt;/p&gt;&#xA;&lt;p&gt;You don&#x27;t actually have to own the domain at this point; DigitalOcean won&#x27;t do any checks other than ensuring that you don&#x27;t enter a domain that is registered by something else within DigitalOcean (either one of your own resources, or a resource owned by another DigitalOcean customer). If you really wanted to, you could enter a subdomain of a domain that you know that you &lt;em&gt;can&#x27;t&lt;/em&gt; own, like &amp;quot;myawesomeexperiment.google.com&amp;quot; - but it wouldn&#x27;t make a lot of sense to do this, since you would never be able to connect that subdomain to your app!&lt;/p&gt;&#xA;&lt;p&gt;In my case, I wanted to use &amp;quot;search.productiverage.com&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; It&#x27;s &lt;em&gt;only&lt;/em&gt; the domain or subdomain that you have to enter here, &lt;em&gt;not&lt;/em&gt; the protocol (&amp;quot;http&amp;quot; or &amp;quot;https&amp;quot;) because (thankfully) it&#x27;s not really an option to operate without https these days. Back in the dim and distant past, SSL certificates were frustrating to purchase, and register, and renew - and they weren&#x27;t free! Today, life is a lot easier, and DigitalOcean handles it for you automatically when you use a custom subdomain on your application; they register the certificate, and automatically renew it. When you have everything working, you can look up the SSL certificate of the subdomain to confirm this - eg. when I use &lt;a href=&quot;https://www.sslshopper.com/ssl-checker.html#hostname=productiverage.com&quot;&gt;sslshopper.com to look up productiverage.com&lt;/a&gt; then I see that the details include &amp;quot;Server Type: GitHub.com&amp;quot; (same if I look up &amp;quot;www.productiverage.com&amp;quot;) because I have my domain configured to point at GitHub Pages, and they look after that SSL certificate. But if I use &lt;a href=&quot;https://www.sslshopper.com/ssl-checker.html#hostname=search.productiverage.com&quot;&gt;sslshopper.com to look up search.productiverage.com&lt;/a&gt; then I see &amp;quot;Server Type: cloudflare&amp;quot; (although it doesn&#x27;t mention DigitalOcean, it&#x27;s clearly a different certificate).&lt;/p&gt;&#xA;&lt;p&gt;With your sub/domain entered (and with DigitalOcean having checked that it&#x27;s of a valid form, and not already in use by another resource), you will be asked to select some DNS management options. Click &amp;quot;You manage your domain&amp;quot; and then the &amp;quot;Add Domain&amp;quot; button at the bottom of the page.&lt;/p&gt;&#xA;&lt;p&gt;This will redeploy your app. After which, you should see the new domain listed in the table that opened after clicked &amp;quot;Edit&amp;quot; alongside &amp;quot;Domains&amp;quot; in the &amp;quot;Settings&amp;quot; tab of your app. It will &lt;em&gt;probably&lt;/em&gt; show the status as &amp;quot;Pending&amp;quot;. It &lt;em&gt;might&lt;/em&gt; show the status as &amp;quot;Configuring&amp;quot; at this point - if it doesn&#x27;t, then refreshing the page and clicking &amp;quot;Edit&amp;quot; again alongside the &amp;quot;Domains&amp;quot; section should result in it now showing &amp;quot;Configuring&amp;quot;.#&lt;/p&gt;&#xA;&lt;p&gt;There will be a &amp;quot;?&amp;quot; icon alongside the &amp;quot;Configuring&amp;quot; status - if you hover over it you will see the message &amp;quot;&lt;strong&gt;Your domain is not yet active because the CNAME record was not found&lt;/strong&gt;&amp;quot;. Once we do some work on the domain registrar side (eg. GoDaddy), this status will change!&lt;/p&gt;&#xA;&lt;h3 id=&quot;digitalocean-app-platform-avoiding-networking-domains&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/hosting-a-digitalocean-app-platform-app-on-a-custom-subdomain-with-cors#digitalocean-app-platform-avoiding-networking-domains&quot;&gt;DigitalOcean App Platform - Avoiding &amp;quot;Networking&amp;quot; / &amp;quot;Domains&amp;quot;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I read some explanations of this process that said that you should configure your custom domain &lt;em&gt;not&lt;/em&gt; by starting with the app settings, but by clicking the &amp;quot;Networking&amp;quot; link in the left hand nav (under &amp;quot;Manage&amp;quot;) and then clicking into &amp;quot;Domains&amp;quot;. I spent an embarrassing amount of time going down this route, and getting frustrated when I reached a step that would say something like &amp;quot;using the dropdown in the &#x27;Directs to&#x27; column, select where the custom domain should be used&amp;quot; - I never had a dropdown, and couldn&#x27;t find an explanation why!&lt;/p&gt;&#xA;&lt;p&gt;When you configure a custom sub/domain this way, it can only be connected to (iirc) Load Balancers (which &amp;quot;let you distribute traffic between multiple Droplets either regionally or globally&amp;quot;) or, &lt;em&gt;I think,&lt;/em&gt; Reserved IPs (which you can associate with any individual Droplet, or with a DigitalOcean&#x27;s managed Kubernetes service - referred to as &amp;quot;DOKS&amp;quot;). &lt;strong&gt;You can not select an App Platform instance in a &#x27;Directs To&#x27; dropdown in the &amp;quot;Networking&amp;quot; / &amp;quot;Domains&amp;quot; section&lt;/strong&gt;, and that is what was causing me to stumble since I only have my single App Platform instance right now (I don&#x27;t have a load balancer or any other, more complicated infrastructure).&lt;/p&gt;&#xA;&lt;p&gt;Final note on this; if you configure a custom domain as I&#x27;m describing, you won&#x27;t see that custom domain shown in the &amp;quot;Networking&amp;quot; / &amp;quot;Domains&amp;quot; list. That is nothing to worry about - everything will still work!&lt;/p&gt;&#xA;&lt;h3 id=&quot;my-use-of-godaddy-in-short-i-configure-dns-to-serve-github-pages-content&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/hosting-a-digitalocean-app-platform-app-on-a-custom-subdomain-with-cors#my-use-of-godaddy-in-short-i-configure-dns-to-serve-github-pages-content&quot;&gt;My use of GoDaddy (in short; I configure DNS to serve GitHub Pages content)&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Long ago, I registered my domain with GoDaddy and hosted my blog with them as an ASP.NET site. I wasn&#x27;t happy with the performance of it - it was fast much of the time, but would intermittently serve requests very slowly. I had a friend who had purchased a load of hosting capacity somewhere, so I shifted my site over to that (where it was still hosted as an ASP.NET site) and configured GoDaddy to send requests that way.&lt;/p&gt;&#xA;&lt;p&gt;Back in 2016, I shifted over to serving the blog through GitHub Pages as static content. The biggest stumbling block to this would have been the site search functionality, which I had written for my ASP.NET app in C# - but &lt;a href=&quot;https://www.productiverage.com/the-neocities-challenge-aka-the-full-text-indexer-goes-clientside&quot;&gt;I had put together a way to push that all to JS in the client&lt;/a&gt; in 2013 when I got excited about &lt;a href=&quot;https://neocities.org/&quot;&gt;Neocities&lt;/a&gt; being released (I&#x27;m of an age where I remember the often-hideous, but easy-to-build-and-experiment-with, Geocities pages.. back before the default approaches to publishing content seemed to within walled gardens or on pay-to-access platforms).&lt;/p&gt;&#xA;&lt;p&gt;As my blog is on GitHub Page, I have &lt;code&gt;A&lt;/code&gt; records configured in the DNS settings for my domain within GoDaddy that point to GitHub servers, and a &lt;code&gt;CNAME&lt;/code&gt; record that points &amp;quot;www&amp;quot; to my GitHub subdomain &amp;quot;productiverage.github.io&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;The GitHub documentation page &amp;quot;&lt;a href=&quot;https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site&quot;&gt;Managing a custom domain for your GitHub Pages site&lt;/a&gt;&amp;quot; describes the steps that I followed to end up in this position - see the section &amp;quot;&lt;strong&gt;Configuring an apex domain and the www subdomain variant&lt;/strong&gt;&amp;quot;. The redirect from &amp;quot;productiverage.com&amp;quot; to &amp;quot;www.productiverage.com&amp;quot; is managed by GitHub, as is the SSL certificate, &lt;em&gt;and&lt;/em&gt; the redirection from &amp;quot;http&amp;quot; to &amp;quot;https&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;Until I created my DigitalOcean app, GoDaddy&#x27;s only role was to ensure that when someone tried to visit my blog that the DNS lookup resulted in them going to GitHub, who would pick up the request and serve my content.&lt;/p&gt;&#xA;&lt;h3 id=&quot;configuring-the-subdomain-for-digitalocean-in-godaddy&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/hosting-a-digitalocean-app-platform-app-on-a-custom-subdomain-with-cors#configuring-the-subdomain-for-digitalocean-in-godaddy&quot;&gt;Configuring the subdomain for DigitalOcean in GoDaddy&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Within the GoDaddy &amp;quot;cPanel&amp;quot; (ie. their control panel), click into your domain, then into the &amp;quot;DNS&amp;quot; tab, and then click the &amp;quot;Add New Record&amp;quot; button. Select &lt;code&gt;CNAME&lt;/code&gt; in the &amp;quot;Type&amp;quot; dropdown, type the subdomain segment into the &amp;quot;Name&amp;quot; text (in my case, I want DigitalOcean to use the subdomain &amp;quot;search.productiverage.com&amp;quot; so I entered &amp;quot;search&amp;quot; into that textbox, since I was managing my domain &amp;quot;productiverage.com&amp;quot;), paste the DigitalOcean-generated domain into the &amp;quot;Value&amp;quot; textbox (&amp;quot;productiverage-search-58yr4.ondigitalocean.app&amp;quot; for my app), and click &amp;quot;Save&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;You should see a message informing you that DNS changes may take up to 48 hours to propagate, but that it usually all happens in less than an hour.&lt;/p&gt;&#xA;&lt;p&gt;In my experience, it often only takes a few minutes for everything to work.&lt;/p&gt;&#xA;&lt;p&gt;If you want to get an idea about how things are progressing, there are a couple of things you can do -&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;If you open a command prompt and ping the DigitalOcean-generated subdomain (eg. &amp;quot;productiverage-search-58yr4.ondigitalocean.app&amp;quot;) and then ping your new subdomain (eg. &amp;quot;search.productiverage.com&amp;quot;) they should resolve to the same IP address&lt;/li&gt;&#xA;&lt;li&gt;With the IP address resolving correctly, you can try visiting the subdomain in a browser - if you get an error message like &amp;quot;Can&#x27;t Establish a Secure Connection&amp;quot; then DigitalOcean hasn&#x27;t finished configuring the SSL certificate, but this error is still an indicator that the DNS change has been applied (which is good news!)&lt;/li&gt;&#xA;&lt;li&gt;If you go back to your app in the DigitalOcean control panel, and refresh the &amp;quot;Settings&amp;quot; tab, and click &amp;quot;Edit&amp;quot; alongside the &amp;quot;Domains&amp;quot; section, the status will have changed from &amp;quot;Configuring&amp;quot; to &amp;quot;Active&amp;quot; when it&#x27;s ready (you may have to refresh a couple of times, depending upon how patient you&#x27;re being, how slow the internet is being, and whether DigitalOcean&#x27;s UI automatically updates itself or not)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;If you don&#x27;t want to mess about with these steps, you are free to go and make a cup of tea, and everything should sort itself out on its own!&lt;/p&gt;&#xA;&lt;p&gt;I had gone round and round so many times trying to make it work that I was desperate to have some additional insight into whether it was working or not, but now that I&#x27;m confident in the process I would probably just wait five minutes if I did this again, and jump straight to the final step..&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;At this point, you should be able to hit your DigitalOcean app in the browser!&lt;/strong&gt; Hurrah!&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;If it fails, then it&#x27;s worth checking that the app is still running and working when you access it via the DigitalOcean-generated address&lt;/li&gt;&#xA;&lt;li&gt;If the app works at the DigitalOcean-generated address but &lt;em&gt;still&lt;/em&gt; doesn&#x27;t work on your custom subdomain, hopefully running again through those three steps above will help you identify where the blocker is, or maybe you&#x27;ll find clues in the app logs in DigitalOcean&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&quot;bonus-material-enabling-cors-access-for-the-app-in-digitalocean&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/hosting-a-digitalocean-app-platform-app-on-a-custom-subdomain-with-cors#bonus-material-enabling-cors-access-for-the-app-in-digitalocean&quot;&gt;&lt;strong&gt;Bonus material:&lt;/strong&gt; Enabling CORS access for the app (in DigitalOcean)&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Depending upon your needs, you may be done by this point.&lt;/p&gt;&#xA;&lt;p&gt;After I&#x27;d finished whooping triumphantly, however, I realised that &lt;em&gt;I&lt;/em&gt; wasn&#x27;t done..&lt;/p&gt;&#xA;&lt;p&gt;My app exposes a html form that will perform a semantic search across my blog content (it&#x27;s essentially my blog&#x27;s &lt;a href=&quot;https://github.com/ProductiveRage/Blog/tree/master/SemanticSearchDemo&quot;&gt;Semantic Search Demo&lt;/a&gt; project, except that - depending upon when you read this post and when I update that code - it uses a smaller embedding model and it adds a call to a Cohere Reranker to better remove poor matches from the result set). That html form works fine in isolation.&lt;/p&gt;&#xA;&lt;p&gt;However, the app also supports &lt;code&gt;application/json&lt;/code&gt; requests, because I wanted to improve my blog&#x27;s search by incorporating semantic search results into my existing lexical search. This meant that I would be calling the app from JS on my blog. And &lt;em&gt;that&lt;/em&gt; would be a problem, because trying to call &lt;a href=&quot;https://search.productiverage.com&quot;&gt;https://search.productiverage.com&lt;/a&gt; from JS code executed within the context of &lt;a href=&quot;https://www.productiverage.com&quot;&gt;https://www.productiverage.com&lt;/a&gt; would be rejected due to the &lt;strong&gt;&amp;quot;Cross-Origin Resource Sharing&amp;quot; (CORS) mechanism, which exists for security purposes - essentially, to ensure that potentially-malicious JS can&#x27;t send content from a site to another domain&lt;/strong&gt; (even if the sites are on subdomains of the same domain).&lt;/p&gt;&#xA;&lt;p&gt;To make a request through JS within the context of one domain (eg. &amp;quot;www.productiverage.com&amp;quot;) to another (eg. &amp;quot;search.productiverage.com&amp;quot;), the second domain must be explicitly configured to allow access from the first. This configuration is done against the DigitalOcean app -&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;In the DigitalOcean control panel, navigate back to the &amp;quot;Settings&amp;quot; tab for your app&lt;/li&gt;&#xA;&lt;li&gt;The first line (under the tab navigation and above the title &amp;quot;App Settings&amp;quot;) should display &amp;quot;App&amp;quot; on the left and &amp;quot;Components&amp;quot; on the right - &lt;strong&gt;you need to click into the component&lt;/strong&gt; (I only have a single component in my case)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/DigitalOceanComponentSelection.png&quot; alt=&quot;DigitalOcean &#x27;component&#x27; selection within an app&quot; title=&quot;DigitalOcean &#x27;component&#x27; selection within an app&quot;&gt;&lt;/p&gt;&#xA;&lt;ol start=&quot;3&quot;&gt;&#xA;&lt;li&gt;Click &amp;quot;Edit&amp;quot; in the &amp;quot;HTTP Request Routes&amp;quot; section and click &amp;quot;Configure CORS&amp;quot; by the route that you will need to request from another domain (again, I only have a single route, which is for the root of my application)&lt;/li&gt;&#xA;&lt;li&gt;I want to provide access to my app &lt;em&gt;only&lt;/em&gt; from my blog, so I set a value for the &lt;code&gt;Access-Control-Allow-Origins&lt;/code&gt; header, that has a &amp;quot;Match Type&amp;quot; of &amp;quot;Exact&amp;quot; and an &amp;quot;Origin&amp;quot; of &amp;quot;https://www.productiverage.com&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;Click &amp;quot;Apply CORS&amp;quot; - and you should be done!&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Now, you should be able to access your DigitalOcean app on the custom subdomain from &lt;em&gt;another&lt;/em&gt; domain through JS code, without the browser giving you an error about CORS restrictions denying your attempt!&lt;/p&gt;&#xA;&lt;p&gt;To see an example of this in action, you can go to &lt;a href=&quot;https://www.productiverage.com/&quot;&gt;www.productiverage.com&lt;/a&gt;, open the dev tools in your browser, go to the &amp;quot;Network&amp;quot; tab and filter requests to &amp;quot;Fetch/XHR&amp;quot;, type something into the &amp;quot;Site Search&amp;quot; text box on the site and click &amp;quot;Search&amp;quot;, and you &lt;em&gt;should&lt;/em&gt; see requests for content &lt;code&gt;SearchIndex-{something}.lz.txt&lt;/code&gt; (which is used for lexical searching) &lt;em&gt;and&lt;/em&gt; a single request that looks like &lt;code&gt;?q={what you searched for}&lt;/code&gt; which (if you view the Headers for) you should see comes from &lt;a href=&quot;https://search.productiverage.com/&quot;&gt;search.productiverage.com&lt;/a&gt;. Woo, success!!&lt;/p&gt;&#xA;</description>
                <pubDate>Sun, 06 Apr 2025 19:27:00 GMT</pubDate>
            </item>
            <item>
                <title>(Approximately) correcting perspective with C# (fixing a blurry presentation video - part two)</title>
                <link>https://www.productiverage.com/approximately-correcting-perspective-with-c-sharp-fixing-a-blurry-presentation-video-part-two</link>
                <guid>https://www.productiverage.com/approximately-correcting-perspective-with-c-sharp-fixing-a-blurry-presentation-video-part-two</guid>
                <description>&lt;h3 id=&quot;tldr&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/approximately-correcting-perspective-with-c-sharp-fixing-a-blurry-presentation-video-part-two#tldr&quot;&gt;TL;DR&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I have a video of a presentation where the camera keeps losing focus such that the slides are unreadable. I have the original slide deck and I want to fix this.&lt;/p&gt;&#xA;&lt;p&gt;Step one was &lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one&quot;&gt;identifying the area in each frame that it seemed likely was where the slides were being projected&lt;/a&gt;, now step two is to correct the perspective of the projection back into a rectangle to make it easier to perform comparisons against the original slide deck images and try to determine which slide was being projected.&lt;/p&gt;&#xA;&lt;p&gt;(&lt;strong&gt;An experimental TL;DR approach:&lt;/strong&gt; See this &lt;a href=&quot;https://dotnetfiddle.net/pEzbHD&quot;&gt;small scale .NET Fiddle demonstration&lt;/a&gt; of what I&#x27;ll be discussing)&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/PerspectiveCorrectedSlide.jpg&quot; alt=&quot;A slide extracted from a frame of a video presentation and &#x27;perspective-corrected&#x27; back into a rectangle&quot; title=&quot;A slide extracted from a frame of a video presentation and &#x27;perspective-corrected&#x27; back into a rectangle&quot;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;the-basic-approach&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/approximately-correcting-perspective-with-c-sharp-fixing-a-blurry-presentation-video-part-two#the-basic-approach&quot;&gt;The basic approach&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;An overview of the processing to do this looks as follows:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Load the original slide image into a &lt;code&gt;Bitmap&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;Using the projected-slide-area region calculated in step one..&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Take the line from the top left of the region to the top right&lt;/li&gt;&#xA;&lt;li&gt;Take the line from the bottom left of the region to the bottom right (note that this line may be a little longer or shorter than the first line)&lt;/li&gt;&#xA;&lt;li&gt;Create vertical slices of the image by stepping through the first line (the one across the top), connecting each pixel to a pixel on the bottom line&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;These vertical slices will not all be the same height and so they&#x27;ll need to be adjusted to a consistent size (the further from the camera that a vertical slice of the projection is, the smaller it will be)&lt;/li&gt;&#xA;&lt;li&gt;The height-adjusted vertical slices are then combined into a single rectangle, which will result in an approximation of a perspective-corrected version of the projection of the slide&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The reason that this process is only going to be an approximation is due to the way that the height of the output image will be determined -&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;For my purposes, it will be fine to use the largest of the top-left-to-bottom-left length (ie. the left-hand edge of the projection) and the top-right-to-bottom-right length (the right-hand edge of the projected) but this will always result in an output whose aspect ratio is stretched vertically slightly because the largest of those two lengths will be &amp;quot;magnified&amp;quot; somewhat due to the perspective effect&lt;/li&gt;&#xA;&lt;li&gt;What might seem like an obvious improvement would be to take an average of the left-hand-edge-height and the right-hand-edge-height but I decided not to do this because I would be losing some fidelity from the vertical slices that would be shrunken down to match this average &lt;em&gt;and&lt;/em&gt; because this would &lt;em&gt;still&lt;/em&gt; be an approximation as..&lt;/li&gt;&#xA;&lt;li&gt;The correct way to determine the appropriate aspect ratio for the perspective-corrected image is to use some clever maths to try to determine that angle of the wall that the projection is on (look up perspective correction and vanishing points if you&#x27;re really curious!) and to use &lt;em&gt;that&lt;/em&gt; to decide what ratio of the left-hand-edge-height and the right-hand-edge-height to use&#xA;&lt;ul&gt;&#xA;&lt;li&gt;(The reason that the take-an-average approach is still an approximation is that perspective makes the larger edge grow more quickly than the smaller edge shrinks, so this calculation would still skew towards a vertically-stretched image)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 id=&quot;slice-dice&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/approximately-correcting-perspective-with-c-sharp-fixing-a-blurry-presentation-video-part-two#slice-dice&quot;&gt;Slice &amp;amp; dice!&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;So if we follow the plan above then we&#x27;ll generate a list of vertical slices a bit like this:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/PerspectiveCorrectionSlices.jpg&quot; alt=&quot;An illustration of how the vertical slices are taken from a projected image&quot; title=&quot;An illustration of how the vertical slices are taken from a projected image&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;.. which, when combined would look like this:&lt;/p&gt;&#xA;&lt;img class=&quot;AlwaysFullWidth&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PerspectiveCorrectionSlicesOriginalHeights.jpg&quot; alt=&quot;The vertical slices of the projected image before their heights are normalised&quot; title=&quot;The vertical slices of the projected image before their heights are normalised&quot;&gt;&#xA;&lt;p&gt;This is very similar to the original projection except that:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;The top edge is now across the top of the rectangular area&lt;/li&gt;&#xA;&lt;li&gt;The bottom left corner is aligned with the left-hand side of the image&lt;/li&gt;&#xA;&lt;li&gt;The bottom right corner is aligned with the right-hand side of the image&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;We&#x27;re not done yet but this has brought things much closer!&lt;/p&gt;&#xA;&lt;p&gt;In fact, all that is needed is to stretch those vertical slices so that they are all the same length and; ta-da!&lt;/p&gt;&#xA;&lt;img class=&quot;AlwaysFullWidth&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PerspectiveCorrectionFinal.jpg&quot; alt=&quot;The projected image contorted back into a rectangle&quot; title=&quot;The projected image contorted back into a rectangle&quot;&gt;&#xA;&lt;h3 id=&quot;implementation-for-slicing-and-stretching&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/approximately-correcting-perspective-with-c-sharp-fixing-a-blurry-presentation-video-part-two#implementation-for-slicing-and-stretching&quot;&gt;Implementation for slicing and stretching&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;So, from &lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one&quot;&gt;previous analysis&lt;/a&gt;, I know that the bounding area for the projection of the slide in the frames of my video is:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;topLeft: (1224, 197)&#xA;topRight: (1915, 72)&#xA;&#xA;bottomLeft: (1229, 638)&#xA;bottomRight: (1915, 662)&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Since I&#x27;m going to walk along the top edge and create vertical slices from that, I&#x27;m going to need the length of that edge - which is easy enough with some Pythagoras:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static int LengthOfLine((PointF From, PointF To) line)&#xA;{&#xA;    var deltaX = line.To.X - line.From.X;&#xA;    var deltaY = line.To.Y - line.From.Y;&#xA;    return (int)Math.Round(Math.Sqrt((deltaX * deltaX) &#x2B; (deltaY * deltaY)));&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;So although it&#x27;s only 691px horizontally from the top left to the top right (1915 - 1224), the actual length of that edge is 702px (because it&#x27;s not a line that angles up slightly rather than being a flat horizontal one).&lt;/p&gt;&#xA;&lt;p&gt;This edge length determines how many vertical slices that we&#x27;ll take and we&#x27;ll get them by looping across this top edge, working out where the corresponding point on the bottom edge should be and joining them together into a line; one vertical slice. Each time that the loop increments, the current point on the top edge is going to move slightly to the right and even more slightly upwards while each corresponding point on the bottom edge will also move slightly to the right but it will move slightly &lt;em&gt;down&lt;/em&gt; as the projection on the wall gets closer and closer to the camera.&lt;/p&gt;&#xA;&lt;p&gt;One way to get all of these vertical slice lines is a method such as the following:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private sealed record ProjectionDetails(&#xA;    Size ProjectionSize,&#xA;    IEnumerable&amp;lt;((PointF From, PointF To) Line, int Index)&amp;gt; VerticalSlices&#xA;);&#xA;&#xA;private static ProjectionDetails GetProjectionDetails(&#xA;    Point topLeft,&#xA;    Point topRight,&#xA;    Point bottomRight,&#xA;    Point bottomLeft)&#xA;{&#xA;    var topEdge = (From: topLeft, To: topRight);&#xA;    var bottomEdge = (From: bottomLeft, To: bottomRight);&#xA;    var lengthOfEdgeToStartFrom = LengthOfLine(topEdge);&#xA;    var dimensions = new Size(&#xA;        width: lengthOfEdgeToStartFrom,&#xA;        height: Math.Max(&#xA;            LengthOfLine((topLeft, bottomLeft)),&#xA;            LengthOfLine((topRight, bottomRight))&#xA;        )&#xA;    );&#xA;    return new ProjectionDetails(dimensions, GetVerticalSlices());&#xA;&#xA;    IEnumerable&amp;lt;((PointF From, PointF To) Line, int Index)&amp;gt; GetVerticalSlices() =&amp;gt;&#xA;        Enumerable&#xA;            .Range(0, lengthOfEdgeToStartFrom)&#xA;            .Select(i =&amp;gt;&#xA;            {&#xA;                var fractionOfProgressAlongPrimaryEdge = (float)i / lengthOfEdgeToStartFrom;&#xA;                return (&#xA;                    Line: (&#xA;                        GetPointAlongLine(topEdge, fractionOfProgressAlongPrimaryEdge),&#xA;                        GetPointAlongLine(bottomEdge, fractionOfProgressAlongPrimaryEdge)&#xA;                    ),&#xA;                    Index: i&#xA;                );&#xA;            });&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This returns the dimensions of the final perspective-corrected projection (which is as wide as the top edge is long and which is as high as the greater of the left-hand edge&#x27;s length and the right-hand edge&#x27;s length) as well as an &lt;code&gt;IEnumerable&lt;/code&gt; of the start and end points for each slice that we&#x27;ll be taking.&lt;/p&gt;&#xA;&lt;p&gt;The dimensions are going to allow us to create a bitmap that we&#x27;ll paste the slices into when we&#x27;re ready - but, before that, we need to determine pixel values for every point on every vertical slice. As the horizontal distance across the top edge is 691px and the vertical distance is 125px but its actual length is 702px, each time we move one along in that 702px loop the starting point for the vertical slice will move (691 / 702) = 0.98px across and (125 / 702) = 0.18px up. So almost all of these vertical slices are going to have start and end points that are not whole pixel values - and the same applies to each point &lt;em&gt;on&lt;/em&gt; that vertical slice. This means that we&#x27;re going to have to take average colour values for when we&#x27;re dealing with fractional pixel locations.&lt;/p&gt;&#xA;&lt;p&gt;For example, if we&#x27;re at the point (1309.5, 381.5) and the colours at (1309, 381), (1310, 381), (1309, 382), (1310, 382) are all white then the averaging is really easy - the &amp;quot;averaged&amp;quot; colour is white! If we&#x27;re at the point (1446.5, 431.5) and the colours at (1446, 431), (1447, 431), (1446, 432), (1447, 432) are #BCA6A9, #B1989C, #BCA6A9, #B1989C then it&#x27;s also not too complicated - because (1446.5, 431.5) is at the precise midpoint between all four points then we can take a really simple average by adding all four R values together, all four G values together, all four B values together and diving them by 4 to get a combined result. It gets a little more complicated where it&#x27;s not 0.5 of a pixel and it&#x27;s slightly more to the left or to the right and/or to the top or to the bottom - eg. (1446.1, 431.9) would get more of its averaged colour from the pixels on the left than on the right (as 1446.1 is only just past 1446) while it would get more of its averaged colour from the pixels on the bottom than the top (as 431.9 is practically ay 432). On the other hand, on the rare occasion where it &lt;em&gt;is&lt;/em&gt; a precise location (with no fractional pixel values), such as (1826, 258), then it&#x27;s the absolute simplest case because no averaging is required!&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static Color GetAverageColour(Bitmap image, PointF point)&#xA;{&#xA;    var (integralX, fractionalX) = GetIntegralAndFractional(point.X);&#xA;    var x0 = integralX;&#xA;    var x1 = Math.Min(integralX &#x2B; 1, image.Width);&#xA;&#xA;    var (integralY, fractionalY) = GetIntegralAndFractional(point.Y);&#xA;    var y0 = integralY;&#xA;    var y1 = Math.Min(integralY &#x2B; 1, image.Height);&#xA;&#xA;    var (topColour0, topColour1) = GetColours(new Point(x0, y0), new Point(x1, y0));&#xA;    var (bottomColour0, bottomColour1) = GetColours(new Point(x0, y1), new Point(x1, y1));&#xA;&#xA;    return CombineColours(&#xA;        CombineColours(topColour0, topColour1, fractionalX),&#xA;        CombineColours(bottomColour0, bottomColour1, fractionalX),&#xA;        fractionalY&#xA;    );&#xA;&#xA;    (Color c0, Color c1) GetColours(Point p0, Point p1)&#xA;    {&#xA;        var c0 = image.GetPixel(p0.X, p0.Y);&#xA;        var c1 = (p0 == p1) ? c0 : image.GetPixel(p1.X, p1.Y);&#xA;        return (c0, c1);&#xA;    }&#xA;&#xA;    static (int Integral, float Fractional) GetIntegralAndFractional(float value)&#xA;    {&#xA;        var integral = (int)Math.Truncate(value);&#xA;        var fractional = value - integral;&#xA;        return (integral, fractional);&#xA;    }&#xA;&#xA;    static Color CombineColours(Color x, Color y, float proportionOfSecondColour)&#xA;    {&#xA;        if ((proportionOfSecondColour == 0) || (x == y))&#xA;            return x;&#xA;&#xA;        if (proportionOfSecondColour == 1)&#xA;            return y;&#xA;&#xA;        return Color.FromArgb(&#xA;            red: CombineComponent(x.R, y.R),&#xA;            green: CombineComponent(x.G, y.G),&#xA;            blue: CombineComponent(x.B, y.B),&#xA;            alpha: CombineComponent(x.A, y.A)&#xA;        );&#xA;&#xA;        int CombineComponent(int x, int y) =&amp;gt;&#xA;            Math.Min(&#xA;                (int)Math.Round((x * (1 - proportionOfSecondColour)) &#x2B; (y * proportionOfSecondColour)),&#xA;                255&#xA;            );&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This gives us the capability to split the wonky projection into vertical slices, to loop over each slice and to walk &lt;em&gt;down&lt;/em&gt; each slice and get a list of pixel values for each point down that slice. The final piece of the puzzle is that we then need to resize each vertical slice so that they all match the projection height returned from the &lt;code&gt;GetProjectionDetails&lt;/code&gt; method earlier. Handily, the .NET &lt;code&gt;Bitmap&lt;/code&gt; drawing code has &lt;code&gt;DrawImage&lt;/code&gt; functionality that can resize content, so we can:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Create a &lt;code&gt;Bitmap&lt;/code&gt; whose dimensions are those returned from &lt;code&gt;GetProjectionDetails&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;Loop over each vertical slice (which is an &lt;code&gt;IEnumerable&lt;/code&gt; also returned from &lt;code&gt;GetProjectionDetails&lt;/code&gt;)&lt;/li&gt;&#xA;&lt;li&gt;Create a bitmap just for that slice - that is 1px wide and only as tall as the current vertical slice is long&lt;/li&gt;&#xA;&lt;li&gt;Use &lt;code&gt;DrawImage&lt;/code&gt; to paste that slice&#x27;s bitmap onto the full-size projection &lt;code&gt;Bitmap&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;In code:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static void RenderSlice(&#xA;    Bitmap projectionBitmap,&#xA;    IEnumerable&amp;lt;Color&amp;gt; pixelsOnLine,&#xA;    int index)&#xA;{&#xA;    var pixelsOnLineArray = pixelsOnLine.ToArray();&#xA;&#xA;    using var slice = new Bitmap(1, pixelsOnLineArray.Length);&#xA;    for (var j = 0; j &amp;lt; pixelsOnLineArray.Length; j&#x2B;&#x2B;)&#xA;        slice.SetPixel(0, j, pixelsOnLineArray[j]);&#xA;&#xA;    using var g = Graphics.FromImage(projectionBitmap);&#xA;    g.DrawImage(&#xA;        slice,&#xA;        srcRect: new Rectangle(0, 0, slice.Width, slice.Height),&#xA;        destRect: new Rectangle(index, 0, 1, projectionBitmap.Height),&#xA;        srcUnit: GraphicsUnit.Pixel&#xA;    );&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h3 id=&quot;pulling-it-all-together&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/approximately-correcting-perspective-with-c-sharp-fixing-a-blurry-presentation-video-part-two#pulling-it-all-together&quot;&gt;Pulling it all together&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;If we combine all of this logic together then we end up with a fairly straightforward static class that does all the work - takes a &lt;code&gt;Bitmap&lt;/code&gt; that is a frame from a video where there is a section that should be extracted and then &amp;quot;perspective-corrected&amp;quot;, takes the four points that describe that region and then returns a new &lt;code&gt;Bitmap&lt;/code&gt; that is the extracted content in a lovely rectangle!&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;&#xA;/// This uses a simple algorithm to try to undo the distortion of a rectangle in an image&#xA;/// due to perspective - it takes the content of the rectangle and stretches it into a&#xA;/// rectangle. This is only a simple approximation and does not guarantee accuracy (in&#xA;/// fact, it will result in an image that is slightly vertically stretched such that its&#xA;/// aspect ratio will not match the original content and a more thorough approach would&#xA;/// be necessary if this is too great an approximation)&#xA;/// &amp;lt;/summary&amp;gt;&#xA;internal static class SimplePerspectiveCorrection&#xA;{&#xA;    public static Bitmap ExtractAndPerspectiveCorrect(&#xA;        Bitmap image,&#xA;        Point topLeft,&#xA;        Point topRight,&#xA;        Point bottomRight,&#xA;        Point bottomLeft)&#xA;    {&#xA;        var (projectionSize, verticalSlices) =&#xA;            GetProjectionDetails(topLeft, topRight, bottomRight, bottomLeft);&#xA;&#xA;        var projection = new Bitmap(projectionSize.Width, projectionSize.Height);&#xA;        foreach (var (lineToTrace, index) in verticalSlices)&#xA;        {&#xA;            var lengthOfLineToTrace = LengthOfLine(lineToTrace);&#xA;&#xA;            var pixelsOnLine = Enumerable&#xA;                .Range(0, lengthOfLineToTrace)&#xA;                .Select(j =&amp;gt;&#xA;                {&#xA;                    var fractionOfProgressAlongLineToTrace = (float)j / lengthOfLineToTrace;&#xA;                    var point = GetPointAlongLine(lineToTrace, fractionOfProgressAlongLineToTrace);&#xA;                    return GetAverageColour(image, point);&#xA;                });&#xA;&#xA;            RenderSlice(projection, pixelsOnLine, index);&#xA;        }&#xA;        return projection;&#xA;&#xA;        static Color GetAverageColour(Bitmap image, PointF point)&#xA;        {&#xA;            var (integralX, fractionalX) = GetIntegralAndFractional(point.X);&#xA;            var x0 = integralX;&#xA;            var x1 = Math.Min(integralX &#x2B; 1, image.Width);&#xA;&#xA;            var (integralY, fractionalY) = GetIntegralAndFractional(point.Y);&#xA;            var y0 = integralY;&#xA;            var y1 = Math.Min(integralY &#x2B; 1, image.Height);&#xA;&#xA;            var (topColour0, topColour1) = GetColours(new Point(x0, y0), new Point(x1, y0));&#xA;            var (bottomColour0, bottomColour1) = GetColours(new Point(x0, y1), new Point(x1, y1));&#xA;&#xA;            return CombineColours(&#xA;                CombineColours(topColour0, topColour1, fractionalX),&#xA;                CombineColours(bottomColour0, bottomColour1, fractionalX),&#xA;                fractionalY&#xA;            );&#xA;&#xA;            (Color c0, Color c1) GetColours(Point p0, Point p1)&#xA;            {&#xA;                var c0 = image.GetPixel(p0.X, p0.Y);&#xA;                var c1 = (p0 == p1) ? c0 : image.GetPixel(p1.X, p1.Y);&#xA;                return (c0, c1);&#xA;            }&#xA;&#xA;            static (int Integral, float Fractional) GetIntegralAndFractional(float value)&#xA;            {&#xA;                var integral = (int)Math.Truncate(value);&#xA;                var fractional = value - integral;&#xA;                return (integral, fractional);&#xA;            }&#xA;&#xA;            static Color CombineColours(Color x, Color y, float proportionOfSecondColour)&#xA;            {&#xA;                if ((proportionOfSecondColour == 0) || (x == y))&#xA;                    return x;&#xA;&#xA;                if (proportionOfSecondColour == 1)&#xA;                    return y;&#xA;&#xA;                return Color.FromArgb(&#xA;                    red: CombineComponent(x.R, y.R),&#xA;                    green: CombineComponent(x.G, y.G),&#xA;                    blue: CombineComponent(x.B, y.B),&#xA;                    alpha: CombineComponent(x.A, y.A)&#xA;                );&#xA;&#xA;                int CombineComponent(int x, int y) =&amp;gt;&#xA;                    Math.Min(&#xA;                        (int)Math.Round(&#xA;                            (x * (1 - proportionOfSecondColour)) &#x2B;&#xA;                            (y * proportionOfSecondColour)&#xA;                        ),&#xA;                        255&#xA;                    );&#xA;            }&#xA;        }&#xA;    }&#xA;&#xA;    private sealed record ProjectionDetails(&#xA;        Size ProjectionSize,&#xA;        IEnumerable&amp;lt;((PointF From, PointF To) Line, int Index)&amp;gt; VerticalSlices&#xA;    );&#xA;&#xA;    private static ProjectionDetails GetProjectionDetails(&#xA;        Point topLeft,&#xA;        Point topRight,&#xA;        Point bottomRight,&#xA;        Point bottomLeft)&#xA;    {&#xA;        var topEdge = (From: topLeft, To: topRight);&#xA;        var bottomEdge = (From: bottomLeft, To: bottomRight);&#xA;        var lengthOfEdgeToStartFrom = LengthOfLine(topEdge);&#xA;        var dimensions = new Size(&#xA;            width: lengthOfEdgeToStartFrom,&#xA;            height: Math.Max(&#xA;                LengthOfLine((topLeft, bottomLeft)),&#xA;                LengthOfLine((topRight, bottomRight))&#xA;            )&#xA;        );&#xA;        return new ProjectionDetails(dimensions, GetVerticalSlices());&#xA;&#xA;        IEnumerable&amp;lt;((PointF From, PointF To) Line, int Index)&amp;gt; GetVerticalSlices() =&amp;gt;&#xA;            Enumerable&#xA;                .Range(0, lengthOfEdgeToStartFrom)&#xA;                .Select(i =&amp;gt;&#xA;                {&#xA;                    var fractionOfProgressAlongPrimaryEdge = (float)i / lengthOfEdgeToStartFrom;&#xA;                    return (&#xA;                        Line: (&#xA;                            GetPointAlongLine(topEdge, fractionOfProgressAlongPrimaryEdge),&#xA;                            GetPointAlongLine(bottomEdge, fractionOfProgressAlongPrimaryEdge)&#xA;                        ),&#xA;                        Index: i&#xA;                    );&#xA;                });&#xA;    }&#xA;&#xA;    private static PointF GetPointAlongLine((PointF From, PointF To) line, float fraction)&#xA;    {&#xA;        var deltaX = line.To.X - line.From.X;&#xA;        var deltaY = line.To.Y - line.From.Y;&#xA;        return new PointF(&#xA;            (deltaX * fraction) &#x2B; line.From.X,&#xA;            (deltaY * fraction) &#x2B; line.From.Y&#xA;        );&#xA;    }&#xA;&#xA;    private static int LengthOfLine((PointF From, PointF To) line)&#xA;    {&#xA;        var deltaX = line.To.X - line.From.X;&#xA;        var deltaY = line.To.Y - line.From.Y;&#xA;        return (int)Math.Round(Math.Sqrt((deltaX * deltaX) &#x2B; (deltaY * deltaY)));&#xA;    }&#xA;&#xA;    private static void RenderSlice(&#xA;        Bitmap projectionBitmap,&#xA;        IEnumerable&amp;lt;Color&amp;gt; pixelsOnLine,&#xA;        int index)&#xA;    {&#xA;        var pixelsOnLineArray = pixelsOnLine.ToArray();&#xA;&#xA;        using var slice = new Bitmap(1, pixelsOnLineArray.Length);&#xA;        for (var j = 0; j &amp;lt; pixelsOnLineArray.Length; j&#x2B;&#x2B;)&#xA;            slice.SetPixel(0, j, pixelsOnLineArray[j]);&#xA;&#xA;        using var g = Graphics.FromImage(projectionBitmap);&#xA;        g.DrawImage(&#xA;            slice,&#xA;            srcRect: new Rectangle(0, 0, slice.Width, slice.Height),&#xA;            destRect: new Rectangle(index, 0, 1, projectionBitmap.Height),&#xA;            srcUnit: GraphicsUnit.Pixel&#xA;        );&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h3 id=&quot;coming-next&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/approximately-correcting-perspective-with-c-sharp-fixing-a-blurry-presentation-video-part-two#coming-next&quot;&gt;Coming next&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;So step one was to take frames from a video and to work out what the bounds were of the area where slides were being projected (and to filter out any intro and outro frames), step two has been to be able to take the bounded area from any slide and project it back into a rectangle to make it easier to match against the original slide images.. step three will be to use these projections to try to guess what slide is being displayed on what frame!&lt;/p&gt;&#xA;&lt;p&gt;The frame that I&#x27;ve been using as an example throughout this post probably looks like a fairly easy case - big blocks of white or black and not actually &lt;em&gt;too&lt;/em&gt; out of focus.. but some of the frames look like this and that&#x27;s a whole other kettle of fish!&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/OutOfFocusFrame.jpg&quot; alt=&quot;An out of focus frame from a presentation&quot; title=&quot;An out of focus frame from a presentation&quot;&gt;&lt;/p&gt;&#xA;&lt;div class=&quot;Related&quot;&gt;&lt;h3&gt;You may also be interested in (see &lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;here&lt;/a&gt; for information about how these are generated):&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one&quot;&gt;Finding the brightest area in an image with C# (fixing a blurry presentation video - part one)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp&quot;&gt;How are barcodes read?? (Library-less image processing in C#)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/face-or-no-face-finding-faces-in-photos-using-c-sharp-and-accordnet&quot;&gt;Face or no face (finding faces in photos using C# and Accord.NET)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
                <pubDate>Tue, 29 Mar 2022 19:02:00 GMT</pubDate>
            </item>
            <item>
                <title>Finding the brightest area in an image with C# (fixing a blurry presentation video - part one)</title>
                <link>https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one</link>
                <guid>https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one</guid>
                <description>&lt;h3 id=&quot;tldr&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one#tldr&quot;&gt;TL;DR&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I have a video of a presentation where the camera keeps losing focus such that the slides are unreadable. I have the original slide deck and I want to fix this.&lt;/p&gt;&#xA;&lt;p&gt;The first step is analysing the individual frames of the video to find a common &amp;quot;most illuminated area&amp;quot; so that I can work out where the slide content was being projected, and that is what is described in this post.&lt;/p&gt;&#xA;&lt;p&gt;(&lt;strong&gt;An experimental TL;DR approach:&lt;/strong&gt; See this &lt;a href=&quot;https://dotnetfiddle.net/X8IPgQ&quot;&gt;small scale .NET Fiddle demonstration&lt;/a&gt; of what I&#x27;ll be discussing)&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/OutOfFocusFrame.jpg&quot; alt=&quot;An out of focus frame from a presentation&quot; title=&quot;An out of focus frame from a presentation&quot;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;the-basic-approach&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one#the-basic-approach&quot;&gt;The basic approach&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;An overview of the processing to do this looks as follows:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Load the image into a &lt;code&gt;Bitmap&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;Convert the image to greyscale&lt;/li&gt;&#xA;&lt;li&gt;Identify the lightest and darkest values in the greyscale range&lt;/li&gt;&#xA;&lt;li&gt;Calculate a 2/3 threshold from that range and create a mask of the image where anything below that value is zero and anything equal to or greater is one&#xA;&lt;ul&gt;&#xA;&lt;li&gt;eg. If the darkest value was 10 and the lightest was 220 then the difference is 220 - 10 = 210 and the cutoff point would be 2/3 of this range on top of the minimum, so the threshold value would equal ((2/3) * range) &#x2B; minimum = ((2/3) * 210) &#x2B; 10 = 140 &#x2B; 10 = 150&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;Find the largest bounded area within this mask (if there is one) and presume that that&#x27;s the projection of the slide in the darkened room!&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Before looking at code to do that, I&#x27;m going to toss in a few other complications that arise from having to process a &lt;em&gt;lot&lt;/em&gt; of frames from throughout the video, rather than just one..&lt;/p&gt;&#xA;&lt;p&gt;Firstly, the camera loses focus at different points in the video and to different extents and so some frames are blurrier than others. Following the steps above, the blurrier frames are likely to report a larger projection area for the slides. I would really like to identify a common projection area that is reasonable to use across all frames because this will make later processing (where I try to work out what slide is currently being shown in the frame) easier.&lt;/p&gt;&#xA;&lt;p&gt;Secondly, this video has intro and outro animations and it would be nice if I was able to write code that worked out when they stopped and started.&lt;/p&gt;&#xA;&lt;h3 id=&quot;the-implementation-for-a-single-image&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one#the-implementation-for-a-single-image&quot;&gt;The implementation for a single image&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;To do this work, I&#x27;m going to introduce a variation of my old friend the &lt;code&gt;DataRectangle&lt;/code&gt; (from &amp;quot;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp&quot;&gt;How are barcodes read?&lt;/a&gt;&amp;quot; and &amp;quot;&lt;a href=&quot;https://www.productiverage.com/face-or-no-face-finding-faces-in-photos-using-c-sharp-and-accordnet&quot;&gt;Face or no face&lt;/a&gt;&amp;quot;) -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;public static class DataRectangle&#xA;{&#xA;    public static DataRectangle&amp;lt;T&amp;gt; For&amp;lt;T&amp;gt;(T[,] values) =&amp;gt; new DataRectangle&amp;lt;T&amp;gt;(values);&#xA;}&#xA;&#xA;public sealed class DataRectangle&amp;lt;T&amp;gt;&#xA;{&#xA;    private readonly T[,] _protectedValues;&#xA;    public DataRectangle(T[,] values) : this(values, isolationCopyMayBeBypassed: false) { }&#xA;    private DataRectangle(T[,] values, bool isolationCopyMayBeBypassed)&#xA;    {&#xA;        if ((values.GetLowerBound(0) != 0) || (values.GetLowerBound(1) != 0))&#xA;            throw new ArgumentException(&amp;quot;Both dimensions must have lower bound zero&amp;quot;);&#xA;        var arrayWidth = values.GetUpperBound(0) &#x2B; 1;&#xA;        var arrayHeight = values.GetUpperBound(1) &#x2B; 1;&#xA;        if ((arrayWidth == 0) || (arrayHeight == 0))&#xA;            throw new ArgumentException(&amp;quot;zero element arrays are not supported&amp;quot;);&#xA;&#xA;        Width = arrayWidth;&#xA;        Height = arrayHeight;&#xA;&#xA;        if (isolationCopyMayBeBypassed)&#xA;            _protectedValues = values;&#xA;        else&#xA;        {&#xA;            _protectedValues = new T[Width, Height];&#xA;            Array.Copy(values, _protectedValues, Width * Height);&#xA;        }&#xA;    }&#xA;&#xA;    public int Width { get; }&#xA;&#xA;    public int Height { get; }&#xA;&#xA;    public T this[int x, int y]&#xA;    {&#xA;        get&#xA;        {&#xA;            if ((x &amp;lt; 0) || (x &amp;gt;= Width))&#xA;                throw new ArgumentOutOfRangeException(nameof(x));&#xA;            if ((y &amp;lt; 0) || (y &amp;gt;= Height))&#xA;                throw new ArgumentOutOfRangeException(nameof(y));&#xA;            return _protectedValues[x, y];&#xA;        }&#xA;    }&#xA;&#xA;    public IEnumerable&amp;lt;(Point Point, T Value)&amp;gt; Enumerate()&#xA;    {&#xA;        for (var x = 0; x &amp;lt; Width; x&#x2B;&#x2B;)&#xA;        {&#xA;            for (var y = 0; y &amp;lt; Height; y&#x2B;&#x2B;)&#xA;            {&#xA;                var value = _protectedValues[x, y];&#xA;                var point = new Point(x, y);&#xA;                yield return (point, value);&#xA;            }&#xA;        }&#xA;    }&#xA;&#xA;    public DataRectangle&amp;lt;TResult&amp;gt; Transform&amp;lt;TResult&amp;gt;(Func&amp;lt;T, TResult&amp;gt; transformer)&#xA;    {&#xA;        var transformed = new TResult[Width, Height];&#xA;        for (var x = 0; x &amp;lt; Width; x&#x2B;&#x2B;)&#xA;        {&#xA;            for (var y = 0; y &amp;lt; Height; y&#x2B;&#x2B;)&#xA;                transformed[x, y] = transformer(_protectedValues[x, y]);&#xA;        }&#xA;        return new DataRectangle&amp;lt;TResult&amp;gt;(transformed, isolationCopyMayBeBypassed: true);&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;For working with &lt;code&gt;DataRectangle&lt;/code&gt; instances that contain &lt;code&gt;double&lt;/code&gt; values (as we will be here), I&#x27;ve got a couple of convenient extension methods:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;public static class DataRectangleOfDoubleExtensions&#xA;{&#xA;    public static (double Min, double Max) GetMinAndMax(this DataRectangle&amp;lt;double&amp;gt; source) =&amp;gt;&#xA;        source&#xA;            .Enumerate()&#xA;            .Select(pointAndValue =&amp;gt; pointAndValue.Value)&#xA;            .Aggregate(&#xA;                seed: (Min: double.MaxValue, Max: double.MinValue),&#xA;                func: (acc, value) =&amp;gt; (Math.Min(value, acc.Min), Math.Max(value, acc.Max))&#xA;            );&#xA;&#xA;    public static DataRectangle&amp;lt;bool&amp;gt; Mask(this DataRectangle&amp;lt;double&amp;gt; values, double threshold) =&amp;gt;&#xA;        values.Transform(value =&amp;gt; value &amp;gt;= threshold);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;And for working with &lt;code&gt;Bitmap&lt;/code&gt; instances, I&#x27;ve got some extension methods for those as well:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;public static class BitmapExtensions&#xA;{&#xA;    public static Bitmap CopyAndResize(this Bitmap image, int resizeLargestSideTo)&#xA;    {&#xA;        var (width, height) = (image.Width &amp;gt; image.Height)&#xA;            ? (resizeLargestSideTo, (int)((double)image.Height / image.Width * resizeLargestSideTo))&#xA;            : ((int)((double)image.Width / image.Height * resizeLargestSideTo), resizeLargestSideTo);&#xA;&#xA;        return new Bitmap(image, width, height);&#xA;    }&#xA;&#xA;    /// &amp;lt;summary&amp;gt;&#xA;    /// This will return values in the range 0-255 (inclusive)&#xA;    /// &amp;lt;/summary&amp;gt;&#xA;    // Based on http://stackoverflow.com/a/4748383/3813189&#xA;    public static DataRectangle&amp;lt;double&amp;gt; GetGreyscale(this Bitmap image) =&amp;gt;&#xA;        image&#xA;            .GetAllPixels()&#xA;            .Transform(c =&amp;gt; (0.2989 * c.R) &#x2B; (0.5870 * c.G) &#x2B; (0.1140 * c.B));&#xA;&#xA;    public static DataRectangle&amp;lt;Color&amp;gt; GetAllPixels(this Bitmap image)&#xA;    {&#xA;        var values = new Color[image.Width, image.Height];&#xA;        var data = image.LockBits(&#xA;            new Rectangle(0, 0, image.Width, image.Height),&#xA;            ImageLockMode.ReadOnly,&#xA;            PixelFormat.Format24bppRgb&#xA;        );&#xA;        try&#xA;        {&#xA;            var pixelData = new byte[data.Stride];&#xA;            for (var lineIndex = 0; lineIndex &amp;lt; data.Height; lineIndex&#x2B;&#x2B;)&#xA;            {&#xA;                Marshal.Copy(&#xA;                    source: data.Scan0 &#x2B; (lineIndex * data.Stride),&#xA;                    destination: pixelData,&#xA;                    startIndex: 0,&#xA;                    length: data.Stride&#xA;                );&#xA;                for (var pixelOffset = 0; pixelOffset &amp;lt; data.Width; pixelOffset&#x2B;&#x2B;)&#xA;                {&#xA;                    // Note: PixelFormat.Format24bppRgb means the data is stored in memory as BGR&#xA;                    const int PixelWidth = 3;&#xA;                    values[pixelOffset, lineIndex] = Color.FromArgb(&#xA;                        red: pixelData[pixelOffset * PixelWidth &#x2B; 2],&#xA;                        green: pixelData[pixelOffset * PixelWidth &#x2B; 1],&#xA;                        blue: pixelData[pixelOffset * PixelWidth]&#xA;                    );&#xA;                }&#xA;            }&#xA;        }&#xA;        finally&#xA;        {&#xA;            image.UnlockBits(data);&#xA;        }&#xA;        return DataRectangle.For(values);&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;With this code, we can already perform those first steps that I&#x27;ve described in the find-projection-area-in-image process.&lt;/p&gt;&#xA;&lt;p&gt;Note that I&#x27;m going to throw in an extra step of shrinking the input images if they&#x27;re larger than 400px because we don&#x27;t need pixel-perfect accuracy when the whole point of this process is that a lot of the frames are too blurry to read (as a plus, shrinking the images means that there&#x27;s less data to process and the whole thing should finish more quickly).&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;using var image = new Bitmap(&amp;quot;frame_338.jpg&amp;quot;);&#xA;using var resizedImage = image.CopyAndResize(resizeLargestSideTo: 400);&#xA;var greyScaleImageData = resizedImage.GetGreyscale();&#xA;var (min, max) = greyScaleImageData.GetMinAndMax();&#xA;var range = max - min;&#xA;const double thresholdOfRange = 2 / 3d;&#xA;var thresholdForMasking = min &#x2B; (range * thresholdOfRange);&#xA;var mask = greyScaleImageData.Mask(thresholdForMasking);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This gives us a &lt;code&gt;DataRectangle&lt;/code&gt; of boolean values that represent the brighter points as true and the less bright points as false.&lt;/p&gt;&#xA;&lt;p&gt;In the image below, you can see the original frame on the left. In the middle is the content that would be masked out by hiding all but the brightest pixels. On the right is the &amp;quot;binary mask&amp;quot; (where we discard the original colour of the pixel and make them all either black or white) -&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/VideoFrameWithMask.jpg&quot; alt=&quot;A frame from the video with the brightest third of the pixels masked out&quot; title=&quot;A frame from the video with the brightest third of the pixels masked out&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now we need to identify the largest &amp;quot;object&amp;quot; within this mask - wherever bright pixels are adjacent to other bright pixels, they will be considered part of the same object and we would expect there to be several such objects within the mask that has been generated.&lt;/p&gt;&#xA;&lt;p&gt;To do so, I&#x27;ll be reusing some more code from &amp;quot;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp&quot;&gt;How are barcodes read?&lt;/a&gt;&amp;quot; -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static IEnumerable&amp;lt;IEnumerable&amp;lt;Point&amp;gt;&amp;gt; GetDistinctObjects(DataRectangle&amp;lt;bool&amp;gt; mask)&#xA;{&#xA;    // Flood fill areas in the mask to create distinct areas&#xA;    var allPoints = mask&#xA;        .Enumerate()&#xA;        .Where(pointAndIsMasked =&amp;gt; pointAndIsMasked.Value)&#xA;        .Select(pointAndIsMasked =&amp;gt; pointAndIsMasked.Point).ToHashSet();&#xA;    while (allPoints.Any())&#xA;    {&#xA;        var currentPoint = allPoints.First();&#xA;        var pointsInObject = GetPointsInObject(currentPoint).ToArray();&#xA;        foreach (var point in pointsInObject)&#xA;            allPoints.Remove(point);&#xA;        yield return pointsInObject;&#xA;    }&#xA;&#xA;    // Inspired by code at&#xA;    // https://simpledevcode.wordpress.com/2015/12/29/flood-fill-algorithm-using-c-net/&#xA;    IEnumerable&amp;lt;Point&amp;gt; GetPointsInObject(Point startAt)&#xA;    {&#xA;        var pixels = new Stack&amp;lt;Point&amp;gt;();&#xA;        pixels.Push(startAt);&#xA;&#xA;        var valueAtOriginPoint = mask[startAt.X, startAt.Y];&#xA;        var filledPixels = new HashSet&amp;lt;Point&amp;gt;();&#xA;        while (pixels.Count &amp;gt; 0)&#xA;        {&#xA;            var currentPoint = pixels.Pop();&#xA;            if ((currentPoint.X &amp;lt; 0) || (currentPoint.X &amp;gt;= mask.Width)&#xA;            || (currentPoint.Y &amp;lt; 0) || (currentPoint.Y &amp;gt;= mask.Height))&#xA;                continue;&#xA;&#xA;            if ((mask[currentPoint.X, currentPoint.Y] == valueAtOriginPoint)&#xA;            &amp;amp;&amp;amp; !filledPixels.Contains(currentPoint))&#xA;            {&#xA;                filledPixels.Add(new Point(currentPoint.X, currentPoint.Y));&#xA;                pixels.Push(new Point(currentPoint.X - 1, currentPoint.Y));&#xA;                pixels.Push(new Point(currentPoint.X &#x2B; 1, currentPoint.Y));&#xA;                pixels.Push(new Point(currentPoint.X, currentPoint.Y - 1));&#xA;                pixels.Push(new Point(currentPoint.X, currentPoint.Y &#x2B; 1));&#xA;            }&#xA;        }&#xA;        return filledPixels;&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;As the code mentions, this is based on an article &amp;quot;&lt;a href=&quot;https://simpledevcode.wordpress.com/2015/12/29/flood-fill-algorithm-using-c-net/&quot;&gt;Flood Fill algorithm (using C#.NET)&lt;/a&gt;&amp;quot; and its output is a list of objects, where each object is a list of points within that object. So the way to determine which object is largest is to take the one that contains the most points!&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/VideoFrameWithLargestBinaryMaskObject.jpg&quot; alt=&quot;A binary mask of a frame in the video with all but the single largest areas hidden&quot; title=&quot;A binary mask of a frame in the video with all but the single largest areas hidden&quot;&gt;&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var pointsInLargestHighlightedArea = GetDistinctObjects(mask)&#xA;    .OrderByDescending(points =&amp;gt; points.Count())&#xA;    .FirstOrDefault();&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;em&gt;(&lt;strong&gt;Note:&lt;/strong&gt; If &lt;code&gt;pointsInLargestHighlightedArea&lt;/code&gt; is null then we need to escape out of the method that we&#x27;re in because the source image didn&#x27;t produce a mask with any highlighted objects - this could happen if the image has every single with the same colour, for example; an edge case, surely, but one that we should handle)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;From this largest object, we want to find a bounding quadrilateral, which we do by looking at every point and finding the one closest to the top left of the image (because this will be the top left of the bounding area), the point closest to the top right of the image (for the top right of the bounding area) and the same for the points closest to the bottom left and bottom right.&lt;/p&gt;&#xA;&lt;p&gt;This can be achieved by calculating, for each point in the object, the distances from each of the corners to the points and then determining which points have the shortest distances - eg.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var distancesOfPointsFromImageCorners = pointsInLargeHighlightedArea&#xA;    .Select(p =&amp;gt;&#xA;    {&#xA;        // To work out distance from the top left, you would use Pythagoras to take the&#xA;        // squared horizontal distance of the point from the left of the image and add&#xA;        // that to the squared vertical distance of the point from the top of the image,&#xA;        // then you would square root that sum. In this case, we only want to be able to&#xA;        // compare determine which distances are smaller or larger and we don&#x27;t actually&#xA;        // care about the precise distances themselves and so we can save ourselves from&#xA;        // performing that final square root calculation.&#xA;        var distanceFromRight = greyScaleImageData.Width - p.X;&#xA;        var distanceFromBottom = greyScaleImageData.Height - p.Y;&#xA;        var fromLeftScore = p.X * p.X;&#xA;        var fromTopScore = p.Y * p.Y;&#xA;        var fromRightScore = distanceFromRight * distanceFromRight;&#xA;        var fromBottomScore = distanceFromBottom * distanceFromBottom;&#xA;        return new&#xA;        {&#xA;            Point = p,&#xA;            FromTopLeft = fromLeftScore &#x2B; fromTopScore,&#xA;            FromTopRight = fromRightScore &#x2B; fromTopScore,&#xA;            FromBottomLeft = fromLeftScore &#x2B; fromBottomScore,&#xA;            FromBottomRight = fromRightScore &#x2B; fromBottomScore&#xA;        };&#xA;    })&#xA;    .ToArray(); // Call ToArray to avoid repeating this enumeration four times below&#xA;    &#xA;var topLeft = distancesOfPointsFromImageCorners.OrderBy(p =&amp;gt; p.FromTopLeft).First().Point;&#xA;var topRight = distancesOfPointsFromImageCorners.OrderBy(p =&amp;gt; p.FromTopRight).First().Point;&#xA;var bottomLeft = distancesOfPointsFromImageCorners.OrderBy(p =&amp;gt; p.FromBottomLeft).First().Point;&#xA;var bottomRight = distancesOfPointsFromImageCorners.OrderBy(p =&amp;gt; p.FromBottomRight).First().Point;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Finally, because we want to find the bounding area of the largest object in the original image, we may need to multiply up the bounds that we just found because we shrank the image down if either dimension was larger than 400px and we were performing calculations on that smaller version.&lt;/p&gt;&#xA;&lt;p&gt;We can tell how much we reduced the data by looking at the width of the original image and comparing it to the width of the greyScaleImageData &lt;code&gt;DataRectangle&lt;/code&gt; that was generated from the shrunken version of the image:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var reducedImageSideBy = (double)image.Width / greyScaleImageData.Width;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Now we only need a function that will multiply the bounding area that we&#x27;ve got according to this value, while ensuring that none of the point values are multiplied such that they exceed the bounds of the original image:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static (Point TopLeft, Point TopRight, Point BottomRight, Point BottomLeft) Resize(&#xA;    Point topLeft,&#xA;    Point topRight,&#xA;    Point bottomRight,&#xA;    Point bottomLeft,&#xA;    double resizeBy,&#xA;    int minX,&#xA;    int maxX,&#xA;    int minY,&#xA;    int maxY)&#xA;{&#xA;    if (resizeBy &amp;lt;= 0)&#xA;        throw new ArgumentOutOfRangeException(&amp;quot;must be a positive value&amp;quot;, nameof(resizeBy));&#xA;&#xA;    return (&#xA;        Constrain(Multiply(topLeft)),&#xA;        Constrain(Multiply(topRight)),&#xA;        Constrain(Multiply(bottomRight)),&#xA;        Constrain(Multiply(bottomLeft))&#xA;    );&#xA;&#xA;    Point Multiply(Point p) =&amp;gt;&#xA;        new Point((int)Math.Round(p.X * resizeBy), (int)Math.Round(p.Y * resizeBy));&#xA;&#xA;    Point Constrain(Point p) =&amp;gt;&#xA;        new Point(Math.Min(Math.Max(p.X, minX), maxX), Math.Min(Math.Max(p.Y, minY), maxY));&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The final bounding area for the largest bright area of an image is now retrieved like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var bounds = Resize(&#xA;    topLeft,&#xA;    topRight,&#xA;    bottomRight,&#xA;    bottomLeft,&#xA;    reducedImageSideBy,&#xA;    minX: 0,&#xA;    maxX: image.Width - 1,&#xA;    minY: 0,&#xA;    maxY: image.Height - 1&#xA;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;For the example image that we&#x27;re looking at, this area is outlined liked this:&lt;/p&gt;&#xA;&lt;img alt=&quot;An outline around the largest object within the binary mask of a frame from the video&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/VideoFrameWithLargestBinaryMaskObjectOutlined.jpg&quot; class=&quot;AlwaysFullWidth&quot; title=&quot;An outline around the largest object within the binary mask of a frame from the video&quot;&gt;&#xA;&lt;h3 id=&quot;applying-the-process-to-multiple-images&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one#applying-the-process-to-multiple-images&quot;&gt;Applying the process to multiple images&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Say that we put all of the above functionality into a method called &lt;code&gt;GetMostHighlightedArea&lt;/code&gt; that took a &lt;code&gt;Bitmap&lt;/code&gt; to process and returned a tuple of the four points that represented the bounds of the brightest area, we could then easily prepare a LINQ statement that ran that code and found the most common brightest-area-bounds across all of the source images that I have. &lt;em&gt;(As I said before, the largest-bounded-area will vary from image to image in my example as the camera recording the session gained and lost focus)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var files = new DirectoryInfo(&amp;quot;Frames&amp;quot;).EnumerateFiles(&amp;quot;*.jpg&amp;quot;);&#xA;var (topLeft, topRight, bottomRight, bottomLeft) = files&#xA;    .Select(file =&amp;gt;&#xA;    {&#xA;        using var image = new Bitmap(file.FullName);&#xA;        return IlluminatedAreaLocator.GetMostHighlightedArea(image);&#xA;    })&#xA;    .GroupBy(area =&amp;gt; area)&#xA;    .OrderByDescending(group =&amp;gt; group.Count())&#xA;    .Select(group =&amp;gt; group.Key)&#xA;    .FirstOrDefault();&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Presuming that there is a folder called &amp;quot;Frames&amp;quot; in the output folder of project*, this will read them all, look for the largest bright area on each of them individually, then return the area that appears most often across all of the images. &lt;em&gt;(Note: If there are no images to read then the &lt;code&gt;FirstOrDefault&lt;/code&gt; call at the bottom will return a default tuple-of-four-Points, which will be 4x (0,0) values)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(Since you probably don&#x27;t happen to have a bunch of images from a video of my presentation lying around, see the next section for some code that will download some in case you want to try this all out!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;This ties in nicely with my recent post &amp;quot;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp&quot;&gt;Parallelising (LINQ) work in C#&lt;/a&gt;&amp;quot; because the processing required for each image is..&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Completely independent from the processing of the other images (important for parallelising work)&lt;/li&gt;&#xA;&lt;li&gt;Expensive enough that the overhead from splitting the work into multiple threads and then combining their results back together would be overshadowed by the work performed (which is also important for parallelising work - if individual tasks are too small and the computer spends more time scheduling the work on threads and then pulling all the results back together than it does on actually performing that work then using multiple threads can be &lt;em&gt;slower&lt;/em&gt; than using a single one!)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;All that we would have to change in order to use multiple threads to process multiple images is the addition of a single line:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var files = new DirectoryInfo(&amp;quot;Frames&amp;quot;).EnumerateFiles(&amp;quot;*.jpg&amp;quot;);&#xA;var (topLeft, topRight, bottomRight, bottomLeft) = files&#xA;    .AsParallel() // &amp;lt;- WOO!! This is all that we needed to add!&#xA;    .Select(file =&amp;gt;&#xA;    {&#xA;        using var image = new Bitmap(file.FullName);&#xA;        return IlluminatedAreaLocator.GetMostHighlightedArea(image);&#xA;    })&#xA;    .GroupBy(area =&amp;gt; area)&#xA;    .OrderByDescending(group =&amp;gt; group.Count())&#xA;    .Select(group =&amp;gt; group.Key)&#xA;    .FirstOrDefault();&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;em&gt;(&lt;strong&gt;Parallelisation sidebar:&lt;/strong&gt; When we split up the work like this, if the processing for each image was solely in memory then it would be a no-brainer that using more threads would make sense - however, the processing for each image involves LOADING the image from disk and THEN processing it in memory and if you had a spinning rust hard disk then you may fear that trying to ask it to read multiple files simultaneously would be slower than asking it to read them one at a time because its poor little read heads have to physically move around the plates.. it turns out that this is not necessarily the case and that you can find more information in this article that I found interesting; &amp;quot;&lt;a href=&quot;https://pkolaczk.github.io/disk-parallelism/&quot;&gt;Performance Impact of Parallel Disk Access&lt;/a&gt;&amp;quot;)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;testing-the-code-on-your-own-computer&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one#testing-the-code-on-your-own-computer&quot;&gt;Testing the code on your own computer&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I haven&#x27;t quite finished yet but I figured that there may be &lt;em&gt;some&lt;/em&gt; wild people out there that would like to try running this code locally themselves - maybe just to see it work or maybe even to get it working and then chop and change it for some new and exciting purpose!&lt;/p&gt;&#xA;&lt;p&gt;To this end, I have some sample frames available from this video that I&#x27;m trying to fix - with varying levels of fuzziness present. To download them, use the following method:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static async Task EnsureSamplesAvailable(DirectoryInfo framesfolder)&#xA;{&#xA;    // Note: The GitHub API is rate limited quite severely for non-authenticated apps, so we just&#xA;    // only call use it if the framesFolder doesn&#x27;t exist or is empty - if there are already files&#xA;    // in there then we presume that we downloaded them on a previous run (if the API is hit too&#xA;    // often then it will return a 403 &amp;quot;rate limited&amp;quot; response)&#xA;    if (framesfolder.Exists &amp;amp;&amp;amp; framesfolder.EnumerateFiles().Any())&#xA;    {&#xA;        Console.WriteLine(&amp;quot;Sample images have already been downloaded and are ready for use&amp;quot;);&#xA;        return;&#xA;    }&#xA;&#xA;    Console.WriteLine(&amp;quot;Downloading sample images..&amp;quot;);&#xA;    if (!framesfolder.Exists)&#xA;        framesfolder.Create();&#xA;&#xA;    string namesAndUrlsJson;&#xA;    using (var client = new WebClient())&#xA;    {&#xA;        // The API refuses requests without a User Agent, so set one before calling (see&#xA;        // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required)&#xA;        client.Headers.Add(HttpRequestHeader.UserAgent, &amp;quot;ProductiveRage Blog Post Example&amp;quot;);&#xA;        namesAndUrlsJson = await client.DownloadStringTaskAsync(new Uri(&#xA;            &amp;quot;https://api.github.com/repos/&amp;quot; &#x2B;&#xA;            &amp;quot;ProductiveRage/NaivePerspectiveCorrection/contents/Samples/Frames&amp;quot;&#xA;        ));&#xA;    }&#xA;&#xA;    // Deserialise the response into an array of entries that have Name and Download_Url properties&#xA;    var namesAndUrls = JsonConvert.DeserializeAnonymousType(&#xA;        namesAndUrlsJson,&#xA;        new[] { new { Name = &amp;quot;&amp;quot;, Download_Url = (Uri?)null } }&#xA;    );&#xA;    if (namesAndUrls is null)&#xA;    {&#xA;        Console.WriteLine(&amp;quot;GitHub reported zero sample images to download&amp;quot;);&#xA;        return;&#xA;    }&#xA;&#xA;    await Task.WhenAll(namesAndUrls&#xA;        .Select(async entry =&amp;gt;&#xA;        {&#xA;            using var client = new WebClient();&#xA;            await client.DownloadFileTaskAsync(&#xA;                entry.Download_Url,&#xA;                Path.Combine(framesfolder.FullName, entry.Name)&#xA;            );&#xA;        })&#xA;    );&#xA;&#xA;    Console.WriteLine($&amp;quot;Downloaded {namesAndUrls.Length} sample image(s)&amp;quot;);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;.. and call it with the following argument, presuming you&#x27;re trying to read images from the &amp;quot;Frames&amp;quot; folder as the code earlier illustrated:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;await EnsureSamplesAvailable(new DirectoryInfo(&amp;quot;Frames&amp;quot;));&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h3 id=&quot;filtering-out-introoutro-slides&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one#filtering-out-introoutro-slides&quot;&gt;Filtering out intro/outro slides&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;So I said earlier that it would also be nice if I could programmatically identify which frames were part of the intro/outro animations of the video that I&#x27;m looking at.&lt;/p&gt;&#xA;&lt;p&gt;It feels logical that any frame that is of the actual presentation will have a fairly similarly-sized-and-located bright area (where a slide is being projected onto a wall in a darkened room) while any frame that is part of an intro/outro animation won&#x27;t. So we should be able to take the &lt;strong&gt;most-common&lt;/strong&gt;-largest-brightest-area and then look at every frame and see if &lt;em&gt;its&lt;/em&gt; largest bright area is approximately the same - if it&#x27;s similar enough then it&#x27;s probably a frame that is part of the projection but if it&#x27;s too dissimilar then it&#x27;s probably &lt;em&gt;not&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Rather than waste time going too far down a rabbit hole that I&#x27;ve found won&#x27;t immediately result in success, I&#x27;m going to use a slightly altered version of that plan (I&#x27;ll explain why in a moment). I&#x27;m still going to take that common largest brightest area and compare the largest bright area on each frame to it but, instead of saying &amp;quot;largest-bright-area-is-close-enough-to-the-most-common = presentation frame / largest-bright-area-&lt;strong&gt;not&lt;/strong&gt;-close-enough = intro or outro&amp;quot;, I&#x27;m going to find the &lt;em&gt;first&lt;/em&gt; frame whose largest bright area is close enough and the &lt;em&gt;last&lt;/em&gt; frame that is and declare that that range is probably where the frames for the presentation are.&lt;/p&gt;&#xA;&lt;p&gt;The reason that I&#x27;m going to do this is that I found that there are some slides with more variance that can skew the results if the first approach was taken - if a frame in the middle of the presentation is so blurry that the range in intensity from darkest pixel to brightest pixel is squashed down too far then it can result in it identifying a largest bright area that isn&#x27;t an accurate representation of the image. It&#x27;s quite possible that I could still have made the first approach work by tweaking some other parameters in the image processing - such as considering changing that arbitrary &amp;quot;create a mask where the intensity threshold is 2/3 of the range of the brightness of all pixels&amp;quot; (maybe 3/4 would have worked better?), for example - but I know that this second approach works for my data and so I didn&#x27;t pursue the first one too hard.&lt;/p&gt;&#xA;&lt;p&gt;To do this, though, we are going to need to know what order the frames are supposed to appear in - it&#x27;s no longer sufficient for there to simply be a list of images that are frames out of the video, we now need to know what were they appeared relative to each other. This is simple enough with my data because they all have names like &amp;quot;frame_1052.jpg&amp;quot; where 1052 is the frame index from the original video.&lt;/p&gt;&#xA;&lt;p&gt;So I&#x27;m going to change the frame-image-loading code to look like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;// Get all filenames, parse the frame index from them and discard any that don&#x27;t&#xA;// match the filename pattern that is expected (eg. &amp;quot;frame_1052.jpg&amp;quot;)&#xA;var frameIndexMatcher = new Regex(@&amp;quot;frame_(\d&#x2B;)\.jpg&amp;quot;, RegexOptions.IgnoreCase);&#xA;var files = new DirectoryInfo(&amp;quot;Frames&amp;quot;)&#xA;    .EnumerateFiles()&#xA;    .Select(file =&amp;gt;&#xA;    {&#xA;        var frameIndexMatch = frameIndexMatcher.Match(file.Name);&#xA;        return frameIndexMatch.Success&#xA;            ? (file.FullName, FrameIndex: int.Parse(frameIndexMatch.Groups[1].Value))&#xA;            : default;&#xA;    })&#xA;    .Where(entry =&amp;gt; entry != default);&#xA;&#xA;// Get the largest bright area for each file&#xA;var allFrameHighlightedAreas = files&#xA;    .AsParallel()&#xA;    .Select(file =&amp;gt;&#xA;    {&#xA;        using var image = new Bitmap(file.FullName);&#xA;        return (&#xA;            file.FrameIndex,&#xA;            HighlightedArea: IlluminatedAreaLocator.GetMostHighlightedArea(image)&#xA;        );&#xA;    })&#xA;    .ToArray()&#xA;&#xA;// Get the most common largest bright area across all of the images&#xA;var (topLeft, topRight, bottomRight, bottomLeft) = allFrameHighlightedAreas&#xA;    .GroupBy(entry =&amp;gt; entry.HighlightedArea)&#xA;    .OrderByDescending(group =&amp;gt; group.Count())&#xA;    .Select(group =&amp;gt; group.Key)&#xA;    .FirstOrDefault();&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;em&gt;(Note that I&#x27;m calling &lt;code&gt;ToArray()&lt;/code&gt; when declaring &lt;code&gt;allFrameHighlightedAreas&lt;/code&gt; - that&#x27;s to store the results now because I know that I&#x27;m going to need every result in the list that is generated and because I&#x27;m going to enumerate it twice in the work outlined here, so there&#x27;s no point leaving &lt;code&gt;allFrameHighlightedAreas&lt;/code&gt; to be a lazily-evaluated &lt;code&gt;IEnumerable&lt;/code&gt; that would be recalculated each time it was looped over; then it would be doing all of the &lt;code&gt;IlluminatedAreaLocator.GetMostHighlightedArea&lt;/code&gt; calculations for each image twice if enumerated the list twice, which would just be wasteful!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now to look at the &lt;code&gt;allFrameHighlightedAreas&lt;/code&gt; list and try to decide if each &lt;code&gt;HighlightedArea&lt;/code&gt; value is close enough to the most common area that we found. I&#x27;m going to use a very simple algorithm for this - I&#x27;m going to:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Take all four points from the &lt;code&gt;HighlightedArea&lt;/code&gt; on each entry in &lt;code&gt;allFrameHighlightedAreas&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;Take all four points from the most common area (which are the &lt;code&gt;topLeft&lt;/code&gt;, &lt;code&gt;topRight&lt;/code&gt;, &lt;code&gt;bottomRight&lt;/code&gt;, &lt;code&gt;bottomLeft&lt;/code&gt; values that we already have in the code above)&lt;/li&gt;&#xA;&lt;li&gt;Take the differences in &lt;code&gt;X&lt;/code&gt; value between all four points in these two areas and add them up&lt;/li&gt;&#xA;&lt;li&gt;Compare this difference to the width of the most common highlighted area - if it&#x27;s too big of a proportion (say if the sum of the &lt;code&gt;X&lt;/code&gt; differences is greater than 20% of the width of the entire area) then we&#x27;ll say it&#x27;s not a match and drop out of this list&lt;/li&gt;&#xA;&lt;li&gt;If the &lt;code&gt;X&lt;/code&gt; values aren&#x27;t too bad then we&#x27;ll take the differences in &lt;code&gt;Y&lt;/code&gt; value between all four points in these two areas and add &lt;em&gt;those&lt;/em&gt; up&lt;/li&gt;&#xA;&lt;li&gt;That total will be compared to the height of the most common highlighted area - if it&#x27;s more than the 20% threshold then we&#x27;ll say that it&#x27;s not a match&lt;/li&gt;&#xA;&lt;li&gt;If we got to here then we&#x27;ll say that the highlighted area in the current frame &lt;em&gt;is&lt;/em&gt; close enough to the most common highlighted area and so the current frame probably is part of the presentation - yay!&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;In code:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var highlightedAreaWidth = Math.Max(topRight.X, bottomRight.X) - Math.Min(topLeft.X, bottomLeft.X);&#xA;var highlightedAreaHeight = Math.Max(bottomLeft.Y, bottomRight.Y) - Math.Min(topLeft.Y, topRight.Y);&#xA;const double thresholdForPointVarianceComparedToAreaSize = 0.2;&#xA;var frameIndexesThatHaveTheMostCommonHighlightedArea = allFrameHighlightedAreas&#xA;    .Where(entry =&amp;gt;&#xA;    {&#xA;        var (entryTL, entryTR, entryBR, entryBL) = entry.HighlightedArea;&#xA;        var xVariance =&#xA;            new[]&#xA;            {&#xA;                entryBL.X - bottomLeft.X,&#xA;                entryBR.X - bottomRight.X,&#xA;                entryTL.X - topLeft.X,&#xA;                entryTR.X - topRight.X&#xA;            }&#xA;            .Sum(Math.Abs);&#xA;        var yVariance =&#xA;            new[]&#xA;            {&#xA;                entryBL.Y - bottomLeft.Y,&#xA;                entryBR.Y - bottomRight.Y,&#xA;                entryTL.Y - topLeft.Y,&#xA;                entryTR.Y - topRight.Y&#xA;            }&#xA;            .Sum(Math.Abs);&#xA;        return&#xA;            (xVariance &amp;lt;= highlightedAreaWidth * thresholdForPointVarianceComparedToAreaSize) &amp;amp;&amp;amp;&#xA;            (yVariance &amp;lt;= highlightedAreaHeight * thresholdForPointVarianceComparedToAreaSize);&#xA;    })&#xA;    .Select(entry =&amp;gt; entry.FrameIndex)&#xA;    .ToArray();&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This gives us a &lt;code&gt;frameIndexesThatHaveTheMostCommonHighlightedArea&lt;/code&gt; array of frame indexes that have a largest brightest area that is fairly close to the most common one. So to decide which frames are probably the start of the presentation and the end, we simply need to say:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var firstFrameIndex = frameIndexesThatHaveTheMostCommonHighlightedArea.Min();&#xA;var lasttFrameIndex = frameIndexesThatHaveTheMostCommonHighlightedArea.Max();&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Any frames whose index is less than &lt;code&gt;firstFrameIndex&lt;/code&gt; or greater than &lt;code&gt;lastFrameIndex&lt;/code&gt; is probably part of the intro or outro sequence - eg.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/VideoFrameWithMask-Intro.jpg&quot; alt=&quot;A frame from the intro of the video - the largest bright area is not near the slide projection&quot; title=&quot;A frame from the intro of the video - the largest bright area is not near the slide projection&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;Any frames whose index is within the &lt;code&gt;firstFrameIndex&lt;/code&gt; / &lt;code&gt;lastFrameIndex&lt;/code&gt; range is probably part of the presentation - eg.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/VideoFrameWithMask-Alternate.jpg&quot; alt=&quot;A frame from the presentation part of the video - the largest bright area IS the slide projection&quot; title=&quot;A frame from the presentation part of the video - the largest bright area IS the slide projection&quot;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;coming-soon&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one#coming-soon&quot;&gt;Coming soon&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;As the title of this post strongly suggests, this is only the first step in my desire to fix up my blurry presentation video. What I&#x27;m going to have to cover in the future is to:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Extract the content from the most-common-brightest-area in each frame of the video that is part of the presentation and contort it back into a rectangle - undoing the distortion that is introduced by perspective due to the position of the camera and where the slides were projected in the room (I&#x27;ll be tackling this in a slightly approximate-but-good-enough manner because to do it super accurately requires lots of complicated maths and I&#x27;ve managed to forget nearly all of the maths degree that I got twenty years ago!)&lt;/li&gt;&#xA;&lt;li&gt;Find a way to compare the perspective-corrected projections from each frame against a clean image of the original slide deck and work out which slide each frame is most similar to (this should be possible with some surprisingly rudimentary calculations inspired by some of the image preprocessing that I&#x27;ve mentioned in a couple of my &lt;a href=&quot;https://www.productiverage.com/Archive/Tag/Machine%20Learning&quot;&gt;posts that touch on machine learning&lt;/a&gt; but without requiring any machine learning itself)&lt;/li&gt;&#xA;&lt;li&gt;Some tweaks that were required to get the best results with my particular images (for example, when I described the &lt;code&gt;GetMostHighlightedArea&lt;/code&gt; function earlier, I picked 400px as an arbitrary value to resize images to before greyscaling them, masking them and looking for their largest bright area; maybe it will turn out that smaller or larger values for that process result in improved or worsened results - we&#x27;ll find out!)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Once this is all done, I will take the original frame images and, for each one, overlay a clean version of the slide that appeared blurrily in the frame (again, I&#x27;ll have clean versions of each slide from the original slide deck that I produced, so that should be an easy part) - then I&#x27;ll mash them all back together into a new video, combined with the original audio. To do this (the video work), I&#x27;ll likely use the same tool that I used to extract the individual frame files from the video in the first place - the famous &lt;a href=&quot;https://www.ffmpeg.org/&quot;&gt;FFmpeg&lt;/a&gt;!&lt;/p&gt;&#xA;&lt;p&gt;I doubt that I&#x27;ll have a post on this last section as it would only be a small amount of C# code that combines two images for each frame, writes the results to disk, followed by me making a command line call to FFmpeg to produce the video - and I don&#x27;t think that there&#x27;s anything particularly exciting there! If I get this all completed, though, I will - of course - link to the fixed-up presentation video.. because why not shameless plug myself given any opportunity!&lt;/p&gt;&#xA;&lt;div class=&quot;Related&quot;&gt;&lt;h3&gt;You may also be interested in (see &lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;here&lt;/a&gt; for information about how these are generated):&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/approximately-correcting-perspective-with-c-sharp-fixing-a-blurry-presentation-video-part-two&quot;&gt;(Approximately) correcting perspective with C# (fixing a blurry presentation video - part two)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp&quot;&gt;How are barcodes read?? (Library-less image processing in C#)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/face-or-no-face-finding-faces-in-photos-using-c-sharp-and-accordnet&quot;&gt;Face or no face (finding faces in photos using C# and Accord.NET)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
                <pubDate>Tue, 15 Mar 2022 21:06:00 GMT</pubDate>
            </item>
            <item>
                <title>So.. what is machine learning? (#NoCodeIntro)</title>
                <link>https://www.productiverage.com/so-what-is-machine-learning-nocodeintro</link>
                <guid>https://www.productiverage.com/so-what-is-machine-learning-nocodeintro</guid>
                <description>&lt;h3 id=&quot;tldr&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#tldr&quot;&gt;TL;DR&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Strap in, this is a long one. If the title of the post isn&#x27;t enough of a summary for you but you think that this &amp;quot;TL;DR&amp;quot; is too long then this probably isn&#x27;t the article for you!&lt;/p&gt;&#xA;&lt;p&gt;A previous job I had was, in a nutshell, working on improving searching for files and documents by incorporating machine learning algorithms - eg. if I&#x27;ve found a PowerPoint presentation that I produced five years ago on my computer and I want to find the document that I made with loads of notes and research relating to it, how can I find it if I&#x27;ve forgotten the filename or where I stored it? This product could pull in data from many data sources (such as Google Docs, as well as files on my computer) and it could, amongst other things, use clever similarity algorithms to suggest which documents may be related to that presentation. This is just one example but even this is a bit of a mouthful! So when people outside of the industry asked me what I did, it was often hard to answer them in a way that satisfied us both.&lt;/p&gt;&#xA;&lt;p&gt;This came to a head recently when I tried to explain to someone semi-technical what the difference actually was between machine learning and.. er, &lt;em&gt;not&lt;/em&gt; machine learning. The &amp;quot;classic approach&amp;quot;, I suppose you might call it. I tried hard but I made a real meal of the explanation and doing lots of hand waving did not make up for a lack of whiteboard, drawing apparatus or a generally clear manner to explain. So, for my own peace of mind (and so that I can share this with them!), I want to try to describe the difference at a high level and then talk about how machine learning can work (in this case, I&#x27;ll mostly be talking about &amp;quot;supervised classification&amp;quot; - I&#x27;ll explain what that means shortly and list some of the other types) in a way that is hopefully understandable without requiring any coding or mathematical knowledge.&lt;/p&gt;&#xA;&lt;h3 id=&quot;the-short-version&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#the-short-version&quot;&gt;The short version&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;The &amp;quot;classic approach&amp;quot; involves the programmer writing very specific code for every single step in a given process&lt;/li&gt;&#xA;&lt;li&gt;&amp;quot;Supervised classification&amp;quot; involves the programmer writing some quite general (ie. not specific to the precise task) code and then giving it lots of information along with a brief summary of each piece of information (also known as a label) so that it can create a &amp;quot;trained model&amp;quot; that can guess how to label new information that it hasn&#x27;t seen before&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 id=&quot;what-this-means-in-practice&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#what-this-means-in-practice&quot;&gt;What this means in practice&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;An example of the first (&amp;quot;classic&amp;quot;) approach might be to calculate the total for a list of purchases:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;The code will look through each item and lookup in a database what rate of tax should be applied to it (for example, books are exempt from &lt;a href=&quot;https://en.wikipedia.org/wiki/Value-added_tax&quot;&gt;VAT&lt;/a&gt; in the UK)&lt;/li&gt;&#xA;&lt;li&gt;If there &lt;em&gt;is&lt;/em&gt; a tax to apply then the tax for the item will be calculated and this will be added to the initial item&#x27;s cost&lt;/li&gt;&#xA;&lt;li&gt;All of these costs will be added up to produce a total&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;This sort of code is easy to understand and if there are any problems encountered in the process then it&#x27;s easy to diagnose them. For example, if an item appeared on the list that wasn&#x27;t in the database - and so it wasn&#x27;t possible to determine whether it should be taxed or not - then the problem could easily stop with an &amp;quot;item not found in database&amp;quot; error. There are a lot of advantages to code being simple to comprehend and having it easy to understand how and why bad things have happened.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(Anyone involved in coding knows that dealing with &amp;quot;the happy path&amp;quot; of everything going to plan is only a small part of the job and it&#x27;s often when things go wrong that life gets hard - and the easier it is to understand precisely what happened when something &lt;em&gt;does&lt;/em&gt; go wrong, the better!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;An example of the second (&amp;quot;machine learning&amp;quot;) approach might be to determine whether a given photo is of a cat or a dog:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;There will be non-specific (or &amp;quot;generic&amp;quot;) code that is written that can take a list of &amp;quot;labelled&amp;quot; items (eg. this is a picture of a cat, this is a picture of a dog) and use it to predict an unseen and unlabelled item (eg. here is a picture - is it a cat or a dog?) - this code is considered to be generic because nothing in the way it is written relates to cats or dogs, all it is intended to do is be able to receive lots of labelled data and produce a trained model that can make predictions on future items&lt;/li&gt;&#xA;&lt;li&gt;The code will be given a &lt;em&gt;lot&lt;/em&gt; of labelled data (maybe there are 10,000 pictures of cats and 10,000 pictures of dogs) and it will perform some sort of clever mathematics that allows it to build a model trained to differentiate between cats and dogs - generally, the more labelled data that is provided, the better the final trained model will be at making predictions.. but the more data that there is, the longer that it will take to train&lt;/li&gt;&#xA;&lt;li&gt;When it is finished &amp;quot;training&amp;quot; (ie. producing this &amp;quot;trained model&amp;quot;), it will then be able to be given a picture of a cat or a dog and say how likely it is that it thinks it is a cat vs a dog&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;This sounds like quite a silly example but there are many applications of this sort of approach that are really useful - for example, the same non-specific/generic code could be given inputs that are scans of hospital patients where it is suspected that there is a cancerous growth in the image. It would be trained by being given 1,000s of images that doctors have already said &amp;quot;this looks like a malignant growth&amp;quot; or &amp;quot;this looks like nothing to worry about&amp;quot; and the trained model that would be produced from that information would then be able to take images of patient scans that it&#x27;s never seen before and predict whether it shows something to worry about.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(This sort of thing would almost certainly never replace doctors but it could be used to streamline some medical processes - maybe the trained model is good enough that if it predicts with more than 90% certainty that the scan is clear then a doctor wouldn&#x27;t need to look at it but if there was even a 10% chance that it could be a dangerous growth then a doctor &lt;strong&gt;should&lt;/strong&gt; look at it with higher priority)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Other examples could be taken from self-driving cars; from the images coming from the cameras on the car, does it look like any of them indicate pedestrians nearby? Does it look like there are speed limit signs that affect how quickly the car may travel?&lt;/p&gt;&#xA;&lt;p&gt;The results of the trained model need not be binary (only two options), either - ie. &amp;quot;is this a picture of a cat or is it a picture of a dog?&amp;quot;. It could be trained to predict a wide range of different animals, if we&#x27;re continuing on the animal-recognition example. In fact, an application that I&#x27;m going to look at in more depth later is using machine learning to recognise hand-written digits (ie. numbers 0 through 9) because, while this is a very common introductory task into the world of machine learning, it&#x27;s a sufficiently complicated task that it would be difficult to imagine how you might solve it using the &amp;quot;classic&amp;quot; approach to coding.&lt;/p&gt;&#xA;&lt;p&gt;Back to the definition of the type of machine learning that I want to concentrate on.. the reason it&#x27;s referred to as &amp;quot;supervised classification&amp;quot; is two-fold:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;The trained model that it produces has the sole task of taking inputs (such as pictures in the examples above, although there are other forms of inputs that I&#x27;ll mention soon) and predicting a &amp;quot;classification&amp;quot; for them. Generally, it will offer a &amp;quot;confidence score&amp;quot; for each of the classifications that it&#x27;s aware of - to continue the cat/dog example, if the trained model was given a picture of a cat then it would hopefully give a high prediction score that it was a cat (generally presented as a percentage) and the less confident it was that it was a cat, the more confident it would be that the picture was of a dog.&lt;/li&gt;&#xA;&lt;li&gt;The model is trained by the &amp;quot;labelled data&amp;quot; - it can&#x27;t guess which of the initial pictures are cats and which are dogs if it&#x27;s just given a load of unlabelled pictures and no other information to work from. The fact that this data is labelled means that someone has had to go through the process of manually applying these labels. This is the &amp;quot;supervised&amp;quot; aspect.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;There &lt;em&gt;are&lt;/em&gt; machine learning algorithms (where an &amp;quot;algorithm&amp;quot; is just a set of steps and calculations performed to produce some result) that are described as &amp;quot;unsupervised classification&amp;quot; but the most common example of this would be to train a model on a load of inputs and ask it to split them into groups based upon which it thinks seem most similar. It won&#x27;t be able to give a name to each group because all it has access to is the raw data of each item and no &amp;quot;label&amp;quot; for what each one represents.&lt;/p&gt;&#xA;&lt;p&gt;This sort of approach is a little similar to how the &amp;quot;find related documents&amp;quot; technology that I described at the top of this post works - the algorithm looks for &amp;quot;features&amp;quot;* that it thinks makes it quite likely that two documents contain the same sort of content and uses this to produce a confidence score that they may be related. I&#x27;ll talk about other types of machine learning briefly near the end of this post but, in an effort to give this any semblance of focus, I&#x27;m going to stick with talking about &amp;quot;supervised classification&amp;quot; for the large part.&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(&amp;quot;Features&amp;quot; has a specific meaning in terms of machine learning algorithms but I won&#x27;t go into detail on it right now, though I will later - for now, in the case of similar documents, you can imagine &amp;quot;features&amp;quot; as being uncommon words or phrases that are more likely to crop up in documents that are similar in some manner than in documents that are talking about entirely different subject matters)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;supervised-classification-with-neural-networks&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#supervised-classification-with-neural-networks&quot;&gt;Supervised classification with neural networks&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Right, &lt;em&gt;now&lt;/em&gt; we&#x27;re sounding all fancy and technical! A &amp;quot;neural network&amp;quot; is a model commonly used for supervised classification and I&#x27;m going to go through the steps of explaining how it is constructed and how it works. But first I&#x27;m going to try to explain what one is.&lt;/p&gt;&#xA;&lt;p&gt;The concept of a neural net was inspired by the human brain and how it has neurons that connect to each other with varying strengths. The strengths of the connections are developed based upon patterns that we&#x27;ve come to recognise. The human brain is amazing at recognising patterns and that&#x27;s why &lt;a href=&quot;https://news.mit.edu/2017/explained-neural-networks-deep-learning-0414&quot;&gt;two Chicago researchers in 1944&lt;/a&gt; were inspired to wonder if a similar structure could be used for some form of automated pattern recognition. I&#x27;m being intentionally vague here because the details aren&#x27;t too important and the way that connections are made in the human brain is much more complicated than those in the neural networks that I&#x27;ll be talking about here, so I only really mention it for a little historical context and to explain some of the names of things.&lt;/p&gt;&#xA;&lt;p&gt;A neural net has a set of &amp;quot;input neurons&amp;quot; and a set of &amp;quot;output neurons&amp;quot;, where the input neurons are connected to the output neurons.&lt;/p&gt;&#xA;&lt;p&gt;Each time that the network is given a single &amp;quot;input&amp;quot; (such as an image of a cat), that input needs to be broken down into values to feed to the input neurons; these input neurons accept numeric values between 0 and 1 (inclusive of those two values) and it may not immediately be apparent how a picture can be somehow represented by a list of 0-to-1 values but I&#x27;ll get to that later.&lt;/p&gt;&#xA;&lt;p&gt;There are broadly two types of classifier and this determines how many output neurons there will be - there are &amp;quot;binary classifiers&amp;quot; (is the answer yes or no; eg. &amp;quot;does this look like a malignant growth or not?&amp;quot;) and there are &amp;quot;multi-class classifier&amp;quot; (such as a classifier that tries to guess what kind of fruit an image is of; a banana, an apple, a mango, an orange, etc..). A binary classifier will have one output neuron whose output is a confidence score for the yes/no classification (eg. it is 10% certain that it is not an indication of cancer) while a multi-class classifier will have as many output neurons as there are known outputs (so an image of a mango will hopefully produce a high confidence score for the mango output neuron and a lower confidence score for the output neurons relating to the other types of fruit that it is trained to recognise).&lt;/p&gt;&#xA;&lt;p&gt;Each connection from the input neurons to the output neurons has a &amp;quot;weight&amp;quot; - a number that represents how strong the connection is. When an &amp;quot;input pattern&amp;quot; (which is the name for the list of 0-to-1 input neuron values that a single input, such as a picture of a cat, maybe represented by) is applied to the input neurons, the output neurons are set to a value that is the sum of every input neuron&#x27;s value that is connected to it multiplied by the weight of the connection.&lt;/p&gt;&#xA;&lt;p&gt;I know that this is sounding very abstract, so let&#x27;s visualise some extremely simple possible neural nets.&lt;/p&gt;&#xA;&lt;h3 id=&quot;examples-that-are-silly-to-use-machine-learning-for-but-which-are-informative-for-illustrating-the-principles&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#examples-that-are-silly-to-use-machine-learning-for-but-which-are-informative-for-illustrating-the-principles&quot;&gt;Examples that are silly to use machine learning for but which are informative for illustrating the principles&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The image below depicts a binary classifier (because there is only one output neuron) where there are only two input neurons. The connections between the two inputs neurons to the single output neuron each have a weight of 0.3.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/NeuralNetwork-AND.png&quot; alt=&quot;A simple neural network with two inputs and one output and connection weights of 0.3&quot; title=&quot;A simple neural network with two inputs and one output and connection weights of 0.3&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;This &lt;em&gt;could&lt;/em&gt; be considered to be a trained model for performing a boolean &amp;quot;AND&amp;quot; operation.. if you&#x27;ll allow me a few liberties that I will take back and address properly shortly.&lt;/p&gt;&#xA;&lt;p&gt;An &amp;quot;AND&amp;quot; operation could be described as a light bulb that is connected to two switches and the light bulb only illuminates if &lt;em&gt;both&lt;/em&gt; of the switches are set to on. If both switches are off then the light bulb is off, if only one of the switches is on (and the other is off) then the light bulb is off, if both switches are on then the light bulb turns on.&lt;/p&gt;&#xA;&lt;p&gt;Since the neuron inputs have to accept values between 0 and 1 then we could consider an &amp;quot;off&amp;quot; switch as being a 0 input and an &amp;quot;on&amp;quot; switch as being a 1 input.&lt;/p&gt;&#xA;&lt;p&gt;If both switches are off then the value at the output is &lt;strong&gt;(0 x 0.3) &#x2B; (0 x 0.3) = 0&lt;/strong&gt; because we take the input values, multiply them by their connection weights to the output and add these values up.&lt;/p&gt;&#xA;&lt;p&gt;If one switch is on and the other is off then the output is either &lt;strong&gt;(1 x 0.3) &#x2B; (0 x 0.3)&lt;/strong&gt; or &lt;strong&gt;(0 x 0.3) &#x2B; (1 x 0.3)&lt;/strong&gt;, both of which equal 0.3 and we&#x27;ll consider 0.5 to be the cut-off point at which we consider the output to be &amp;quot;on&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;If both switches are on then the output is &lt;strong&gt;(1 x 0.3) &#x2B; (1 x 0.3) = 0.6&lt;/strong&gt;, which is greater than 0.5 and so we consider the output to be on, which is the result that we wanted!&lt;/p&gt;&#xA;&lt;p&gt;Just in case it&#x27;s not obvious, this is &lt;em&gt;not&lt;/em&gt; a good use case for machine learning - this is an extremely simple process that would be written much more obviously in the &amp;quot;classic approach&amp;quot; to programming.&lt;/p&gt;&#xA;&lt;p&gt;Not only would it be simpler to use the classic approach, but this is also not suited for machine learning because we know &lt;em&gt;all&lt;/em&gt; of the possible input states and what their outputs should be - we know what happens when both switches are off and when precisely one switch is on and when both switches are on. The amazing thing with machine learning is that we can produce a trained model that can then make predictions about data that we&#x27;ve never seen before! Unlike this two-switches situation, we can&#x27;t possibly examine every single picture of either a cat or a dog in the entire world but we &lt;em&gt;can&lt;/em&gt; train a model to learn from one big set of pictures and then perform the crucial task of cat/dog identification in the future for photos that haven&#x27;t even been taken yet!&lt;/p&gt;&#xA;&lt;p&gt;For a little longer, though, I&#x27;m going to stick with some super-simple boolean operation examples because we can learn some important concepts.&lt;/p&gt;&#xA;&lt;p&gt;Where the &amp;quot;AND&amp;quot; operation requires both inputs to be &amp;quot;on&amp;quot; for the output to be &amp;quot;on&amp;quot;, there is an &amp;quot;OR&amp;quot; operation where the output will be &amp;quot;on&amp;quot; if either &lt;em&gt;or&lt;/em&gt; both of the inputs are on. The weights on the network shown above will not work for this.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/NeuralNetwork-OR.png&quot; alt=&quot;A simple neural network with two inputs and one output and connection weights of 0.5&quot; title=&quot;A simple neural network with two inputs and one output and connection weights of 0.5&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now this second network &lt;em&gt;would&lt;/em&gt; work to imitate an OR operation - if both switches are off then the output is &lt;strong&gt;(0 x 0.5) &#x2B; (0 x 0.5) = 0&lt;/strong&gt;, if precisely one switch is on then the output is &lt;strong&gt;(1 x 0.5) &#x2B; (0 x 0.5)&lt;/strong&gt; or &lt;strong&gt;(0 x 0.5) &#x2B; (1 x 0.5) = 0.5&lt;/strong&gt;, if both switches are on then the output is &lt;strong&gt;(1 x 0.5) &#x2B; (1 x 0.5) = 1&lt;/strong&gt;. So if both switches are off then the output is 0, which means the light bulb should be off, but if one or both of the switches are on then the output is at least 0.5, which means that the light bulb should be on.&lt;/p&gt;&#xA;&lt;p&gt;This highlights that the input neurons and output neurons determine the form of the data that the model can receive and what sort of output/prediction it can make - but it is the weight of the connections that control what processing occurs and how the input values contribute in producing produce the output value.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;*(Note that there can be more layers of neurons in between the input and output layer, which we&#x27;ll see an example of shortly)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;In both of the two examples above, it was as if we were looking at already-trained models for the AND and the OR operations but how did they get in that state? Obviously, for the purposes of this post, I made up the values to make the examples work - but that&#x27;s not going to work in the real world with more complex problems where I can&#x27;t just pull the numbers out of my head; what we want is for the computer to determine these connection weight values and it does this by a process of trial and improvement and it is &lt;em&gt;this&lt;/em&gt; act that is the actual &amp;quot;machine learning&amp;quot;!&lt;/p&gt;&#xA;&lt;p&gt;The way that it often works is that, as someone who wants to train a model, I decide on the number of inputs and outputs that are appropriate to my task and then the computer has a representation of a neural network of that shape in its memory where it initially sets all of the connection weights to random values. I then give it my labelled data (which, again, is a list of inputs and the expected output - where each individual &amp;quot;input&amp;quot; is really a list of input values that are all 0-1) and it tries running each of those inputs through its neural network and compares the calculated outputs to the outputs that I&#x27;ve told it to expect. Since this first attempt will be using random connection weights, the chances are that a lot of its calculated output will &lt;em&gt;not&lt;/em&gt; match the outputs that I&#x27;ve told it to expect. It will then try to adjust the connection weights so that hopefully things get a bit closer and then it will try running all of the inputs through the neural network with the new weights and see if the calculated outputs are closer to the expected output. It will do this over and over again, making small adjustments to the connection weights each time until it produces a network with connection weights that calculate the expected output for every input that I gave it to learn with.&lt;/p&gt;&#xA;&lt;p&gt;The reason that the weights that it uses initially are random values (generally between 0 and 1) is that the connection weight values can actually be any number that makes the network operate properly. While the input values are all 0-1 and the output value should end up being 0-1, the connection weights could be larger than one or they could be negative; they could be anything! So your first instinct might be &amp;quot;why set them all to random values instead of setting them all to 0.5 initially&amp;quot; and the answer is that while 0.5 is the mid-point in the allowable ranges for the input values and the output values, there &lt;em&gt;is&lt;/em&gt; no set mid-point for the connection weight values. You &lt;em&gt;may&lt;/em&gt; then wonder why not set them all to zero because &lt;em&gt;that&lt;/em&gt; sounds like it&#x27;s in the middle of &amp;quot;all possible numbers&amp;quot; (since the weights could be positive or they could be negative) and the machine learning could then either change them from zero to positive or negative as seems appropriate.. well, at the risk of skimming over details, numbers often behave a little strangely in some kinds of maths when zeroes are involved and so you generally get better results starting with random connection weight values, rather than starting with them all at zero.&lt;/p&gt;&#xA;&lt;p&gt;Let&#x27;s imagine, then, that we decided that we wanted to train a neural network to perform the &amp;quot;OR&amp;quot; operation. We know that there are two inputs required and one output. And we then let the computer represent this model in memory and have it give the connections random weight values. Let&#x27;s say that it picks weights 0.9 and 0.1 for the connections from Input1-to-Output and Input2-to-Output, respectively.&lt;/p&gt;&#xA;&lt;p&gt;We know that the labelled data that we&#x27;re training with looks like this:&lt;/p&gt;&#xA;&lt;div class=&quot;TableScrollWrapper&quot;&gt;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 1&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 2&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Output&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&lt;/div&gt;&#xA;&lt;p&gt;.. and the first time that we tried running these inputs through our 0.9 / 0.1 connection weight neural network, we&#x27;d get these results:&lt;/p&gt;&#xA;&lt;div class=&quot;TableScrollWrapper&quot;&gt;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 1&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 2&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Calculated Output&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Expected Output&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: left;&quot;&gt;Is Correct&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x0.9) &#x2B; (0x0.1) = &lt;strong&gt;0.0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (0.0 &amp;lt; 0.5 so consider this 0)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x0.9) &#x2B; (1x0.1) = &lt;strong&gt;0.1&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;No&lt;/strong&gt; (0.1 &amp;lt; 0.5 so consider this 0 but we wanted 1)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(1x0.9) &#x2B; (0x0.1) = &lt;strong&gt;0.9&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (0.9 &amp;gt;= 0.5 so consider this 1)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(1x0.9) &#x2B; (1x0.1) = &lt;strong&gt;1.0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (1.9 &amp;gt;= 0.5 so consider this 1)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&lt;/div&gt;&#xA;&lt;p&gt;Unsurprisingly (since completely random connection weights were selected), the results are not correct and so some work is required to adjust the weight values to try to improve things.&lt;/p&gt;&#xA;&lt;p&gt;I&#x27;m going to grossly simplify what really happens at this point but it should be close enough to illustrate the point. The process tries to improve the model by repeatedly running every input pattern through the model (the input patterns in this case are (0, 0), (0, 1), (1, 0), (1, 1)) and comparing the output of the model to the output that is expected for each input pattern (as we know (0, 0) should have an output of &amp;lt; 0.5, while any of the input patterns (0, 1), (1, 0) and (1, 1) should have an output of &amp;gt;= 0.5). When there is a discrepancy in the model&#x27;s output and the expected output, it will adjust one or more of the connection weights up and down.. then it will do it &lt;em&gt;again&lt;/em&gt; and hopefully find that the calculated outputs are closer to the expected outputs, then again and again until the model&#x27;s calculated outputs for every input pattern match the expected outputs.&lt;/p&gt;&#xA;&lt;p&gt;So it will first try the pattern (0, 0) and the output will be 0 and so no change is needed there.&lt;/p&gt;&#xA;&lt;p&gt;Then it will try (0, 1) and find that the output is too low and so it will increase the weight of the connections slightly, so now maybe they go from 0.9 / 0.1 to 0.91 / 0.11.&lt;/p&gt;&#xA;&lt;p&gt;Then it will try (1, 0) with the new 0.91 / 0.11 weights and find that it gets the correct output (more than 0.5) and so make no change.&lt;/p&gt;&#xA;&lt;p&gt;Then it will try (1, 1) with the same increased 0.91 / 0.11 weights and find that it still gets the correct output there and so make no more changes.&lt;/p&gt;&#xA;&lt;p&gt;After this adjustment, the input pattern (0, 1) will &lt;em&gt;still&lt;/em&gt; be too low &lt;strong&gt;(0 x 0.9) &#x2B; (1 x 0.11)&lt;/strong&gt; and so it will have to go round again.&lt;/p&gt;&#xA;&lt;p&gt;It might continue doing this multiple times until the weights end up something like 0.5 / 1.4 and now it will have a model that gets all of the right values!&lt;/p&gt;&#xA;&lt;div class=&quot;TableScrollWrapper&quot;&gt;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 1&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 2&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Calculated Output&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Expected Output&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: left;&quot;&gt;Is Correct&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x1.4) &#x2B; (0x0.5) = &lt;strong&gt;0.0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (0.0 &amp;lt; 0.5 so consider this 0)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x1.4) &#x2B; (1x0.5) = &lt;strong&gt;0.5&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (0.5 &amp;gt;= 0.5 so consider this 1)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(1x1.4) &#x2B; (0x0.5) = &lt;strong&gt;1.4&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (1.4 &amp;gt;= 0.5 so consider this 1)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(1x1.4) &#x2B; (1x0.5) = &lt;strong&gt;1.9&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (1.9 &amp;gt;= 0.5 so consider this 1)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&lt;/div&gt;&#xA;&lt;p&gt;That&#x27;s the very high-level gist, that it goes round and round in trying each input pattern and comparing the computed output to the expected output until the computed and expected outputs match. Great success!&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(I&#x27;m not going to go into any more detail about &lt;strong&gt;how&lt;/strong&gt; this weight-adjusting process works because I&#x27;m trying to avoid digging into any code in this post - just be aware that this process of calculating the output for each known input and then adjusting the connection weights and retrying until the output values are what we expect for each set of inputs &lt;strong&gt;is&lt;/strong&gt; the actual training of the model, which I&#x27;ll be referring to multiple times throughout the explanations here)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now, there are a few things that may seem wrong based upon what I&#x27;ve said previously and how exactly it adjusts those weights through trial-and-improvement:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;The calculations here show that the four outputs are 0.0, 0.5, 1.4 and 1.9 but I said earlier that the input values should all be in the range 0-1 &lt;em&gt;and&lt;/em&gt; the output values should be in the same range of 0-1&lt;/li&gt;&#xA;&lt;li&gt;Why does it adjust the weights so slowly when it needs to alter them; why would it only add 0.01 to the weight that connects Input1-to-Output each time?&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Because I&#x27;m contrary, I&#x27;ll address the second point first. If the weights were increased too quickly then the outputs may then &amp;quot;overshoot&amp;quot; the target output values that we&#x27;re looking for and the next time that it tries to improve the values, it may find that it has to &lt;em&gt;reduce&lt;/em&gt; them. Now, in this simple case where we&#x27;re trying to model an &amp;quot;OR&amp;quot; operation, that&#x27;s not going to be a problem because the input pattern (0, 0) will always get an output of 0 since it is calculated as &lt;strong&gt;(0 x Input1-to-Output-connection-weight) &#x2B; (0 x Input2-to-Output-connection-weight)&lt;/strong&gt; and that will always be 0, while the other three input patterns should all end up with an output of 0.5 or greater. However, for more complicated models, there will be times when weights need to be reduced in some cases as well as increased. If the changes made to the weights are too large then they might bounce back and forth on each attempt and ever settle into the correct values, so smaller adjustments are more likely to result in training a model that matches the requirements but at the cost of having to go round and round on the trial-and-improvement attempts more often.&lt;/p&gt;&#xA;&lt;p&gt;This means that it will take longer to come to the final result and this is one of the issues with machine learning - for more complicated models, there can be a huge number of these trial-and-improvement attempts and each attempt has to run &lt;em&gt;every&lt;/em&gt; input pattern through the model. When I was talking about training a model with 10,000 pictures of cats and 10,000 pictures of dogs and &lt;em&gt;all&lt;/em&gt; these inputs have to be fed through a neural network until the outputs are correct then it can take a long time. That&#x27;s not the case here (where there are only 4 input patterns and it&#x27;s a very simple network) but for larger cases, there can be a point where you allow the model to train for a certain period and then accept that it won&#x27;t be perfect but hope that it&#x27;s good enough for your purposes, as a compromise against how long it takes to train - it can take &lt;em&gt;days&lt;/em&gt; to train some really complex models with lots and lots of labelled data! Likewise, another challenge/compromise is trying to decide how quickly the weights should be adjusted - the larger the changes that it makes to the weight values of connections between neurons, the closer that it can get to a good result but it might actually make it impossible to get the best possible result if it keeps bouncing some of the weights back and forth, as I just explained!&lt;/p&gt;&#xA;&lt;p&gt;Now to address the first point. There&#x27;s a modicum of maths involved here but you don&#x27;t have to understand it in any great depth. I&#x27;ve been pretending that the way to calculate the output value on our network is to take &lt;strong&gt;(Input1&#x27;s value x the Input1-to-Output&#x27;s connection weight) &#x2B; (Input2&#x27;s value x the Input2-to-Output&#x27;s connection weight)&lt;/strong&gt; but, as we&#x27;ve just seen, this result of this can be greater than 1 and input values and output values are all supposed to be within the 0-1 range. In fact, using this calculation, it would be possible to get a negative output value because neuron connection weights can be negative (I&#x27;ll explain why in some more examples of machine learning a little later on) and that would also mean that the output value would fall outside of the 0-1 range that we require.&lt;/p&gt;&#xA;&lt;p&gt;To fix this, we take the simple calculation that I&#x27;ve been using so far and pass the value through a formula that can take any number and squash it into the 0-1 range. While there are different formula options for neural networks, a common one is the &amp;quot;sigmoid function&amp;quot; and it would look like this if it was drawn on a graph (picture courtesy of &lt;a href=&quot;https://de.wikipedia.org/wiki/Datei:Sigmoid-function-2.svg&quot;&gt;Wikipedia&lt;/a&gt;) -&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/SigmoidFunction.png&quot; alt=&quot;The sigmoid function&quot; title=&quot;The sigmoid function&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;Although this graph only shows values from -8 to &#x2B;8, you can see that its &amp;quot;S shape&amp;quot; means that the lines get very flat the larger that the number is. So if the formula is given a value of 0 then the result will be 0.5, if it&#x27;s given a value of 2 then the result will be about 0.88, if it&#x27;s given a value of 4 then the result is about 0.98, it&#x27;s given a value of 8 then it&#x27;s over 0.999 and the larger the value that the function is given the closer that the result will be to 1. It has the same effect for negative numbers - negative numbers that are -8 or larger (-12, -100, -1000) will all return a value very close to 0.&lt;/p&gt;&#xA;&lt;p&gt;The actual formula for this graph is shown on the top left of the image (&amp;quot;&lt;strong&gt;sig(t) = 1 / (1 &#x2B; e^-t)&lt;/strong&gt;&amp;quot;) but that&#x27;s really not important to us right now, what is important is the shape of the graph and how it constrains all possible values to the range 0-1.&lt;/p&gt;&#xA;&lt;p&gt;If we took the network that we talked about above (that trains a model to perform an &amp;quot;OR&amp;quot; operation and where we ended up with connection weights of 1.4 and 0.5) and &lt;em&gt;then&lt;/em&gt; applied the sigmoid function to the calculated output values then we&#x27;d find that those weights wouldn&#x27;t actually work and the machine learning process would have to produce slightly different weights to get the correct results. But I&#x27;m not going to worry about that now since the point of that example was simply to offer a very approximate overview of how the trial-and-improvement process works. Besides, we&#x27;ve got a more pressing issue to talk about..&lt;/p&gt;&#xA;&lt;h3 id=&quot;the-limits-of-such-a-simple-network-and-the-concept-of-linearly-separable-data&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#the-limits-of-such-a-simple-network-and-the-concept-of-linearly-separable-data&quot;&gt;The limits of such a simple network and the concept of &amp;quot;linearly separable&amp;quot; data&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The two examples of models that we&#x27;ve trained so far are extremely simple in one important way - if you drew a graph with the four input values on them and were asked to draw a straight line that separated the inputs that should relate to an &amp;quot;off&amp;quot; state from the inputs that should relate to an &amp;quot;on&amp;quot; state then you do it very easily, like this:&lt;/p&gt;&#xA;&lt;img alt=&quot;The &#x27;AND&#x27; boolean operation is linearly separable&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/Boolean-AND.png&quot; class=&quot;HalfWidth&quot; title=&quot;The &#x27;AND&#x27; boolean operation is linearly separable&quot;&gt;&#xA;&lt;img alt=&quot;The &#x27;OR&#x27; boolean operation is linearly separable&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/Boolean-OR.png&quot; class=&quot;HalfWidth&quot; title=&quot;The &#x27;OR&#x27; boolean operation is linearly separable&quot;&gt;&#xA;&lt;p&gt;But not all sets of data &lt;em&gt;can&lt;/em&gt; be segregated so simply and, unfortunately, it is a limitation to the very simple network shape that we&#x27;ve seen so far (where the input layer is directly connected to the output layer) that it can &lt;em&gt;only&lt;/em&gt; work if the data can be split with all positive results on one side of a straight line and all negative results on the other side. Cases where this &lt;em&gt;is&lt;/em&gt; possible (such as the &amp;quot;AND&amp;quot; and &amp;quot;OR&amp;quot; examples) are referred to as being &amp;quot;linearly separable&amp;quot; (quite literally, the results in either category can be separated by a &lt;em&gt;single straight line&lt;/em&gt; and the model training is, in effect, to work out where that line should lie). Interestingly, there are actually quite a lot of types of data analysis that have binary outcomes that &lt;em&gt;are&lt;/em&gt; linearly separable - but I don&#x27;t want to go too far into talking about that and listing examples because I can&#x27;t cover &lt;em&gt;everything&lt;/em&gt; about machine learning and automated data analysis in this post!&lt;/p&gt;&#xA;&lt;p&gt;A really simple example of data that is &lt;em&gt;not&lt;/em&gt; linearly separable is an &amp;quot;XOR&amp;quot; operation. While I imagine that the &amp;quot;AND&amp;quot; and &amp;quot;OR&amp;quot; operations are named so simply that you could intuit their definitions without a grounding in boolean logic, this may require slightly more explanation. &amp;quot;XOR&amp;quot; is an abbreviation of &amp;quot;eXclusive OR&amp;quot; and, to return to our light bulb and two switches example, the light should be off if both switches are off, it should be on if &lt;em&gt;one&lt;/em&gt; of the switches is on but it should be off if &lt;em&gt;both&lt;/em&gt; of the switches are on. On the surface, this sounds like a bizarre situation but it&#x27;s actually encountered in nearly every two-storey residence in the modern world - when you have a light on your upstairs landing, there will be a switch for it downstairs and one upstairs. When both switches are off, the light is off. If you are downstairs and switch the downstairs switch on then the light comes on. If you then go upstairs and turn on your bedroom light, you may then switch the upstairs landing light and the light will go off. At this point, both switches are on but the light is off. So the upstairs light is only illuminated if only &lt;em&gt;one&lt;/em&gt; of the switches is on - when they&#x27;re &lt;em&gt;both&lt;/em&gt; on, the light goes off.&lt;/p&gt;&#xA;&lt;p&gt;If we illustrated this with a graph like the &amp;quot;AND&amp;quot; and &amp;quot;OR&amp;quot; graphs above then you can see that there is no way to draw a single straight line on that graph where every state on one side of the line represents the light being on while every state on the other side of the line represents the light being off.&lt;/p&gt;&#xA;&lt;img alt=&quot;The &#x27;XOR&#x27; boolean operation is NOT linearly separable&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/Boolean-XOR.png&quot; class=&quot;HalfWidth&quot; title=&quot;The &#x27;XOR&#x27; boolean operation is NOT linearly separable&quot;&gt;&#xA;&lt;p&gt;This is a case where the data points (where &amp;quot;data&amp;quot; means &amp;quot;all of the input patterns and their corresponding output values&amp;quot;) are not linearly separable. And this means that the simple neural network arrangement that we&#x27;ve seen so far can not produce a trained model that can represent the data. If we tried to train a model in the same way as for AND and OR, the neuron connection weights would go back and forth as the training process kept finding that its &amp;quot;trial-and-improvement&amp;quot; approach continuously came up with at least one wrong result.&lt;/p&gt;&#xA;&lt;p&gt;There is a solution to this, and that is to introduce another layer of neurons into the graph. In our simple network, there are two &amp;quot;layers&amp;quot; of neurons - the &amp;quot;input&amp;quot; neurons on the left and the &amp;quot;output&amp;quot; neuron on the right. What we would need to do here is add a layer in between, which is referred to as a &amp;quot;hidden layer&amp;quot;. A neural network to do this would look something like the following:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/NeuralNetwork-XOR-NoWeights.png&quot; alt=&quot;A neural network with two inputs, one hidden layer with two inputs and one output&quot; title=&quot;A neural network with two inputs, one hidden layer with two inputs and one output&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;To calculate the output value for any pair of input values, more calculations are required now that we have a hidden layer. Whereas before, we only had to multiple Input 1 by the weight that joined it to the Output and add that value to Input 2 multiplied by &lt;em&gt;its&lt;/em&gt; connection we weight, now we have three hidden layer neurons and we have to:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Multiply Input 1&#x27;s value by the weight of its connection to Hidden Input 1 and then add that to Input 2&#x27;s value multiplied by its connection weight to Hidden Input 1 to find Hidden Input 1&#x27;s &amp;quot;initial value&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;Do the same for Input 1 and Input 2 as they connect to Hidden Input 2&lt;/li&gt;&#xA;&lt;li&gt;Apply the sigmoid function for each of the Hidden Input values to ensure that they are between 0 and 1&lt;/li&gt;&#xA;&lt;li&gt;Take Hidden Input 1&#x27;s value multiplied by its connection weight to the output and add that to Hidden Input 2&#x27;s value multiplied by &lt;em&gt;its&lt;/em&gt; connection weight to the Output to find the Output&#x27;s &amp;quot;initial value&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;Apply the sigmoid function to the Output value&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The principle is just the same as when there were only two layers (the Input and Output), except now there are three and we have to take the Input layer and calculate values for the second layer (the Hidden layer) and then use the values there to calculate the value for third and final layer (the Output layer).&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(Note that with this extra layer in the model, it is necessary to apply the sigmoid function after each calculation - we could get away with pretending that it didn&#x27;t exist on the earlier examples but things would fall apart here if kept trying to ignore it)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;The learning process described earlier can be applied here to determine what connection weights to use; start with all connection weights set to random values, calculate the final output for every set of inputs, then adjust the connection weights to try to get closer and repeat until the desired results are achieved.&lt;/p&gt;&#xA;&lt;p&gt;For example, the learning process may result in the following weights being determined as appropriate:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/NeuralNetwork-XOR-WithWeights.png&quot; alt=&quot;A neural network with two inputs, one hidden layer with two inputs and one output - with weights appropriate for XOR&quot; title=&quot;A neural network with two inputs, one hidden layer with two inputs and one output - with weights appropriate for XOR&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;.. which would result in the following calculations occurring for the four sets of inputs (0, 0), (1, 0), (0, 1) and (1, 1) -&lt;/p&gt;&#xA;&lt;div class=&quot;TableScrollWrapper&quot;&gt;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 1&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 2&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Hidden 1 Initial&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Hidden 2 Initial&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Hidden 1 Sigmoid&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Hidden 2 Sigmoid&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Output Initial&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: left;&quot;&gt;Output Sigmoid&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x0.2) &#x2B; (0x0.2)&lt;br&gt;= &lt;strong&gt;0.0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x1) &#x2B; (0x1)&lt;br&gt;= &lt;strong&gt;0.0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0.50&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0.50&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(-3.9x0.50) &#x2B; (3.1x0.50)&lt;br&gt;= &lt;strong&gt;-0.40&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;0.17&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x0.2) &#x2B; (1x0.2)&lt;br&gt;= &lt;strong&gt;0.2&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x1) &#x2B; (0x1)&lt;br&gt;= &lt;strong&gt;1.0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0.69&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0.98&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(-3.9x0.69) &#x2B; (3.1x0.98)&lt;br&gt;= &lt;strong&gt;0.35&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;0.80&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(1x0.2) &#x2B; (0x0.2)&lt;br&gt;= &lt;strong&gt;0.2&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x1) &#x2B; (0x1)&lt;br&gt;= &lt;strong&gt;1.0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0.69&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0.98&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(-3.9x0.69) &#x2B; (3.1x0.98)&lt;br&gt;= &lt;strong&gt;0.35&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;0.80&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(1x0.2) &#x2B; (1x0.2)&lt;br&gt;= &lt;strong&gt;0.4&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(0x1) &#x2B; (0x1)&lt;br&gt;= &lt;strong&gt;2.0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0.83&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1.00&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;(-3.9x0.83) &#x2B; (3.1x1.00)&lt;br&gt;= &lt;strong&gt;-0.14&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;0.36&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&lt;/div&gt;&#xA;&lt;p&gt;Since we&#x27;re considering an output greater than or equal to 0.5 to be equivalent to 1 and an output less than 0.5 to be equivalent to 0, we can see that these weights have given us the outputs that we want:&lt;/p&gt;&#xA;&lt;div class=&quot;TableScrollWrapper&quot;&gt;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 1&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Input 2&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Calculated Output&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: center;&quot;&gt;Expected Output&lt;/th&gt;&#xA;&lt;th style=&quot;text-align: left;&quot;&gt;Is Correct&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;strong&gt;0.17&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (0.17 &amp;lt; 0.5 so consider this 0)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;strong&gt;0.80&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (0.80 &amp;gt;= 0.5 so consider this 1)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;strong&gt;0.80&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (1.80 &amp;gt;= 0.5 so consider this 1)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;1&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;strong&gt;0.36&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;&#xA;&lt;td style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Yes&lt;/strong&gt; (0.36 &amp;lt; 0.5 so consider this 0)&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&lt;/div&gt;&#xA;&lt;h3 id=&quot;how-do-you-know-if-your-data-is-linearly-separable&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#how-do-you-know-if-your-data-is-linearly-separable&quot;&gt;How do you know if your data is linearly separable?&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Or, to put the question another way, &lt;strong&gt;how many layers should your model have??&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;A somewhat flippant response would be that if you try to specify a model that &lt;em&gt;doesn&#x27;t&lt;/em&gt; have a hidden input layer and the training never stops calculating because it can find weights that perfectly match the data then it&#x27;s not linearly separable. While it was easy to see with the AND and OR examples above that a training approach of fiddling with the connection weights between the two input nodes and the output should result in values for the model that calculate the outputs correctly, if we tried to train a model of the same shape (two inputs, one output, no hidden layers) for the XOR case then it would be impossible for the computer to find a combination of weights that would correctly calculate outputs for all of the possible inputs. You could claim that because the training process for the XOR case could never finish that it must not be linearly separable - and this is, sort of, technically, correct. But it&#x27;s not very useful.&lt;/p&gt;&#xA;&lt;p&gt;One reason that it&#x27;s not very useful is that the AND, OR, XOR examples only exist to illustrate how neural networks can be arranged, how they can be trained and how outputs are calculated from the inputs. In the real world, it would be crazy to use a neural network for a tiny amount of fixed data for which all of the outputs are known - where a neural network becomes useful is when you use past data to predict &lt;em&gt;future results&lt;/em&gt;. An example that I&#x27;ve used before is a fictitious history of a manager&#x27;s decisions for feature requests that a team receives:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/ManagerDecisionHistory-Predictions.jpg&quot; alt=&quot;Manager Decision History&quot; title=&quot;Manager Decision History&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;The premise is that every time this manager decides whether to give the green light or not to a feature that has been requested, they consider what strategic importance it has to the company and how much of the work the customer that is requesting it is willing to pay. If it&#x27;s of high strategic importance and the customer expects to receive such value from it that they are willing to pay 100% of the costs of implementation then surely this manager will be delighted to schedule it! If the customer&#x27;s budget is less than what it will cost to implement but the feature has sufficiently high strategic value to the company (maybe it will be a feature that could then be sold to many other customers to almost zero cost to the company or maybe it is an opportunity to address an enormous chunk of technical debt) then it still may get the go-ahead! But if the strategic value is low &lt;em&gt;and&lt;/em&gt; the customer doesn&#x27;t have the budget to cover the entire cost of development then the chances are that it will be rejected.&lt;/p&gt;&#xA;&lt;p&gt;This graph only shows a relatively small number of points and it &lt;em&gt;is&lt;/em&gt; linearly separable. As such, the data points on the graph could be used to train a simple two-input (strategic value on a scale of 0-1 and percentage payable by the customer on a scale of 0-1) and single-output binary classifier (the output is whether the feature gets agreed or rejected) neural network. The hope would be that the data used to train it is indicative of how that manager reacts to incoming requests and so it should be possible to take &lt;em&gt;future&lt;/em&gt; requests and predict whether that manager is likely to take them on.&lt;/p&gt;&#xA;&lt;p&gt;However, neural networks are often intended to be used with huge data sets and whenever there is a large amount of data then there is almost always bound to be some outliers present - results that just seem a little out of keeping with those around them. If you were going to train a model like this with 100,000 previous decisions then you might be satisfied with a model that can correctly is 99.9% accurate, which would mean that out of every 100,00 decisions that it might get 100 of them wrong. If you were trying to train a neural network model using 100,000 sets of historical inputs and outputs then you might decide that the computer can stop its training process when the neuron connection weights that it calculates results in outputs being calculated that are correct in 99.9% of cases, rather than hoping that a success rate of 100% can be achieved. This will have the advantage of finishing slightly more quickly but there&#x27;s always a chance that a couple of those historical input/output entries were written down wrong and, with them included, the data isn&#x27;t linearly separable - but with them excluded, the data &lt;em&gt;is linearly separable&lt;/em&gt;. And so there is a distinction that can be made between whether the &lt;em&gt;entirety&lt;/em&gt; of the data is, strictly speaking, linearly separable and whether a simple model can be trained (without any hidden layers) that is a close enough approximation.&lt;/p&gt;&#xA;&lt;p&gt;The next problem with that simple approach (&amp;quot;if you can&#x27;t use your data to train a model without hidden layers then it&#x27;s not linearly separable&amp;quot;) is that it suggests that adding in hidden layers will automatically mean that a neural network &lt;em&gt;can&lt;/em&gt; be trained with the provided data - which is definitely not correct. Say, for example, that someone believed that this manager would accept or reject feature requests based upon what day of the week it was and what colour tie they were wearing that day. This person could provide historical data for sets of inputs and output - they know that decisions are only made Monday-to-Friday, which so that works itself easily into a 0-1 scale for one input (0 = Monday, 0.2 = Tuesday, etc..) and they have noticed that there are only three colours of tie worn (so a similar numeric value can be associated with each colour). The issue is that there is no correlation between these two inputs and the output, so it&#x27;s extremely unlikely that a computer would be able to train a simple two-input / one-output model but that does &lt;em&gt;not&lt;/em&gt; mean that adding in hidden layers would fix the problem!&lt;/p&gt;&#xA;&lt;p&gt;This conveniently brings us to the subject of &amp;quot;feature selection&amp;quot;. As I touched on earlier, features are measurable aspects of whatever we are trying to make predictions for. The &amp;quot;strategic importance&amp;quot; and &amp;quot;percentage that the customer will pay&amp;quot; were features on the example data before. Feature selection is an important part of machine learning - if you don&#x27;t capture the right information then it&#x27;s unlikely that you&#x27;ll be able to produce something that makes good predictions. When I said before that there could be results in the managerial decision history that don&#x27;t fit a linearly separable model for these two features, maybe it&#x27;s &lt;em&gt;not&lt;/em&gt; because some of the data points were written down wrong; maybe it&#x27;s because other factors were at play. Maybe this manager is taking into account other factors such as an agreement with a customer that they can&#x27;t foot the bill for the entirety of the current feature but they &lt;em&gt;will&lt;/em&gt; contribute significantly to another feature that is of high strategic importance to the company but &lt;em&gt;only&lt;/em&gt; if this first feature is also completed.&lt;/p&gt;&#xA;&lt;p&gt;Capturing another feature in the model (perhaps something that reflects how likely the current feature request is to bring in future valuable revenue) is a case of adding another input neuron. So far, we&#x27;ve only seen networks that have two inputs but that has only been the case because they&#x27;re very easy to talk about and to illustrate as diagrams and to describe calculations for &lt;em&gt;and&lt;/em&gt; to draw graphs for! If a third feature was added then all of the calculation processes are essentially the same (if there are three inputs and one output then the output value is calculated by adding together each of the three input values multiplied by their connection weights) and it would still be possible to visualise, it&#x27;s just that it would be in 3D rather than being a 2D graph. Adding a fourth dimension would mean that it couldn&#x27;t be easily visualised but the maths and the training process would be the same - and this holds for adding a fifth, sixth or hundredth dimension!&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(Other examples of features for the manager decision data might include &amp;quot;team capacity&amp;quot; and &amp;quot;opportunity cost&amp;quot; - would we be sacrificing other, more valuable work if we agree to do this task - and &amp;quot;required timescale for the feature&amp;quot; - is the customer only able to pay for it if it&#x27;s delivered in a certain time frame, after which they would not be willing to contribute? I&#x27;m sure that you wouldn&#x27;t have to think very hard to conjure up features like this that could explain the results that appear to be &amp;quot;outliers&amp;quot; when only the original two proposed features were considered)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;It may well be that adding further relevant features is what makes a data set linearly separable, whereas before - in absence of sufficient information - it wasn&#x27;t. And, while it can be possible to train a neural network using hidden layers such that it can take all of your historical input/output data and calculate neuron connection weights that make the network appear to operate correctly, it may not actually be useful for making &lt;em&gt;future&lt;/em&gt; predictions - where it takes inputs that it hasn&#x27;t seen before and determines an output. If it can&#x27;t do this, then it&#x27;s not actually very useful! A model that is trained to match its historical data but that is poor at making future predictions is said to have been a victim of &amp;quot;overfitting&amp;quot;. A way to avoid that is to split up the historical data into &amp;quot;training data&amp;quot; and &amp;quot;test data&amp;quot; as I&#x27;ll explain in the next section*.&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(I&#x27;ll also finish answering the question about how many layers you should use and how large they should be!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;A final note on feature selection: In general, gathering &lt;em&gt;more&lt;/em&gt; features in your data is going to be better than having &lt;em&gt;fewer&lt;/em&gt;. The weights between neurons in a network represent how important the input is that the weight is associated with - this means that inputs/features that make a larger difference to the final output are going to end up with a higher weight in the trained model than inputs/features that are of lower importance. As such, irrelevant features tend to get ignored and so there is little downside to the final trained model from including them - in fact, there will be many circumstances where you don&#x27;t know beforehand which features are going to be the really important ones and so you may be preventing yourself from training a good model if you exclude data! The downside is that the more inputs that there are, the more calculations need to be performed for each iteration of the &amp;quot;see how good the network is with the current weights and then adjust them accordingly&amp;quot;, which can add up quickly if the amount of historical data being used to train is large. But you may well be happier with a model that took a long time to train into a useful state than you would be with a model that was quick to train but which is terrible at making predictions!&lt;/p&gt;&#xA;&lt;h3 id=&quot;a-classic-example-reading-handwritten-numeric-digits&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#a-classic-example-reading-handwritten-numeric-digits&quot;&gt;A classic example: Reading handwritten numeric digits&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;An extremely well-known data set that is often used in introductions to machine learning is MNIST; the Modified National Institute of Standards and Technology database, which consists of a large number of 28x28 pixel images of handwritten digits and the number that each image is a picture of (making it a collection of &amp;quot;labelled data&amp;quot; as each entry contains input data, in the form of the image, and an output value, which is the number that the image is known to be of).&lt;/p&gt;&#xA;&lt;p&gt;It may seem counterintuitive but those 28x28 pixels can be turned into a flat list of 784 numbers (28 x 28 = 784) and they may be used as inputs for a neural network. The main reason that I think that it may be counterintuitive is that by going from a square of pixels to a one-dimensional list of numbers, you might think that valuable information is being discarded in terms of the structure of the image; surely it&#x27;s important which pixels are above which other pixels or which pixels are to the left of which other pixels? Well, it turns out that discarding this &amp;quot;spatial information&amp;quot; doesn&#x27;t have a large impact on the results!&lt;/p&gt;&#xA;&lt;p&gt;Each 28x28 pixel image is in greyscale and every pixel has a brightness value in the range 0-255, which can easily be scaled down to the range 0-1 by dividing each value by 255.&lt;/p&gt;&#xA;&lt;img alt=&quot;The process of reducing an image of a hand-written digit into a list of numbers&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/MNIST-Vector-Generation.png&quot; class=&quot;NoBorder&quot; title=&quot;The process of reducing an image of a hand-written digit into a list of numbers&quot;&gt;&#xA;&lt;p&gt;For the outputs, this is &lt;em&gt;not&lt;/em&gt; a binary classifier (which is what we&#x27;ve looked at mostly so far); this is a multi-class classifier that has ten outputs because, for any given input image, it should predict whether the digit is 0, 1, etc.. up to 9.&lt;/p&gt;&#xA;&lt;p&gt;The MNIST data (which is readily available for downloading from the internet) has two sets of data - the training data and the test data. Both sets of data are in the exact same format but the training data contains 60,000 labelled images while the test data contains 10,000 labelled images. The idea behind this split is that we can use the training data to train a model that can correctly give the correct output for each of its labelled images and then we confirm that the model is good by running the test data through it. The test data is the equivalent of &amp;quot;future data&amp;quot; that trained neural networks are supposed to be able to make good predictions for - again, if a neural net is great at giving the right answer for data that it has already seen (ie. the data that it was trained with) but it&#x27;s rubbish at making predictions for data that it &lt;em&gt;hasn&#x27;t&lt;/em&gt; seen before then it&#x27;s not a very useful model! Having labelled training data &lt;em&gt;and&lt;/em&gt; test data should help us ensure that we don&#x27;t construct a model that suffers from &amp;quot;overfitting&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;Whereas we have so far mostly been looking at binary classifiers that have a single output (where a value of greater than or equal to 0.5 indicates a &amp;quot;yes&amp;quot; and a value less than 0.5 indicates a &amp;quot;no&amp;quot;), here we want a model with ten output nodes - for each of the possible digits. And the 0-1 value that each output will receive will be a &amp;quot;confidence&amp;quot; value for that output. For example, if our trained network processes and image and the outputs for 0, 1, .. 7 are low (say, 0.1) the outputs for 8 and 9 are both similarly high then it indicates that the model is fairly sure that the image is either an 8 or a 9 but it is not very sure &lt;em&gt;which&lt;/em&gt;. On the other hand, if 0..7 are low (0.1-ish) and 8 is high (say, 0.9) and 9 is in between (0.6 or so) then the model still thinks that 8 or 9 are the most likely results but, in this case, it is much more confident that 8 is the correct output.&lt;/p&gt;&#xA;&lt;p&gt;What we know about the model at this point, for sure, is that there are 784 input neurons (for each pixel in the source data) and 10 output neurons (for each possible label for each image). What we don&#x27;t know is whether we need a hidden layer or not. What we &lt;em&gt;do&lt;/em&gt; know in this case, though, is how we can measure the effectiveness of any model that we train - because we train it using the training data and then see how well that trained model predicts the results of the test data. If our trained model gets 99% of the correct answers when we try running the test data through it then we know that we&#x27;ve done a great job and if we only get 10% of the correct answers for our test data then we&#x27;ve &lt;em&gt;not&lt;/em&gt; got a good model!&lt;/p&gt;&#xA;&lt;p&gt;I said earlier that one way to try to train a model is to take the approach of &amp;quot;start with random weights and see how well it predicts outputs for the training data then adjust weights to improve it and then run through the training data again and then adjust weights to improve it..&amp;quot; iterations until the model either calculates all of the outputs correctly &lt;em&gt;or&lt;/em&gt; gets to within an acceptable range. Well, another way to approach it is to decide how many iterations you&#x27;re willing to do and to just stop the training at that point. Each iteration is referred to as an &amp;quot;epoch&amp;quot; and you might decide that you will run this training process for 500 epochs and then test the model that results against your test data to see how accurate it is. Depending upon your training data, this &lt;em&gt;may&lt;/em&gt; have the advantage that the resulting model will not have an acceptably low error rate but one advantage that it definitely does have is that the training process &lt;em&gt;will&lt;/em&gt; end - whereas if you were trying to train a model that doesn&#x27;t have any hidden layer of neurons and the training data is not linearly separable then the training process would &lt;em&gt;never&lt;/em&gt; end if you were going to let it run until it was sufficiently accurate &lt;em&gt;because it&#x27;s just not possible for a model of that form to be accurate for that sort of data&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;This gives us a better way to answer the question &amp;quot;how many layers should the model have?&amp;quot; because you could start with just an input layer and an output layer, with randomly generated connection weights (as we always start with), have the iterative run-through-the-input-data-and-check-the-outputs-against-the-expected-values-then-try-to-improve-the-weights-slightly-to-get-better-results process run for 500 epochs &lt;em&gt;and then&lt;/em&gt; see how well the resulting model does as handling the test data (for each of the 10k labelled images, run them through the model and count it as a pass if the output that matches the correct digit for the image has the highest value out of all ten of the outputs and count it is a fail if that is not the case).&lt;/p&gt;&#xA;&lt;p&gt;Since we&#x27;re not looking at code here to perform all this work, you&#x27;ll have to take my word for what would happen - this simple model shape would not do very well! Yes, it &lt;em&gt;might&lt;/em&gt; predict the correct digit for some of the inputs but even if a broken clock is right twice a day!&lt;/p&gt;&#xA;&lt;p&gt;This indicates that we need a different model shape and the only other shape that we&#x27;ve seen so far has an additional &amp;quot;hidden layer&amp;quot; between the input layer and the output layer. However, even this introduces some questions - first, if we add a hidden layer then how many neurons should it have in it? The XOR example has two neurons in the input layer, two neurons in the hidden layer and one neuron in the output layer - does this tell us anything about how many we should use here? Secondly, is there any reason why we have to limit ourselves to a single hidden layer? Could there be any advantages to having &lt;em&gt;multiple&lt;/em&gt; hidden layers (say, the input layer, a first hidden layer, a second hidden layer, the output layer)? If so, should each hidden layer have the same number of neurons as the other hidden layers or should they be different sizes?&lt;/p&gt;&#xA;&lt;p&gt;Well, again, on the one hand, we now have a mechanism to try to answer these questions - we could guess that we want a single hidden layer that has 100 neurons (a number that I&#x27;ve completely made up for the sake of an example) and try training that model for 500 epochs* using the training, then see how accurate the resulting model is at predicting the results of the test data. If the accuracy seems acceptable then you could say that you&#x27;ve found a good model! But if you want to see if the accuracy could be improved then you might try repeating the process but with 200 neurons in the hidden layer and trying again - or maybe even reducing it down to 50 neurons in the hidden layer to see how that impacts the results! If you&#x27;re not happy with any of these results then maybe you could try adding a second hidden layer and then playing around with how many neurons are in the first layer &lt;em&gt;and&lt;/em&gt; the second layer!&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(This 500 value is also one that I&#x27;ve just made up for sake of example right now - deciding how many iterations to attempt when training the model may come down to how much training data that you have because the more data that there is to train with, the longer each iteration will take.. so if 500 epochs can be completed in a reasonable amount of time then it could be a good value to use but if it takes an entire day to perform those 500 iterations then maybe a small number would be better!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;One thing to be aware of when adding hidden layers is that the more layers that there are, the more calculations that must be performed as the model is trained. Similarly, the more neurons that there are in the hidden layers, the more calculations there are that must be performed for each training iteration/epoch. If you have a lot of training data then this could be a concern - it&#x27;s tedious to fiddle around with different shapes of models if you have to wait hours (or even days!) each time that you want to train a model in a new configuration. And so erring on the side of fewer layers and few neurons in those layers is a good starting point - if you can get good results from that then you will get over the finish line sooner!&lt;/p&gt;&#xA;&lt;p&gt;To finally offer some concrete advice, I&#x27;m going to quote a &lt;a href=&quot;https://stats.stackexchange.com/questions/181&quot;&gt;Stack Overflow answer&lt;/a&gt; that repeats a rule of thumb that I&#x27;ve read in various other places:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;(i) number of hidden layers equals one;&lt;/p&gt;&#xA;&lt;p&gt;and (ii) the number of neurons in that layer is the mean of the neurons in the input and output layers.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;This advice recommends that we start with a single hidden layer and that it have 397 neurons (which is the average of the number of neurons in the input layer = 28 * 28 pixels = 784 inputs and the number of neurons in the output layer = 10).&lt;/p&gt;&#xA;&lt;img alt=&quot;A few awkward MNIST examples&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/BadMnistDigits.jpg&quot; class=&quot;NoBorder&quot; title=&quot;A few awkward MNIST examples&quot;&gt;&#xA;&lt;p&gt;Using the MNIST training data to train a neural network of this shape (784 inputs, 387 neurons in hidden layer, 10 outputs) across 10 epochs will result in a model that has 95.26%* accuracy when the test data is run through it. Considering what some of these handwritten digits in the test data look like, I think that that is pretty good!&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(I know this because I went through the process of writing the code to do this a few years ago - I was contemplating using it as part of a series of posts about picking up F# if you&#x27;re a C# developer but I ran out of steam.. maybe one day I&#x27;ll pick it back up!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;To try to put this 95.26% accuracy into perspective, the PDF &amp;quot;&lt;a href=&quot;https://arxiv.org/pdf/1905.06684.pdf&quot;&gt;Formal Derivation of Mesh Neural Networks with Their Forward-Only Gradient Propagation&lt;/a&gt;&amp;quot; claims that the MNIST data has an &amp;quot;average human performance of 98.29%&amp;quot;* (though there are &lt;a href=&quot;https://www.reddit.com/r/MachineLearning/comments/2qzibe/comment/cnb84li&quot;&gt;people on Reddit who find this hard to believe and that it is too low&lt;/a&gt;) while the &lt;a href=&quot;https://benchmarks.ai/mnist&quot;&gt;state of the art error rate for MNIST&lt;/a&gt; (where, presumably, the greatest machine learning minds of our time compete) is 0.21%, which indicates an accuracy of 99.71% (if I&#x27;ve interpreted the leaderboard&#x27;s information correctly!).&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(Citing P. Simard, Y. LeCun, and J. Denker, &amp;quot;Efficient pattern recognition using a new transformation distance&amp;quot; in Advances in Neural Information Processing Systems (S. Hanson, J. Cowan, and C. Giles, eds.), vol. 5, pp. 50&#x2013;58, Morgan-Kaufmann, 1993)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If I had trained a model that had TWO hidden layers where, instead of there being a single hidden layer whose neuron count was 1/2 x number-of-inputs-plus-number-of-outputs, the two layers had 2/3 and 1/3 x number-of-inputs-plus-number-of-outputs then the accuracy could be increased to 97.13% - and so the advice above is not something set in stone, it&#x27;s only a guideline or a starting point. But I don&#x27;t want to get too bogged down in this right now as the section just below talks more about multiple hidden layers and about other options, such as pre-processing of data; with machine learning, there can be a lot of experimentation required to get a &amp;quot;good enough&amp;quot; result and you should never expect perfection!&lt;/p&gt;&#xA;&lt;h3 id=&quot;other-shapes-of-network-and-other-forms-of-processing&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#other-shapes-of-network-and-other-forms-of-processing&quot;&gt;Other shapes of network and other forms of processing&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The forms of neural network that I&#x27;ve shown above are the most simple (regardless of how many hidden layers there are - or aren&#x27;t) but there is a whole range of variations that offer tools to potentially improve accuracy and/or reduce the amount of time required to train them. This is a large topic and so I won&#x27;t go deeply into any individual possibility but here are some common variations..&lt;/p&gt;&#xA;&lt;p&gt;Firstly, let&#x27;s go back to thinking about those hidden layers and how many of them that you might want. To quote part of a &lt;a href=&quot;https://qr.ae/pGBUYO&quot;&gt;Quora answer&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;There is a well-known problem of facial recognition, where computer learns to detect human faces. Human face is a complex object, it must have eyes, a nose, a mouth, and to be in a round shape, for computer it means that there are a lot of pixels of different colours that are comprised in different shapes. And in order to decide whether there is a human face on a picture, computer has to detect all those objects.&lt;/p&gt;&#xA;&lt;p&gt;Basically, the first hidden layer detects pixels of light and dark, they are not very useful for face recognition, but they are extremely useful to identify edges and simple shapes on the second hidden layer. The third hidden layer knows how to comprise more complex objects from edges and simple shapes. Finally, at the end, the output layer will be able to recognize a human face with some confidence.&lt;/p&gt;&#xA;&lt;p&gt;Basically, each layer in the neural network gets you farther from the input which is raw pixels and closer to your goal to recognize a human face.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I&#x27;ve seen various explanations that describe hidden layers as being like feature inputs for increasingly specific concepts (with the manager decision example, the two input neurons represented very specific features that we had chosen for the model whereas the suggestion here is that the neurons in each hidden layer represent features extracted from the layer before it - though these features are extracted as a result of the training process and they may not be simple human-comprehendible features, such as &amp;quot;strategic value&amp;quot;).. but I get the impression that this is something of an approximation of what&#x27;s going on. In the face detection example above, we don&#x27;t really know for sure that the third hidden layer really consists of &amp;quot;complex objects from edges and simple shapes&amp;quot; - that is just (so far as I understand it) an approximation to give us a feeling of intuition about what is going on during the training process.&lt;/p&gt;&#xA;&lt;p&gt;It&#x27;s important to note that ALL that is happening during the training is fiddling of neuron connection weights such that the known inputs of the training data get closer to producing the expected outputs in the training data corresponding to those inputs! While we might be able to understand and describe this as a mathematical process (and despite these neural network structures having been inspired by the human brain), we shouldn&#x27;t fool ourselves into believing that this process is &amp;quot;thinking&amp;quot; and analysing information in the same way that we do! I&#x27;ll talk about this a little more in the section &amp;quot;The dark side of machine learning predictions a little further down&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;Another variation used is what &amp;quot;activation function&amp;quot; is used for neurons in each layer. When I described the sigmoid function earlier, which took the sums of the neurons in the previous layer multiplied by their connection weights and then applied a formula to squash that value into the 0-1 range; that was an activation function. In the XOR example earlier, the sigmoid function was used as the activation function for each neuron in the hidden layer &lt;em&gt;and&lt;/em&gt; each neuron in the output layer. But after I did this, I had to look at the final output values and say &amp;quot;if it&#x27;s 0.5 or more then consider it to be a 1 and if it&#x27;s less than 0.5 then consider it to be a 0&amp;quot;.. instead of doing that, I could have used a &amp;quot;step function&amp;quot; for the output layer that would be similar to the sigmoid function but which would have sharp cut off points (for either 0 or 1) instead of a nice smooth curve*.&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(The downside to using a step function for the output of a binary classifier is that you lose any information about how confident the result is if you only get 0 or 1; for example, an output of 0.99 indicates a very confident &amp;gt;= 0.5 result while an output of 0.55 is still &amp;gt;= 0.5 and so indicates a 1 result but it is a less confident result - if the information about the confidence is not important, though, then a step function could have made a lot of sense in the XOR example)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Another common activation function that appears in the literature about neural networks is the &amp;quot;Rectified Linear Unit (ReLU)&amp;quot; function - it&#x27;s way out of the scope of this post to explain why but if you have many hidden layers then you can encounter difficulties if you use the sigmoid function in each layer and the ReLU can ease those woes. If you&#x27;re feeling brave enough to dig in further right now then I would recommend starting with &amp;quot;&lt;a href=&quot;https://machinelearningmastery.com/rectified-linear-activation-function-for-deep-learning-neural-networks/&quot;&gt;A Gentle Introduction to the Rectified Linear Unit (ReLU)&lt;/a&gt;&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/SpatialTransformerNetworks-ExampleTransformations.png&quot; alt=&quot;Example pre-processing transformations of MNIST digits from the &#x27;Spatial Transformer Networks&#x27; paper&quot; title=&quot;Example pre-processing transformations of MNIST digits from the &#x27;Spatial Transformer Networks&#x27; paper&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;Finally, there are times when changing your model isn&#x27;t the most efficient way to improve its accuracy. Sometimes, cleaning up the data can have a more profound effect. For example, there is a paper &lt;a href=&quot;https://proceedings.neurips.cc/paper/2015/file/33ceb07bf4eeb3da587e268d663aba1a-Paper.pdf&quot;&gt;Spatial Transformer Networks (PDF)&lt;/a&gt; that I saw mentioned in &lt;a href=&quot;https://datascience.stackexchange.com/a/27237&quot;&gt;a StackOverflow answer&lt;/a&gt; that will try to improve the quality of input images before using them to train a model or to make a prediction on test data or not-seen-before data.&lt;/p&gt;&#xA;&lt;p&gt;In the case of the MNIST images, it can be seen to locate the area of the image that looks to contain the numeral and to then rotate it and stretch it such that it will hopefully reduce the variation between the many different ways that people write numbers. The PDF describes the improvements in prediction accuracy and also talks about using the same approach to improve recognition of other images such as street view house numbers and even the classification of bird species from images. (Unfortunately, while the StackOverflow answer links to a Google doc with further information about the performance improvements, it&#x27;s a private document that you would have to request access to).&lt;/p&gt;&#xA;&lt;p&gt;The approach to image data processing can also be changed by no longer considering the raw pixels but, instead, deriving some information from them. One example would be to look at every pixel and then see how much lighter or darker it is than its surrounding pixels - this results in a form of edge detection and it can be effective at reducing the effect of light levels in the source image (in the case of a photo) by looking at the &lt;em&gt;changes&lt;/em&gt; in brightness, rather than considering the brightness on a pixel-by-pixel basis. This changes the source data to concentrate more on shapes within the images and less on factors such as colours - which, depending upon the task at hand, may be appropriate (in the case of recognising handwritten digits, for example, whether the number was written in red or blue or green or black shouldn&#x27;t have any impact on the classification process used to predict what number an image contains).&lt;/p&gt;&#xA;&lt;img alt=&quot;Our cat Chunk in two pictures - original and after edge detection processing&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/ChunkEdgeDetection.jpg&quot; class=&quot;AlwaysFullWidth NoBorder&quot; title=&quot;Our cat Chunk in two pictures - original and after edge detection processing&quot;&gt;&#xA;&lt;p&gt;&lt;em&gt;(In my post from a couple of years ago, &amp;quot;&lt;a href=&quot;https://www.productiverage.com/face-or-no-face-finding-faces-in-photos-using-c-sharp-and-accordnet&quot;&gt;Face or no face&lt;/a&gt;&amp;quot;, I was using a different technique than a neural network to train a model to differentiate between images that were faces and ones that weren&#x27;t but I used a similar method of calculating &amp;quot;intensity gradients&amp;quot; from the source data and using that to determine &amp;quot;histograms of gradients (HoGs)&amp;quot; - I won&#x27;t repeat the details here but that approach resulted in a more accurate model AND the HoGs data for each image was smaller than the raw pixel data and so the training process was quicker; double win!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;The next thing to introduce is a &amp;quot;convolutional neural network (CNN)&amp;quot;, which is a variation on the neural network model that adds in &amp;quot;convolution layers&amp;quot; that can perform transformations on the data (a little like the change from raw colour image data to changes-in-brightness data, as shown in the edge detection picture above) though they will actually be capable of all sorts of types of alteration, all with multiple configuration options to tweak how they may be applied. &lt;em&gt;But..&lt;/em&gt;&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;a CNN learns the values of these filters on its own during the training process&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;em&gt;(From the article &amp;quot;&lt;a href=&quot;https://ujjwalkarn.me/2016/08/11/intuitive-explanation-convnets/&quot;&gt;An Intuitive Explanation of Convolutional Neural Networks&lt;/a&gt;&amp;quot;)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;.. and so the training process for this sort of model will not just experiment with changing the weights between neurons to try to improve accuracy, it will also try running the entire process over and over again with variations on the convolutional layers to see if altering their settings can produce better results.&lt;/p&gt;&#xA;&lt;p&gt;To throw another complication into the mix - as I mentioned earlier, the more hidden layers (and the more neurons that each layer has), the more calculations that are required by the training process and so the slower that training a model will be. This is because every input neuron is connected to every neuron in the first hidden layer, then every neuron in the first hidden layer is connected to every neuron in the second hidden layer, etc.. until every neuron in the last hidden layer is connected to every neuron in the output layer - which is why the number of calculations expands massively with each additional layer. These are described as &amp;quot;fully-connected layers&amp;quot;. But there is an alternative; the imaginatively-named approach &amp;quot;&lt;em&gt;sparsely&lt;/em&gt; connected layers&amp;quot;. By having fewer connections, the necessary calculations are fewer and the training time should be shorter. In a neural net, there are commonly a proportion of connections that have a great impact on the accuracy of the training model and a proportion that have a much lower (possibly even zero) effect. Removing these &amp;quot;lower value&amp;quot; connections is what allows us to avoid a lot of calculations/processing time but &lt;em&gt;identifying&lt;/em&gt; these connections is a complex subject. I&#x27;m not even going to attempt to go into any detail in this post about how this may be achieved but if you want to know more about the process of intelligently selecting what connections to use, I&#x27;ll happily direct you to the article &amp;quot;&lt;a href=&quot;https://towardsdatascience.com/the-sparse-future-of-deep-learning-bce05e8e094a%22&quot;&gt;The Sparse Future of Deep Learning&lt;/a&gt;!&lt;/p&gt;&#xA;&lt;p&gt;One &lt;em&gt;final&lt;/em&gt; final note for this section, though: in most cases, more data will get you superior results in comparison to trying to eke out better results from a &amp;quot;cleverer&amp;quot; model that is trained with less data. If you have a model that seems decent and you want to improve the accuracy, if you have the choice between spending time obtaining more quality data (where &amp;quot;quality&amp;quot; &lt;em&gt;is&lt;/em&gt; an important word because &amp;quot;more data&amp;quot; isn&#x27;t actually useful if that data is rubbish!) or spending time fiddling with the model to try to get a few more percentage points of accuracy, generally you will be better to get more data. As Peter Norvig, Google&#x27;s Director of Research, was quoted as saying (in the article &amp;quot;&lt;a href=&quot;https://www.cio.com/article/302119/every-buzzword-was-born-of-data.html&quot;&gt;Every Buzzword Was Born of Data&lt;/a&gt;&amp;quot;):&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;We don&#x27;t have better algorithms. We just have more data.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&quot;the-dark-side-of-machine-learning-predictions&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#the-dark-side-of-machine-learning-predictions&quot;&gt;The dark side of machine learning predictions&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The genius of machine learning is that it can take historical data (what inputs lead to what output) and produce a model that can use that information to make an output prediction for a set of inputs that it&#x27;s never seen before.&lt;/p&gt;&#xA;&lt;p&gt;A big downfall of machine learning is that all it is doing is taking historical data and producing a model that uses that information to predict an output for a set of inputs that it&#x27;s never seen before.&lt;/p&gt;&#xA;&lt;p&gt;In an ideal world, this &lt;em&gt;wouldn&#x27;t&lt;/em&gt; be a downfall because all decisions would have been made fairly and without bias. However.. that is very rarely the case and when a model is trained using historical data, you aren&#x27;t &lt;em&gt;directly&lt;/em&gt; imbuing it with any moral values but it &lt;em&gt;will&lt;/em&gt;, in effect, exhibit any biases in the data that was used to train it.&lt;/p&gt;&#xA;&lt;p&gt;One of the earliest examples that stick in my mind of this was of a handheld camera that had blink detection to try to help you get a shot where everyone has their eyes open. However, the data used to train the model used photos of caucasian people, resulting in an Asian American writing a post &amp;quot;Racist Camera! No, I did not blink... I&#x27;m just Asian!&amp;quot; (as reported on &lt;a href=&quot;https://petapixel.com/2010/01/22/racist-camera-phenomenon-explained-almost/&quot;&gt;PetaPixel&lt;/a&gt;) as the camera &amp;quot;detected&amp;quot; that she was blinking when she wasn&#x27;t.&lt;/p&gt;&#xA;&lt;p&gt;And more horrifying is the article from the same site &amp;quot;&lt;a href=&quot;https://petapixel.com/2015/07/02/google-apologizes-after-photos-app-autotags-black-people-as-gorillas/&quot;&gt;Google Apologizes After Photos App Autotags Black People as &#x27;Gorillas&#x27;&lt;/a&gt;&amp;quot; which arose from Flickr adding an auto-tagging facility that would make suggestions as to what it recognised in your photos. Again, this comes down to the source data that was used to train the models - and it&#x27;s not to suggest that the people sourcing and using this data (which could well be two groups of people; one that collects and tags sets of images and a second group that presumes that that labelled data is sufficiently extensive and representative) are unaware of the biases that it contains. As with life, there are always unconscious biases and the only way to tackle them is to be aware of them and try as best you can to eradicate them!&lt;/p&gt;&#xA;&lt;p&gt;Another example is that, a few years ago, Amazon toyed with introducing a resume-screening process using machine learning - where features were extracted from CVs (the features would be occurrences of a long, known list of words or phrases in this case, as opposed to the numeric values in the manager decision example of the pixel brightnesses in the MNIST example) and the recruitment outcomes (hired / not-hired) from historical data to train a model. However, all did not go to plan. Thankfully, they didn&#x27;t just jump into the deep end and accept the results of the model when they received new CVs; instead, they ran them through the model &lt;em&gt;and&lt;/em&gt; performed the manual checks, to try to get an idea of whether the model was effective or not. I&#x27;m going to take some highlights from the article &amp;quot;&lt;a href=&quot;https://www.businessinsider.com/amazon-built-ai-to-hire-people-discriminated-against-women-2018-10?r=US&amp;amp;IR=T&quot;&gt;Amazon built an AI tool to hire people but had to shut it down because it was discriminating against women&lt;/a&gt;&amp;quot;, so feel free to read that if you want more information. The upshot is that historically there had been many more CVs submitted by men than women, which resulted in there being many more &amp;quot;features&amp;quot; present on male CVs that resulted in a &amp;quot;hire them!&amp;quot; result. As I described before, when a neural network (presuming that Amazon was using such an approach) is presented with many features, it will naturally work out which have a greater impact on the final outcome and the connection weights for these input neurons will be higher. What I didn&#x27;t describe at that point is that the opposite also happens - features that are found to have a &lt;em&gt;negative&lt;/em&gt; impact on the final outcome will not just be given a smaller weight, they will be given a &lt;em&gt;negative&lt;/em&gt; weight. And since this model was effectively learning that men are more likely to be hired and women are less likely, the model that it ended up with gave greater positive weight to features that indicated that the CV was for a male and &lt;em&gt;negative&lt;/em&gt; weight to features that indicated that the CV was for a female, such as a mention of them being a &amp;quot;women&#x27;s chess club captain&amp;quot; or even if they had attended one of two all-women&#x27;s colleges (the name of which had presumably been in the list of known words and phrases that would have been used as features - and which would &lt;em&gt;not&lt;/em&gt; have appeared on any man&#x27;s CV). The developers at Amazon working on this project made changes to try to avoid this issue but they couldn&#x27;t be confident that other biases were not having an effect and so the project was axed.&lt;/p&gt;&#xA;&lt;p&gt;There are, alas, almost certainly always going to be issues with bias when models are trained in this manner - hopefully, it can be reduced as training data sets become more representative of the population (for cases of photographs of people, for example) but it is something that we must always be aware of. I thought that some industries were explicitly banned from applying judgements to their customers using a non-accountable system such as the neural networks that we&#x27;ve been talking about but I&#x27;m struggling to find definitive information. I had it in my head that the UK car insurance market was not allowed to produce prices from a system that isn&#x27;t transparent and accountable (eg. it would not be acceptable to say &amp;quot;we will offer you this price because the computer says so&amp;quot; as opposed to a &amp;quot;decision-tree-based process&amp;quot; where it&#x27;s essentially like a big flow chart that &lt;em&gt;could&lt;/em&gt; be explained in clear English, where the impacts on price for each decision are based on statistics from previous claims) but I&#x27;m also unable to find any articles stating that. In fact, sadly, I find articles such as &amp;quot;&lt;a href=&quot;https://www.bbc.co.uk/news/business-43011882&quot;&gt;Insurers &#x27;risk breaking racism laws&#x27;&lt;/a&gt;&amp;quot; which describe how requesting quotes from some companies, where the only detail that varies between them is the name (a traditionally-sounding white English name compared to another traditional English - but not traditionally &lt;em&gt;white&lt;/em&gt; - name, such as Muhammad Khan), results in wildly different prices being offered.&lt;/p&gt;&#xA;&lt;h3 id=&quot;talking-of-training-neural-nets-with-textual-data&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#talking-of-training-neural-nets-with-textual-data&quot;&gt;Talking of training neural nets with textual data..&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;In all of my descriptions before the previous section, I&#x27;ve been using examples where the inputs to the model as simple numbers -&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Zeros and ones for the AND, OR, XOR cases&lt;/li&gt;&#xA;&lt;li&gt;Numeric 0..1 value ranges for the features of the manager decision example (which were, if we&#x27;re being honest, oversimplifications - can you &lt;em&gt;really&lt;/em&gt; reliably and repeatedly rate the strategic importance to the company of a single feature in isolation? But I digress..)&lt;/li&gt;&#xA;&lt;li&gt;Pixel brightness values for the MNIST example, which are in the range 0-255 and so can easily be reduced down to the 0-1 range&lt;/li&gt;&#xA;&lt;li&gt;I mentioned brightness &lt;em&gt;gradients&lt;/em&gt; (rather than looking at the intensity of individual pixels, looking at how much brighter or darker they are compared to surrounding pixels) and this also results in values that are easy to squeeze into the 0-1 range&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;However, there are all sorts of data that don&#x27;t immediately look like they could be represented as numeric values in the 0-1 range. For example, above I was talking about analysis of CVs and that is purely textual content (ok, there might be the odd image and there might be text content in tables or other layouts but you can imagine how simple text content could be derived from that). There are many ways that this &lt;em&gt;could&lt;/em&gt; be done but one easy way to imagine it would be to:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Take a bunch of documents that you want to train a classifier on (to try to avoid the contentiousness of the CV example, let&#x27;s imagine that it&#x27;s a load of emails and you want to automatically classify them as &amp;quot;spam&amp;quot;, &amp;quot;company newsletter&amp;quot;, &amp;quot;family updates&amp;quot; or one of a few other categories&lt;/li&gt;&#xA;&lt;li&gt;Identify every single unique word across all of the documents and record them in one big &amp;quot;master list&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;Go through each document individually and..&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Split it into individual words again&lt;/li&gt;&#xA;&lt;li&gt;Go through each word in the master list and calculate a score by counting how many times it appears in the current document divided by how many words there are in the document (the smaller that this number is, the less common that it is and, potentially, the more interesting it is in differentiating one document from another)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;For each document, you now have a long list of numbers in the range 0-1 and you could potentially use this list to represent the features of the document&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Each list of numbers is the same length for each document because the same master list of words was used (this is vitally important, as we will see shortly)&lt;/li&gt;&#xA;&lt;li&gt;The list of numbers that is now used to describe a given document is called a &amp;quot;&lt;strong&gt;vector&lt;/strong&gt;&amp;quot;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;One problem here is that the vocabulary used throughout these emails could be very large, meaning that the master list would be very large and the list of numbers for each document equally large. The input layer of a neural network that we would train using this data would have to have as many neurons as there are entries in these lists. This might not necessarily be a problem because it &lt;em&gt;is&lt;/em&gt; possible to train large neural networks - but the larger that they are, the longer that it takes.&lt;/p&gt;&#xA;&lt;p&gt;There are almost certainly going to be lots of words that appear that will have no effect on the classification of an email - words like &amp;quot;a&amp;quot; and &amp;quot;the&amp;quot;, for example. As discussed earlier, this is &lt;em&gt;also&lt;/em&gt; not strictly a problem because features that have little effect on the classification of data will naturally end up with low connection weights in the final trained model. However, it feels wasteful to throw data into the mix that we &lt;em&gt;know&lt;/em&gt; isn&#x27;t going to be useful but is certainly going to slow down the training process by forcing it to perform many more calculations. One approach is to ignore &amp;quot;stop words&amp;quot; when generating the master list of all words in the source data -&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Stopwords are the words in any language which does not add much meaning to a sentence. They can safely be ignored without sacrificing the meaning of the sentence.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;em&gt;(From &amp;quot;&lt;a href=&quot;https://medium.com/@saitejaponugoti/stop-words-in-nlp-5b248dadad47&quot;&gt;Stop Words in NLP&lt;/a&gt;&amp;quot;)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;This is a common approach and lists of stop words are freely available - but it&#x27;s important to be aware that they are language-dependent; English stop words are not the same as French stop words, for example, and you will need to consider this if you&#x27;re not always working with English language data!&lt;/p&gt;&#xA;&lt;p&gt;Another approach is to take advantage of the fact that the calculation above generates smaller values for words that are potentially more relevant (because the word &amp;quot;the&amp;quot; is likely to appear many times in a document, if you divide the number of times that it appears by the total number of words in the document then that value will normally be much higher than if you divided the number of time that a less common word - like &amp;quot;bonus&amp;quot; - appeared in a document by the total number of words in that document). You might decide to discard, say, 20% of the words by looking at which of them appear in the documents with a high score and removing those words from the master list (and removing the corresponding entries from each of the lists of numbers that represents each document). By doing so, you would likely end up removing the stop words without having to have had to start with a known list of stop words to get rid of! (Again, this sort of process is language-dependent because words that are common in one language are different in another language and this sort of approach will work best if all of the documents used as training data are written in the same language).&lt;/p&gt;&#xA;&lt;p&gt;This &lt;em&gt;still&lt;/em&gt; leaves us with many more distinct words than is probably optimal. For example, if the document text is split into words in a very simple manner (such as breaking on whitespace) then you may have an email that is a thrilling update about Mark&#x27;s cat, while another email may be talking about Henry&#x27;s cats - in the context of trying to working out what an email is talking about, is there any meaningful distinction between &amp;quot;cat&amp;quot; (singular) and &amp;quot;cats&amp;quot; (plural)? Probably not. And so adding in a pre-processing step to the act of splitting the text into individual words could help reduce the number of distinct words further by grouping words that are probably equivalent. An example of this is the &lt;a href=&quot;https://www.geeksforgeeks.org/snowball-stemmer-nlp/&quot;&gt;Snowball Stemmer&lt;/a&gt; which performs simple transformations such as lower-casing content, removing punctuation and removing common endings from words so that different forms of verbs are combined into the same word; eg.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Dan&#x27;s cats like going to the park&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;.. becomes&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;dan cat like go to the park&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;(There is unlikely to be any benefit to considering the words &amp;quot;Dan&#x27;s&amp;quot; and &amp;quot;Dan&amp;quot; as different features, just as there is little benefit to considering &amp;quot;cat&amp;quot; and &amp;quot;cats&amp;quot; as unique features).&lt;/p&gt;&#xA;&lt;p&gt;Note that the Snowball Stemmer is also language-dependent and so you need to know what language the text that you&#x27;re working with is in - again, if you&#x27;re lucky enough to be dealing with an entirely English set of training documents then that&#x27;s fine.. though the &lt;strong&gt;nltk.stem.snowball&lt;/strong&gt; Python package used in the link above has support for different languages, so long as you tell it which language you want it to use.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(Shameless plug: Some years ago, I wrote my own interpretation of a Full Text Indexer - which actually powers the site search on this blog! - and core functionality for that include &amp;quot;tokenising&amp;quot; and &amp;quot;token normalising&amp;quot;, which are the splitting of a string of text into individual words and the transformation of those tokens such that equivalent tokens like &amp;quot;Cat&amp;quot; and &amp;quot;cat&amp;quot; and &amp;quot;cats&amp;quot; are all reduced down to a single string; if you wanted to see one way to do this sort of work in C# then you could poke around my &lt;a href=&quot;https://github.com/ProductiveRage/FullTextIndexer&quot;&gt;FullTextIndexer GitHub repo&lt;/a&gt; or read some of the articles listed in my &amp;quot;&lt;a href=&quot;https://www.productiverage.com/the-full-text-indexer-post-roundup&quot;&gt;Full Text Indexer Post Round-up&lt;/a&gt;&amp;quot;.. I would be remiss if I didn&#x27;t mention that there are plenty of other libraries for .NET that have been written in the last ten years that are quite likely more featureful and optimised, but this is my blog and so if I want to link to my own code instead then I will! :))&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;In general, it would make sense to apply the stemming logic before culling the most common words from the list of features, so that you have an accurate picture of what words are true most &amp;quot;interesting&amp;quot; (ie. uncommon).&lt;/p&gt;&#xA;&lt;p&gt;When I described the way that you might approach an MNIST classifier earlier, I explained that you could get a surprisingly accurate model by flattening out the 28x28 pixel image into one long 784 list of values - seemingly discarding structural information about where the pixels appear in relation to each other in two dimensions. What I&#x27;ve described just now, regarding the words-as-features, is similar in some ways as it is also discarding information about how words appear in relation to each other; the features that are produced are all solely based on individual words in isolation.&lt;/p&gt;&#xA;&lt;p&gt;However, with natural language, the words that appear around a given word can make a big difference - the word &amp;quot;new&amp;quot; represents an entirely different concept when it talks about releasing a &amp;quot;new feature&amp;quot; for some software to when it is used in the city name &amp;quot;New York&amp;quot;. As such, depending upon the data and the task in hand, it will often result in a more accurate model if the features extracted from textual content are not only individual words but also phrases, such as adjacent pairs of words (like &amp;quot;New York&amp;quot;) or concurrent runs of three words or even more. Doing so will mean that the number of possible features grows again (it&#x27;s not just a master list of individual words any longer, it&#x27;s now a list of words &lt;em&gt;and&lt;/em&gt; phrases - and one of the decisions that you will have to make is how long you allow the extracted phrases to be. The longer that they can be, the more possible features that you will produce (which will make model-training slower). The shorter that they can be, the fewer features there will be (and so model-training will be faster) but you risk missing out on vital context if the limit is too low (and so you might end up training models relatively quickly but finding that their accuracy is low for the task at hand).&lt;/p&gt;&#xA;&lt;p&gt;With this knowledge, you could now picture a neural network model because you can take the documents in your training data and convert them into vectors (which are, as a reminder, simply lists of numeric values) and each vector will be the same length (ie. it will have the same number of values in it). The length of this vector will determine how large the input layer is in the neural network because there must be as many neurons in the input layer as the document vectors are long (if, say, the master list of words used to generate the document vectors had 10,000 unique entries then each document will have been translated into a vector with 10,000 values in it and the input layer for the neural network will have to have 10,000 nodes).&lt;/p&gt;&#xA;&lt;p&gt;This brings us to an important decision.. we will have to decide how large the document vectors should be - before, I said that you might discard the least interesting 20% of the words but that was just an example figure that I made up on the spot. I started this example by saying that you may wish to classify emails into a particular set of categories but you will have had to decide on what exactly is in that &amp;quot;what type of email is this&amp;quot; list before you can create a model &lt;em&gt;and&lt;/em&gt; you&#x27;ll need to have manually classified every document in your training data before you can start training a neural net to do the same work. The idea behind training a classifier is that it can predict a category for data (eg. an email) that it&#x27;s never seen before but it needs historical labelled data to do that, which is what this pre-categorised training data will be.&lt;/p&gt;&#xA;&lt;p&gt;All of the variables that have been mentioned - the percentage of &amp;quot;non-interesting&amp;quot; words (or &amp;quot;tokens&amp;quot;, as they are often referred to) that are discarded, the maximum number of tokens that may be combined into one to produce larger features (like the &amp;quot;New York&amp;quot; example), the number of outputs (which is the number of categories that emails may be assigned to - which will dictate how many neurons in the output layer; one per category), how many hidden layers that your model should have (and how large those layers should be).. these will all impact the training process and are known as &amp;quot;hyperparameters&amp;quot;. Changing some of them may result in large changes (good &lt;em&gt;or&lt;/em&gt; bad) in the accuracy of the trained model while changing others may not have much impact at all. As I said before, part of working with machine learning is in experimenting with different models and different hyperparameters to see what works well for your data and what doesn&#x27;t!&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Side note:&lt;/strong&gt; The process that I&#x27;ve described above (about producing a &amp;quot;master list&amp;quot; of words and phrases) is a sort of simplified/bastardised of TF-IDF (&amp;quot;term frequency-inverse document frequency&amp;quot; - ie. how often do individual words appear in a document relative to the total number of words in that document), for which there is a nice intro at &amp;quot;&lt;a href=&quot;https://monkeylearn.com/blog/what-is-tf-idf/&quot;&gt;Understanding TF-ID: A Simple Introduction&lt;/a&gt;&amp;quot;. And when I spoke about the process of combining multiple individual words together to produce new tokens (such as &amp;quot;New York&amp;quot;), those new tokens are referred to as &amp;quot;n-grams&amp;quot; - so if you want to find out more about all this then there is plenty of great information out there!&lt;/p&gt;&#xA;&lt;h3 id=&quot;other-types-of-machine-learning&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#other-types-of-machine-learning&quot;&gt;Other types of machine learning&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Almost the entirety of this lengthy post has been about neural network classifiers but it&#x27;s definitely worth mentioning that these are &lt;em&gt;not&lt;/em&gt; the only type of machine learning techniques.&lt;/p&gt;&#xA;&lt;p&gt;The first example that I want to talk about is &amp;quot;document similarity&amp;quot; - say you have 100,000 text documents and you are fairly sure that some of them are very nearly duplicates of each other (maybe one is a technical spec for an audio amplifier from 2019 and another is a revision of that document from 2020 that includes some corrections but is basically the same), how would you find these similar documents? Well, a good starting point would be by using the textual content feature extraction described in the previous section - ie. converting each document into a vector. When that is done, it&#x27;s actually quite simple to come up with an approximate &amp;quot;distance measurement&amp;quot;. If you imagine a load of 2D points on a graph, you can easily measure the distance between any two points by using &lt;a href=&quot;https://en.wikipedia.org/wiki/Pythagorean_theorem&quot;&gt;Pythagorean theorem&lt;/a&gt;; take the horizontal difference between the two points and square it, add that to the vertical difference between them squared, then square root the result. This same approach works for points in a 3D world. And the same principle works for points with &lt;em&gt;any&lt;/em&gt; number of dimensions - and this is how we could imagine our vectors that have been generated from each document, as points in some crazy world with many, &lt;em&gt;many&lt;/em&gt; dimensions. We can measure the distances between pairs of these points and pairs that are relatively close to each other are likely to correspond to documents that are fairly similar in content, while pairs of points that are further away will correspond to documents that are &lt;em&gt;less&lt;/em&gt; similar to each other.&lt;/p&gt;&#xA;&lt;p&gt;Now, this doesn&#x27;t actually involve any machine learning - it&#x27;s just extracting features from documents and then measuring the distances between every single pair of points. If you only have 100 documents then these calculations can probably be performed so quickly that it wouldn&#x27;t pose any sort of problem but if we go back to imagining 100,000 documents (or even imagining millions of documents, if not more!) then calculating the distances between &lt;em&gt;every single pair of documents&lt;/em&gt; in that list could become a gargantuan task (as in, performing the calculations in a &amp;quot;brute force&amp;quot; approach - where every single distance is individually calculated - could potentially take a computer longer to compute than you have left to live, which would be very sad). There are clever machine learning algorithms that work out how to &lt;em&gt;approximate&lt;/em&gt; these calculations such that the brute force approach is not necessary while still ensuring that document similarity measures for all of your data remain attainable (one such algorithm is &amp;quot;&lt;a href=&quot;https://arxiv.org/abs/1603.09320&quot;&gt;Hierarchical Navigable Small World (HNSW)&lt;/a&gt;&amp;quot;, which is way too technical for me to do anything about but mention here in passing). Something interesting to note about this technique is that it is an example of &amp;quot;unsupervised&amp;quot; learning - when we looked at the MNIST (handwritten digit recognition) example earlier, that required that the input data was all labelled so that the machine could learn how the images compared or differed to each other - in this case, though, there is no such labelling required; we just tell the computer to go off and work out on its own what documents look like other documents!&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(Last year, I wrote a post about how I was using a &lt;a href=&quot;https://github.com/curiosity-ai/catalyst&quot;&gt;C# machine learning library&lt;/a&gt; that &lt;a href=&quot;https://curiosity.ai/&quot;&gt;a company that I used to work for&lt;/a&gt; published to automatically generate &amp;quot;You may also be interested in&amp;quot; links for each of my blog posts and that used some of the same techniques described here; the &lt;strong&gt;FastText&lt;/strong&gt; algorithm automates the extraction of features from textual data and &lt;strong&gt;HNSW&lt;/strong&gt; calculates distances between the document vectors - I don&#x27;t actually have anywhere near enough posts on my blog for this to be necessary, I just wanted to try it for fun! If you want to find out more, see &amp;quot;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;Automating suggested / related posts links for my blog&lt;/a&gt;&amp;quot;)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;The next example is another form of binary classifier. I mentioned earlier my post &amp;quot;&lt;a href=&quot;https://www.productiverage.com/face-or-no-face-finding-faces-in-photos-using-c-sharp-and-accordnet&quot;&gt;Face or no face&lt;/a&gt;&amp;quot;, where I wanted to write code that could look at a photo and identify areas in it that appeared to be people&#x27;s faces. There was a bunch of pre-processing to take a colour picture, then a very rough algorithm was run to find areas that might potentially be faces - this would return many false positives (ie. sections of a photo that were &lt;em&gt;not&lt;/em&gt; faces) and I trained a &lt;a href=&quot;https://en.wikipedia.org/wiki/Support-vector_machine&quot;&gt;Support Vector Machine (SVM)&lt;/a&gt; to be able to predict with much greater accuracy whether these image subsections were indeed faces or not. An SVM is trained by giving it a large list of labelled points (where a point doesn&#x27;t have to be 2D or 3D, it can have many dimensions - so its&#x27; vectors again, in other words) and leaving it to try to work out a way to split those points so that all of the points on one side of the line are of one category and all of the points on the other side are of another category. We &lt;em&gt;could&lt;/em&gt; train an SVM with the data from the manager decision example from earlier - its training data would be the historical list of 2D points (where the dimensions represented &amp;quot;strategic value to the company&amp;quot; and &amp;quot;percentage that customer will pay for feature development cost&amp;quot;) and whether the manager gave a yes or no answer and the SVM would try to find a line that splits (or &amp;quot;delineates&amp;quot;) those historical points. In a somewhat comparable way to the training of a neural network, it will pick a line at random, see how well or poorly that line delineates the results, adjusts the line to try to improve the situation and then repeats and repeats until it manages to split the data effectively. The vectors for the face-or-no-face code were much larger than two dimensions and so it&#x27;s no longer a case of finding a 1D line that splits points on a 2D plane; instead, it is (cue impressive-sounding technical terms!) a case of finding an {n-1} dimensional &amp;quot;hyperplane&amp;quot; that splits the {n} dimensional space that the vector points exist in. The training data for the face-or-no-face SVM was derived from the publicly accessible &amp;quot;&lt;a href=&quot;http://www.vision.caltech.edu/Image_Datasets/Caltech_10K_WebFaces/&quot;&gt;Caltech 10, 000 Web Faces&lt;/a&gt;&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;I first mentioned unsupervised classification right near the start of this post, where training data will consist of points that should be arranged into groups of other points that they are most similar to. The problem is that the computer has no way of coming up with a description for what this group represents and so it is less directly useful for classifying into categories as those categories don&#x27;t come with titles! However, it could be feasibly be used by someone like Netflix when they want to come up with creative new categories - they could have the computer extract features from tv shows and films (where the features may actually be extracted from metadata about the programs, such as description or even public reviews) and then have it arrange them into groups, which a Netflix employee could manually poke around in to see if a theme presents itself that could be used as a &lt;a href=&quot;https://www.timeout.com/london/blog/did-you-know-that-netflix-has-hundreds-of-secret-genre-categories-heres-how-to-find-them-051717&quot;&gt;new niche category&lt;/a&gt;. But this still feels quite nebulous and so maybe a concrete example may help. One of the hyperparameters that you will need to set is how many groups you want to be generated, so let&#x27;s go back to the MNIST data and imagine that we want to give a machine-learning algorithm all of the source images and for it to split the data into ten groups (for the digits 0..9) but &lt;em&gt;without&lt;/em&gt; giving it any labels for that source data (in contrast to the supervised learning approach that was described before). Well, one such algorithm is &amp;quot;&lt;a href=&quot;https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding&quot;&gt;t-distributed stochastic neighbour embedding (t-SNE)&lt;/a&gt;&amp;quot; and that can produce results such as the following:&lt;/p&gt;&#xA;&lt;img alt=&quot;t-SNE embeddings for MNIST&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/KyleMcDonald-tSNE-MNIST.jpg&quot; class=&quot;NoBorder&quot; title=&quot;t-SNE embeddings for MNIST&quot;&gt;&#xA;&lt;p&gt;&lt;em&gt;(Reproduced under license terms on &lt;a href=&quot;https://www.flickr.com/photos/kylemcdonald/26620503329&quot;&gt;Kyle McDonald&#x27;s Flickr album page&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;The t-SNE takes vectors with 784 dimensions (since the MNIST data is a set of 28x28 pixel images) as input and returns a vector for each of them that has only &lt;em&gt;two&lt;/em&gt; dimensions, which is how the results can be plotted on the graph above. This is known as &amp;quot;dimension reduction&amp;quot; (since the 2D vectors are essentially approximations of the original 784-dimension vectors). One that image, the labels for each of the MNIST images (ie. whether that image is the digit 0 or 1 or .. 9) are used to determine the colour to draw the point as - &lt;em&gt;but this is only to illustrate the results of the t-SNE algorithm&lt;/em&gt;, those labels were &lt;em&gt;not&lt;/em&gt; used as part of the training. And so it&#x27;s quite amazing just how effective it is at grouping similar digits together! (If there were big groups that were full of intermingled colours then that would indicate a poor job but the fact that the groups are so distinct, with only a few outliers here and there, suggests that it&#x27;s done a fantastic job!) Of course, one part of the reason that it does such a good job of separating the images for each digit is that the hyperparameter specified for the algorithm about the number of groups that it should try to identify is set to 10 - if it had been set to 3 or to 12 (or to anything else) then the groupings wouldn&#x27;t have been so obviously correct.&lt;/p&gt;&#xA;&lt;p&gt;Another unsupervised algorithm that is similar to t-SNE is &amp;quot;Uniform Manifold Approximation and Projection (UMAP)&amp;quot;, which you can find available as a &lt;a href=&quot;https://github.com/curiosity-ai/umap-sharp&quot;&gt;C# implementation&lt;/a&gt; (also published by the company that I worked for that released the machine learning library; &lt;a href=&quot;https://curiosity.ai&quot;&gt;Curiosity&lt;/a&gt;!) in case you are a .NET developer and want to try it out. The &amp;quot;Tester&amp;quot; project in there includes a binary file containing the MNIST image data and it will use this to train a model that groups together the images that it thinks are similar and then generates a bitmap of the results that looks similar to the image shown above. Nothing is forcing this algorithm to reduce the vectors to two dimensions, in case you were wondering - that also is a hyperparameter that is set to train the model. It could be set to 3 and it would generate 3-dimensional vectors that could be plotted in 3D space, rather than the 2D graph above.&lt;/p&gt;&#xA;&lt;h3 id=&quot;so-machine-learning-is-always-the-most-amazing-est-thing-right-except-when-its-evil-and-discriminatory&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/so-what-is-machine-learning-nocodeintro#so-machine-learning-is-always-the-most-amazing-est-thing-right-except-when-its-evil-and-discriminatory&quot;&gt;So machine learning is always the most amazing-est thing, right (except when it&#x27;s evil and discriminatory)?&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I know that I&#x27;ve just written thousands of words espousing the power of machine learning but I did also start the post by giving an example of coding in a &amp;quot;classic approach&amp;quot; - summing up a list of purchases that someone has made, ensuring that the correct tax is included for each of them. This is a simple and predictable process and there would not be any benefit to trying to replace this with a machine learning system. For adding together costs and calculating tax, absolute precision is expected and known predictable rules are in place; machine-trained models will almost always have some level of error (much as we may try to tweak the model to minimise it) and they are very often difficult (if not impossible) to definitively reason about - so if someone disputed a bill that had been generated from a machine-trained model, it would be very difficult to justify why it was correct or not!&lt;/p&gt;&#xA;&lt;p&gt;However, there are some middle grounds where you may be tempted to go one way or the other. The first example that comes to mind is that where I used to work, the CTO took the opportunity to learn how to write a Roslyn analyser by implementing a &amp;quot;Stop commenting out code (delete it if you don&#x27;t need it - we have source control, you know!)&amp;quot; analyser. Roslyn (well, the &amp;quot;&lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/&quot;&gt;.NET Compiler Platform SDK (Roslyn APIs)&lt;/a&gt; if you&#x27;re being pernickety) makes it easy to locate comments and to show warnings in the Error List relating to them if you want to, but how to decide whether the comment text is C# code or whether it&#x27;s a useful explanatory message? He &lt;em&gt;could&lt;/em&gt; have:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Performed a one-off analysis of the code base and extracted all of the comments&lt;/li&gt;&#xA;&lt;li&gt;Taken a random subset of those comments (say 5% - it was a large codebase, so even that might have been too high!)&lt;/li&gt;&#xA;&lt;li&gt;Manually classified each of the comments as &amp;quot;code&amp;quot; or &amp;quot;not code&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;Decided on a text-based feature extraction process to translate each piece of text into a vector, resulting in a list of labelled (as either 0 for &amp;quot;not code&amp;quot; or 1 for &amp;quot;code&amp;quot;) vectors&lt;/li&gt;&#xA;&lt;li&gt;Trained a binary classifier using those labelled vectors (perhaps splitting it up to use 70% of them as training data and 30% as test data)&lt;/li&gt;&#xA;&lt;li&gt;Potentially spent time fiddling with the hyperparameters used in the training until the accuracy of the model was sufficiently high, based upon the results of running the test data through it&lt;/li&gt;&#xA;&lt;li&gt;Exported that classifier model as C# code that the analyser could execute so that it could record warnings against comments that looked they were commented-out code&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Alternatively, he could have come up with a system that tried to guess whether a comment was commented-out code or something useful by splitting it into tokens (ie. individual words and symbols), assigning a score (either positive or negative) to a set of known tokens and then adding up the total for the tokens in the comment. For example, a curly brace symbol may have a positive score since they are much more common in C# code than in English phrases. If the score is greater than a particular threshold then the analyser will decide that it&#x27;s probably commented-out code and record a warning about it.&lt;/p&gt;&#xA;&lt;p&gt;This may not be &lt;em&gt;quite&lt;/em&gt; as easy as you might expect because you can&#x27;t just assign a positive score to every keyword in the C# language otherwise comments like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;// this should be a private class&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;.. might be identified as commented-out code because it contains the keywords &amp;quot;private&amp;quot; and &amp;quot;class&amp;quot;, but that would be wrong! And so there would be a fine line to walk to try to get it right for at least enough of the time.&lt;/p&gt;&#xA;&lt;p&gt;(In case you were wondering, he went with the second option and it ended up working pretty well!)&lt;/p&gt;&#xA;&lt;p&gt;A final example is a project that I only heard about very recently and just thought that it was ingenious! &lt;a href=&quot;https://www.bitboost.com/pawsense/&quot;&gt;PawSense&lt;/a&gt; will &amp;quot;catproof your computer&amp;quot; because:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;When cats walk or climb on your keyboard, they can enter random commands and data, damage your files, and even crash your computer. This can happen whether you are near the computer or have suddenly been called away from it.&lt;/p&gt;&#xA;&lt;p&gt;PawSense is a software utility that helps protect your computer from cats. It quickly detects and blocks cat typing, and also helps train your cat to stay off the computer keyboard.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;This &lt;em&gt;might&lt;/em&gt; sound like the sort of thing that you would somehow try to train a model for because surely the difference between me hammering the keys and a cat jumping on the keyboard and smashing some down could be quite subtle?? Having said that, I don&#x27;t personally have any great intuition for this because the closest that my cats get to this is when they lie down too close to the keyboard and end up leaning down some weight on one of the keys near the edge - they don&#x27;t actually &lt;em&gt;walk&lt;/em&gt; across it. However, the author of this software used some simple observations that are explained in the &lt;a href=&quot;https://www.bitboost.com/pawsense/pawsense-faq.html&quot;&gt;FAQ&lt;/a&gt; on the site, that:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;If you carefully measure cat paws, you will find that practically all cat paws are significantly larger than a typical keyboard key. When a cat first places its paw down, the cat&#x27;s weight plus the momentum of the cat&#x27;s movement exerts pounds of force on the keyboard, primarily through the cat&#x27;s paw pads.&lt;/p&gt;&#xA;&lt;p&gt;The cat&#x27;s paw angles and toe positions also undergo complex changes while the paw lands on the keyboard. This forces keys and often key combinations down in a distinctive style of typing which includes unusual timing patterns.&lt;/p&gt;&#xA;&lt;p&gt;Cats&#x27; patterns of overall movement in walking or lying down also help make their typing more recognizable.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;So simple! And yet, when you read the briefer description from the home page:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;PawSense constantly monitors keyboard activity. PawSense analyzes keypress timings and combinations to distinguish cat typing from human typing.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;.. then you could be forgiven for imagining that something much more convoluted would be required and that somehow the author of this software acquired lots of sample data of him (and other humans) typing in various manners (because keyboard interaction varies hugely depending upon whether you&#x27;re scrolling through a web site or if you&#x27;re typing code or if you&#x27;re writing prose) and compared it to lots of samples of cats interacting with a keyboard. But, thinking about it, trying to record lots of keyboard interactions by cats sounds extremely difficult to me - they like to do what &lt;em&gt;they&lt;/em&gt; want to do and are not likely to be &lt;em&gt;coerced&lt;/em&gt; into keyboard walking if they don&#x27;t already feel in the mood to do so!&lt;/p&gt;&#xA;&lt;p&gt;You can see, then, that it&#x27;s not &lt;em&gt;only&lt;/em&gt; programs that have an extremely simple-looking set of interactions (such as the summing of item costs and taxes) that are better done by writing a traditional algorithm, there are also lots of programs that look like they exhibit &amp;quot;fuzzy&amp;quot; or &amp;quot;intelligent&amp;quot; behaviour where there is no clever machine learning involved, it is simply a result of clever observations and experimentation by whoever designed and wrote the code. However, in cases where machine learning can shine it can be &lt;em&gt;incredibly&lt;/em&gt; powerful - whether that is YouTube keeping you on the site longer &lt;a href=&quot;https://medium.com/techtalkers/how-youtube-knows-what-you-want-to-watch-212a24d79f49#42e0&quot;&gt;by recommending videos to watch&lt;/a&gt;, whether it&#x27;s an automated categorisation of incoming help desk tickets in a large company to try to get customer problems directed to the relevant departments more quickly, whether it&#x27;s keeping spam emails out of your inbox or whether it&#x27;s used to program a cat flap to &lt;a href=&quot;https://www.bbc.co.uk/news/technology-48825761&quot;&gt;not let a cat in if it&#x27;s bringing a &amp;quot;gift&amp;quot; with it&lt;/a&gt; (ie. a dead bird or rodent), it makes possible things that traditional algorithms would make very difficult to do well if they could even be done at all. And while it&#x27;s certainly not in all software, you might be astonished to know just how much directly contains some or interacts with services that do!&lt;/p&gt;&#xA;&lt;div class=&quot;Related&quot;&gt;&lt;h3&gt;You may also be interested in (see &lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;here&lt;/a&gt; for information about how these are generated):&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp&quot;&gt;How are barcodes read?? (Library-less image processing in C#)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/learning-f-sharp-via-some-machine-learning-the-single-layer-percepton&quot;&gt;Learning F# via some Machine Learning: The Single Layer Perceptron&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/face-or-no-face-finding-faces-in-photos-using-c-sharp-and-accordnet&quot;&gt;Face or no face (finding faces in photos using C# and Accord.NET)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
                <pubDate>Mon, 28 Feb 2022 23:44:00 GMT</pubDate>
            </item>
            <item>
                <title>Parallelising (LINQ) work in C#</title>
                <link>https://www.productiverage.com/parallelising-linq-work-in-c-sharp</link>
                <guid>https://www.productiverage.com/parallelising-linq-work-in-c-sharp</guid>
                <description>&lt;h3 id=&quot;tldr&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp#tldr&quot;&gt;TL;DR&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;For computationally-expensive work that can be split up into tasks for LINQ &amp;quot;Select&amp;quot; calls, .NET provides a convenient way to execute this code on multiple threads. This &amp;quot;parallelism&amp;quot; should not be confused with &amp;quot;concurrency&amp;quot;, which is what async / await is for.&lt;/p&gt;&#xA;&lt;h3 id=&quot;a-parallelism-vs-concurrency-summary&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp#a-parallelism-vs-concurrency-summary&quot;&gt;A &amp;quot;parallelism vs concurrency&amp;quot; summary&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Before getting started, I want to nip in the bud any confusion with the differences between code that runs &amp;quot;in parallel&amp;quot; and code that runs &amp;quot;concurrently&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;In short, recruiting a parallelisation strategy for code allows you to:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;use multiple cores simultaneously to work on the same task&lt;/strong&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;..while concurrency allows you to:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;handle multiple tasks on the same core&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;A common example that I like to use is to refer to Node.js because it is a &lt;strong&gt;single-threaded&lt;/strong&gt; environment that supports concurrent execution of &lt;strong&gt;multiple&lt;/strong&gt; requests; each request will call out to external resources such as disk, out-of-process cache, a database, etc.. and it will be non-blocking when it does so, meaning that another request can be processed while it waits for that external resource to reply. So there is only a single thread but multiple overlapping requests can be handled because each time one pauses while it waits, another one can proceed until &lt;em&gt;it&lt;/em&gt; calls an external resource. &lt;strong&gt;One thread / multiple requests&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Parallelising a calculation is kind of the opposite - instead of one thread for multiple requests it tackles one request using multiple threads. This only makes sense when the work to be done is some sort of computation that consists of crunching away on data and &lt;em&gt;not&lt;/em&gt; just waiting for an external resource to reply.&lt;/p&gt;&#xA;&lt;p&gt;When talking about concurrency, it&#x27;s worth noting that in ASP.NET, if there is a lot of load then there might be multiple threads used to process work concurrently - each of the threads will be handling requests that spend most of their time waiting for some async work to complete. This is just like &amp;quot;one thread / multiple requests&amp;quot; but multiplied out to be &lt;strong&gt;&amp;quot;{x} threads / {y} requests&amp;quot; where {x} &amp;lt; {y}&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;For a web server, it is possible that it never makes sense to do work that benefits from being parallelised because that work, by its very nature, is very computationally-expensive and you wouldn&#x27;t want multiple requests to get bogged down in repeating the same costly work. You might require complicated synchronisation mechanisms (to avoid multiple requests doing the same work; instead, having one request do the work while other requests queue up and wait for the result to become available) and maybe you would be better moving that computationally-heavy work off into another service entirely (in which case your web server is back to making async requests as it asks a different server to do the work and send back the result).&lt;/p&gt;&#xA;&lt;h3 id=&quot;a-parallelism-vs-concurrency-example&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp#a-parallelism-vs-concurrency-example&quot;&gt;A &amp;quot;parallelism vs concurrency&amp;quot; example&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;This is what concurrent (aka &amp;quot;async&amp;quot;) work looks likes - if we use &lt;strong&gt;Task.Delay&lt;/strong&gt; to imitate the delay that would be incurred by waiting on an external resource then we can create 50 requests and await the completion of them all like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var items = await Task.WhenAll(&#xA;    Enumerable&#xA;        .Range(0, 50)&#xA;        .Select(async i =&amp;gt;&#xA;        {&#xA;            LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;&#xA;            // Pause for 1, 2, 3, 4, 5 or 6 seconds depending upon the value of i&#xA;            await Task.Delay(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;            LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;            return i;&#xA;        })&#xA;);&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message}&amp;quot;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This work will all complete within about 6s because all it does is create 50 tasks (which it can do near-instantly) where the longest of those has a &lt;strong&gt;Task.Delay&lt;/strong&gt; call of 6s. Whenever one task is waiting, other work is free to continue. This means that all 50 of the tasks may be started using a single thread and that single thread may also be used to jump around receiving each of the results of those tasks.&lt;/p&gt;&#xA;&lt;p&gt;In this example, the &lt;strong&gt;Task.WhenAll&lt;/strong&gt; call creates a 50-element array where each element returns the value of &amp;quot;i&amp;quot; where i is 0-49. These 50 elements will be the 50 tasks&#x27; results, appearing in the array in the same order as they were created. This means that enumerating over the array - when &lt;strong&gt;Task.WhenAll&lt;/strong&gt; says that all of the tasks have completed - will reveal the task results to be in the same order in which they were specified.&lt;/p&gt;&#xA;&lt;p&gt;The 50 results, when the work is coordinated by &lt;strong&gt;Task.WhenAll&lt;/strong&gt;, will be:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;In order&lt;/li&gt;&#xA;&lt;li&gt;Not available for enumeration until &lt;em&gt;all&lt;/em&gt; of them have completed (due to the &amp;quot;&lt;strong&gt;Task.WhenAll&lt;/strong&gt;&amp;quot; call) - all of the &amp;quot;Starting {i}&amp;quot; and &amp;quot;Finished {i}&amp;quot; messages will be displayed before any of the &amp;quot;Received item {item}&amp;quot; message&lt;/li&gt;&#xA;&lt;li&gt;Almost certainly handled by a single thread, across all 50 tasks (this isn&#x27;t guaranteed but it&#x27;s extremely likely to be true)&lt;/li&gt;&#xA;&lt;li&gt;The total running time will be about 6s since there is almost no work involved in starting the tasks, nor receiving the results of the tasks - all that we have to wait for is the time it takes for the longest tasks to complete (which is 6s)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Now, if this code is changed such that the &lt;strong&gt;Thread.Sleep&lt;/strong&gt; is used instead of of &lt;strong&gt;Task.Delay&lt;/strong&gt; then the thread will be blocked as each loop is iterated over. Whereas &lt;strong&gt;Task.Delay&lt;/strong&gt; was used to imitate a call to an external service that would do the work, &lt;strong&gt;Thread.Sleep&lt;/strong&gt; is used to imitate an expensive computation performed by the current thread.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var items = Enumerable&#xA;    .Range(0, 50)&#xA;    .Select(i =&amp;gt;&#xA;    {&#xA;        LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;&#xA;        // Pause for 1, 2, 3, 4, 5 or 6 seconds depending upon the value of i&#xA;        Thread.Sleep(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;        LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;        return i;&#xA;    });&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message}&amp;quot;);&#xA;    &#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Because there is no &lt;strong&gt;Task.WhenAll&lt;/strong&gt; call that requires that &lt;em&gt;every&lt;/em&gt; iteration complete before enumeration can begin, the foreach loop will write out a line as soon as iteration finishes. The results will still be written to the console in the order in which they were defined.&lt;/p&gt;&#xA;&lt;p&gt;Note that this code is neither concurrent &lt;em&gt;not&lt;/em&gt; parallelised.&lt;/p&gt;&#xA;&lt;p&gt;Its behaviour, in comparison to the async example above, is that the results are returned:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;In order&lt;/li&gt;&#xA;&lt;li&gt;Available for enumeration as soon as each iteration completes - so the console messages will always appear as &amp;quot;Starting 1&amp;quot;, &amp;quot;Finished 1&amp;quot;, &amp;quot;Receiving item 1&amp;quot;, &amp;quot;Starting 2&amp;quot;, &amp;quot;Finished 2&amp;quot;, &amp;quot;Receiving item 2&amp;quot;, etc..&lt;/li&gt;&#xA;&lt;li&gt;Handled by a single thread as there is merely the one thread that is processing the loop and blocking on each &lt;strong&gt;Thread.Sleep&lt;/strong&gt; call&lt;/li&gt;&#xA;&lt;li&gt;The total running time is the sum of every &lt;strong&gt;Thread.Sleep&lt;/strong&gt; delay, which is 171s (50 iterations where each sleep call is between 1 and 6s)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;With one simple change, we can alter this code such that the work &lt;em&gt;is&lt;/em&gt; parallelised -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var items = Enumerable&#xA;    .Range(0, 50)&#xA;    .AsParallel() // &amp;lt;- Paralellisation enabled here&#xA;    .Select(i =&amp;gt;&#xA;    {&#xA;        LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;                &#xA;        Thread.Sleep(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;        LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;        return i;&#xA;    });&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message}&amp;quot;);&#xA;    &#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This changes the behaviour considerably (unless you happen to be running this code on a single core machine, which is pretty unusual these days!) because as AsParallel() call allows the 50 iterations to be distributed over multiple cores.&lt;/p&gt;&#xA;&lt;p&gt;My computer has 24 cores and so that means that up to 24 iterations can be run simultaneously - there will be up to 24 threads running and while each of those will be blocked as the &lt;strong&gt;Thread.Sleep&lt;/strong&gt; calls are hit (which, again, are intended to mimic an expensive computation that would tie up a thread), the work will be done much more quickly than when a &lt;em&gt;single&lt;/em&gt; thread had to do all the waiting.&lt;/p&gt;&#xA;&lt;p&gt;When this code is running, there will be many &amp;quot;Starting {i}&amp;quot; messages written out at once and then some &amp;quot;Finished {i}&amp;quot; messages will be written as soon as the first threads complete their current iterations and are ready to move onto another (until all 50 have been processed). It also means that &amp;quot;Received item {item}&amp;quot; messages will be interspersed throughout because enumeration of the list can commence as soon as any of the loops complete.&lt;/p&gt;&#xA;&lt;p&gt;It&#x27;s important to note that the scheduling of the threads should be considered undefined in this configuration and there is no guarantee that you will first see &amp;quot;Starting 1&amp;quot;, followed by &amp;quot;Starting 2&amp;quot;, followed by &amp;quot;Starting 3&amp;quot;. In fact, when I run it, the first messages are as follows:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;15:15:10.423 Starting 3&lt;br&gt;&#xA;15:15:10.423 Starting 9&lt;br&gt;&#xA;15:15:10.423 Starting 15&lt;br&gt;&#xA;15:15:10.423 Starting 16&lt;br&gt;&#xA;15:15:10.423 Starting 11&lt;br&gt;&#xA;15:15:10.423 Starting 20&lt;br&gt;&#xA;15:15:10.423 Starting 5&lt;br&gt;&#xA;15:15:10.423 Starting 19&lt;br&gt;&#xA;15:15:10.423 Starting 6&lt;br&gt;&#xA;15:15:10.423 Starting 0&lt;br&gt;&#xA;15:15:10.423 Starting 12&lt;br&gt;&#xA;15:15:10.423 Starting 17&lt;br&gt;&#xA;15:15:10.423 Starting 23&lt;br&gt;&#xA;15:15:10.423 Starting 1&lt;br&gt;&#xA;15:15:10.423 Starting 14&lt;br&gt;&#xA;15:15:10.423 Starting 2&lt;br&gt;&#xA;15:15:10.423 Starting 10&lt;br&gt;&#xA;15:15:10.423 Starting 22&lt;br&gt;&#xA;15:15:10.423 Starting 18&lt;br&gt;&#xA;15:15:10.423 Starting 4&lt;br&gt;&#xA;15:15:10.423 Starting 13&lt;br&gt;&#xA;15:15:10.423 Starting 21&lt;br&gt;&#xA;15:15:10.423 Starting 7&lt;br&gt;&#xA;15:15:10.423 Starting 8&lt;br&gt;&#xA;15:15:11.437 Finished 18&lt;br&gt;&#xA;15:15:11.437 Finished 0&lt;br&gt;&#xA;15:15:11.437 Finished 12&lt;br&gt;&#xA;15:15:11.437 Finished 6&lt;br&gt;&#xA;15:15:11.437 Starting 24&lt;br&gt;&#xA;15:15:11.437 Starting 25&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;While the starting order is not predictable, the iteration-completion order is somewhat more predictable &lt;em&gt;in this example code&lt;/em&gt; as loops 0, 6, 12, etc.. (ie. every multiple of 6) completes in 1s while every other value of i takes longer.&lt;/p&gt;&#xA;&lt;p&gt;As such, the first &amp;quot;Finished {i}&amp;quot; messages are 18, 0, 12, 6 in the output shown above.&lt;/p&gt;&#xA;&lt;p&gt;The &amp;quot;Received item {item}&amp;quot; messages will be interspersed between &amp;quot;Starting {i}&amp;quot; and &amp;quot;Finished {i}&amp;quot; messages because enumeration of the results &lt;em&gt;can&lt;/em&gt; commence as soon as some of the loops have completed.. however, again, it&#x27;s important to note that the ordering of the results should not be considered to be defined as the scheduling of the threads depends upon how .NET decides to use its &lt;strong&gt;ThreadPool&lt;/strong&gt; to handle the work and how it will &amp;quot;join&amp;quot; the separate threads used for the loop iteration back to the primary thread that the program is running as.&lt;/p&gt;&#xA;&lt;p&gt;That may sound a little confusing, so if we change the code a little bit then maybe it can become clearer:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var items = Enumerable&#xA;    .Range(0, 50)&#xA;    .AsParallel() // &amp;lt;- Paralellisation enabled here&#xA;    .Select(i =&amp;gt;&#xA;    {&#xA;        LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;                &#xA;        Thread.Sleep(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;        LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;        return i;&#xA;    });&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message} &amp;quot; &#x2B; &#xA;                      $&amp;quot;(Thread {Thread.CurrentThread.ManagedThreadId})&amp;quot;);&#xA;    &#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Running this now has those first progress messages look like this:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;15:32:56.683 Starting 8 (Thread 19)&lt;br&gt;&#xA;15:32:56.688 Starting 14 (Thread 16)&lt;br&gt;&#xA;15:32:56.699 Starting 19 (Thread 18)&lt;br&gt;&#xA;15:32:56.692 Starting 17 (Thread 11)&lt;br&gt;&#xA;15:32:56.690 Starting 15 (Thread 14)&lt;br&gt;&#xA;15:32:56.687 Starting 11 (Thread 17)&lt;br&gt;&#xA;15:32:56.685 Starting 9 (Thread 12)&lt;br&gt;&#xA;15:32:56.695 Starting 18 (Thread 21)&lt;br&gt;&#xA;15:32:56.688 Starting 13 (Thread 26)&lt;br&gt;&#xA;15:32:56.703 Starting 21 (Thread 27)&lt;br&gt;&#xA;15:32:56.692 Starting 16 (Thread 25)&lt;br&gt;&#xA;15:32:56.700 Starting 20 (Thread 24)&lt;br&gt;&#xA;15:32:56.683 Starting 6 (Thread 4)&lt;br&gt;&#xA;15:32:56.683 Starting 0 (Thread 7)&lt;br&gt;&#xA;15:32:56.683 Starting 5 (Thread 13)&lt;br&gt;&#xA;15:32:56.683 Starting 1 (Thread 5)&lt;br&gt;&#xA;15:32:56.687 Starting 12 (Thread 10)&lt;br&gt;&#xA;15:32:56.683 Starting 2 (Thread 6)&lt;br&gt;&#xA;15:32:56.683 Starting 4 (Thread 9)&lt;br&gt;&#xA;15:32:56.685 Starting 10 (Thread 22)&lt;br&gt;&#xA;15:32:56.683 Starting 7 (Thread 15)&lt;br&gt;&#xA;15:32:56.706 Starting 22 (Thread 20)&lt;br&gt;&#xA;15:32:56.683 Starting 3 (Thread 8)&lt;br&gt;&#xA;15:32:56.706 Starting 23 (Thread 23)&lt;br&gt;&#xA;15:32:57.722 Finished 18 (Thread 21)&lt;br&gt;&#xA;15:32:57.722 Finished 6 (Thread 4)&lt;br&gt;&#xA;15:32:57.722 Finished 0 (Thread 7)&lt;br&gt;&#xA;15:32:57.722 Finished 12 (Thread 10)&lt;br&gt;&#xA;15:32:57.723 Starting 24 (Thread 21)&lt;br&gt;&#xA;15:32:57.723 Starting 25 (Thread 4)&lt;br&gt;&#xA;15:32:57.723 Starting 26 (Thread 7)&lt;br&gt;&#xA;15:32:57.723 Starting 27 (Thread 10)&lt;br&gt;&#xA;15:32:58.711 Finished 1 (Thread 5)&lt;br&gt;&#xA;15:32:58.711 Finished 7 (Thread 15)&lt;br&gt;&#xA;15:32:58.711 Finished 19 (Thread 18)&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Firstly, note that the &amp;quot;Starting {i}&amp;quot; and &amp;quot;Finished {i}&amp;quot; messages are in a different order again - as I said, the order in which the tasks will be delegated to threads from the &lt;strong&gt;ThreadPool&lt;/strong&gt; should be considered undefined and so you can&#x27;t rely on having each loop started in the same order.&lt;/p&gt;&#xA;&lt;p&gt;Secondly, note that all of those first &amp;quot;Starting {i}&amp;quot; messages are being written from a different thread (19, 16, 18, 11, etc..). But when one of the loops is completed, the thread that processed it becomes free to work on a different iteration and so shortly after we see &amp;quot;Finished 18 (Thread 24)&amp;quot; we see &amp;quot;Starting 25 (Thread 24)&amp;quot; - meaning that one thread (the one with ManagedThreadId 24) finished with loop 18 and then became free to be assigned to start working on loop 25.&lt;/p&gt;&#xA;&lt;p&gt;Scrolling further down the output when I run it on my computer, I can see the first &amp;quot;Receiving item {item}&amp;quot; messages:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;15:33:01.732 Received item 9 (Thread 1)&lt;br&gt;&#xA;15:33:01.732 Received item 42 (Thread 1)&lt;br&gt;&#xA;15:33:01.734 Finished 32 (Thread 21)&lt;br&gt;&#xA;15:33:01.734 Received item 18 (Thread 1)&lt;br&gt;&#xA;15:33:01.742 Received item 24 (Thread 1)&lt;br&gt;&#xA;15:33:01.742 Received item 32 (Thread 1)&lt;br&gt;&#xA;15:33:01.734 Finished 37 (Thread 24)&lt;br&gt;&#xA;15:33:01.734 Finished 27 (Thread 10)&lt;br&gt;&#xA;15:33:01.744 Received item 20 (Thread 1)&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Note that all of the &amp;quot;Received item {item}&amp;quot; messages are being logged by thread 1, which is the thread that the &amp;quot;Main&amp;quot; method of my program started on.&lt;/p&gt;&#xA;&lt;p&gt;Having &amp;quot;AsParallel()&amp;quot; join up its enumeration results such that the enumeration itself can happen on the &amp;quot;primary&amp;quot; thread can be useful because there are some environments that get unhappy if you try to do particular types of work on separate threads - for example, if you wrote an old-school WinForms app and had a separate thread do some work and then try to update a control on your form then you would get an error:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Cross-thread operation not valid. Control accessed from a thread other than the thread it was created on.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;em&gt;(You may be wondering why the &amp;quot;Received item {i}&amp;quot; messages appeared a couple of seconds after the corresponding &amp;quot;Finished {i}&amp;quot; messages, rather than immediately after each loop completed - this is due to buffering of the results and I&#x27;ll touch on this later in this post)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;When &amp;quot;AsParallel()&amp;quot; is used in this way, the characteristics (as compared to the &lt;strong&gt;Task.WhenAll&lt;/strong&gt; async work and to the single-thread work) are that:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;The results are not returned in order&lt;/li&gt;&#xA;&lt;li&gt;Enumeration starts before all of the processing has completed&lt;/li&gt;&#xA;&lt;li&gt;Multiple threads are used (by default, one thread per core in your computer - but, again, there are options for this that I&#x27;ll discuss further down)&lt;/li&gt;&#xA;&lt;li&gt;The total running time depends upon the number of cores you have - if you had 50 cores then every loop iteration would be running simultaneously and it would take about 6s for &lt;em&gt;everything&lt;/em&gt; to complete, as the longest iterations take 6s each (but they would be getting processed simultaneously). If you only had 1 core then you would see the same behaviour as the non-parallelised version above and it would take 171s. On my computer, with 24 cores, it takes around 11s because there are threads that get through the quick iterations until they hit the longer &lt;strong&gt;Thread.Sleep&lt;/strong&gt; calls but there will still be multiple of these slower iterations being processed at the same time.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;If ordering of the results is important then the code can easily be changed like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var items = Enumerable&#xA;    .Range(0, 50)&#xA;    .AsParallel() // &amp;lt;- Paralellisation enabled here&#xA;    .Select(i =&amp;gt;&#xA;    {&#xA;        LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;                &#xA;        Thread.Sleep(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;        LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;        return i;&#xA;    })&#xA;    .OrderBy(i =&amp;gt; i); // &amp;lt;- Ordering enforced here&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message} &amp;quot; &#x2B; &#xA;                      $&amp;quot;(Thread {Thread.CurrentThread.ManagedThreadId})&amp;quot;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Now the work will still be performed on multiple threads at once but enumeration will not be able to start until all of the iterations have completed.&lt;/p&gt;&#xA;&lt;p&gt;This means that the console messages will consist entirely of &amp;quot;Starting {i}&amp;quot; and &amp;quot;Finished {i}&amp;quot; messages until &lt;em&gt;all&lt;/em&gt; 50 iterations are completed, then all of the &amp;quot;Received item {item}&amp;quot; messages will be written out. This will still have the same running time (eg. 11s on my computer) because the work is being performed in the same way - the only difference is that the results are all buffered up until the work is completed, otherwise the &lt;strong&gt;OrderBy&lt;/strong&gt; call wouldn&#x27;t be able to do its job because it couldn&#x27;t know all of the values that were going to be produced.&lt;/p&gt;&#xA;&lt;h3 id=&quot;implementation-details&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp#implementation-details&quot;&gt;Implementation details&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;There are a &lt;em&gt;lot&lt;/em&gt; of options and intricacies that you can find if you dig deep enough into how this works in the .NET library. I have no intention of trying to cover all of them but there are a few options and observations that I think are worth including in this post.&lt;/p&gt;&#xA;&lt;p&gt;The first thing to be aware of is that parallelisation of the work will not be enabled until after the &amp;quot;AsParallel()&amp;quot; call is made - for example, the following code will &lt;em&gt;not&lt;/em&gt; spread the &lt;strong&gt;Thread.Sleep&lt;/strong&gt; calls across multiple cores:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var items = Enumerable&#xA;    .Range(0, 50)&#xA;    .Select(i =&amp;gt;&#xA;    {&#xA;        LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;                &#xA;        Thread.Sleep(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;        LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;        return i;&#xA;    })&#xA;    .AsParallel(); // &amp;lt;- Too late!&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message} &amp;quot; &#x2B; &#xA;                      $&amp;quot;(Thread {Thread.CurrentThread.ManagedThreadId})&amp;quot;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This may seem counterintuitive as the &lt;strong&gt;IEnumerable&lt;/strong&gt; returned from &amp;quot;Select&amp;quot; may be lazily evaluated and so you may expect the runtime to be able to distribute its work over multiple cores due to the &amp;quot;AsParallel()&amp;quot; call after it but this is not the case.&lt;/p&gt;&#xA;&lt;p&gt;To get an idea where parallelisation may occur, there are hints in the method return types - eg. where &amp;quot;Enumerable.Range&amp;quot; returns an &lt;strong&gt;IEnumerable&amp;lt;int&amp;gt;&lt;/strong&gt; and a &amp;quot;Select&amp;quot; call following it will also return an &lt;strong&gt;IEnumerable&amp;lt;int&amp;gt;&lt;/strong&gt;, when there is an &amp;quot;AsParallel&amp;quot; call after &amp;quot;Enumerable.Range&amp;quot; then the type is now a &lt;strong&gt;ParallelQuery&amp;lt;int&amp;gt;int&lt;/strong&gt; and there is a &amp;quot;Select&amp;quot; overload on that type that means that when &amp;quot;Select&amp;quot; is called on a &lt;strong&gt;ParallelQuery&lt;/strong&gt; then that too returns a &lt;strong&gt;ParallelQuery&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;h4 id=&quot;limiting-how-many-cores-may-be-used&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp#limiting-how-many-cores-may-be-used&quot;&gt;Limiting how many cores may be used&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;The default behaviour of &amp;quot;AsParallel()&amp;quot; is to spread the work over as many cores as your computer has available (obviously if there are only 10 work items to distribute and there are 24 cores then it won&#x27;t be able to use &lt;em&gt;all&lt;/em&gt; of your cores but if there are at least as many things to do as there are cores then it will use them all until it starts running out of things).&lt;/p&gt;&#xA;&lt;p&gt;Depending upon your scenario, this may or may not be a good thing. For example, in my previous post (&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2&quot;&gt;Automating &amp;quot;suggested / related posts&amp;quot; links for my blog posts - Part 2&lt;/a&gt;), I spoke about how I&#x27;ve started using the C# machine learning library &lt;a href=&quot;https://github.com/curiosity-ai/catalyst&quot;&gt;Catalyst&lt;/a&gt; (produced by a startup that I used to work at) to suggest &amp;quot;you may be also be interested in&amp;quot; links for the bottom of my posts - in this case, it&#x27;s a one-off task performed before I push an update to my blog live and so I want the computer to spend all of its resources calculating this as fast as possible.&lt;/p&gt;&#xA;&lt;p&gt;One of the applicable lines in the library is in the &lt;a href=&quot;https://en.wikipedia.org/wiki/Tf%E2%80%93idf&quot;&gt;TFIDF&lt;/a&gt; implementation and looks like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;documents.AsParallel().ForAll(doc =&amp;gt; UpdateVocabulary(ExtractTokenHashes(doc)));&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;em&gt;(As you can see in the source file &lt;a href=&quot;https://github.com/curiosity-ai/catalyst/blob/70df89be7b725f6c7786187bd75b2032f287141b/Catalyst/src/Models/Special/TF-IDF.cs#L110&quot;&gt;TF-IDF.cs&lt;/a&gt;; along with the rest of the implementation for if you&#x27;re curious)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;However, I could also imagine that there might be a web server that is serving requests from many people each day but &lt;em&gt;occasionally&lt;/em&gt; there is a request that requires some more intense computation and it might take too long to calculate this while feeling responsive to the User if it tried to do the work on a single thread - &lt;em&gt;but&lt;/em&gt; if it used &lt;em&gt;every&lt;/em&gt; core available on the server then it would impact all of the other requests being handled. In this case it may be appropriate to say &amp;quot;parallelise this work but don&#x27;t allow more than four cores to be utilised&amp;quot;. There is a method &amp;quot;WithDegreeOfParallelism&amp;quot; available for just this purpose!&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var items = Enumerable&#xA;    .Range(0, 50)&#xA;    .AsParallel().WithDegreeOfParallelism(4)&#xA;    .Select(i =&amp;gt;&#xA;    {&#xA;        LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;                &#xA;        Thread.Sleep(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;        LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;        return i;&#xA;    });&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message} &amp;quot; &#x2B; &#xA;                      $&amp;quot;(Thread {Thread.CurrentThread.ManagedThreadId})&amp;quot;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;If the value passed to &amp;quot;WithDegreeOfParallelism&amp;quot; exceeds the number of cores then it will have no effect but if it is less then it will constrain that parallelised work such that it will not use more than that number of cores at any time.&lt;/p&gt;&#xA;&lt;h4 id=&quot;buffering-options&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp#buffering-options&quot;&gt;Buffering options&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;I mentioned earlier that when work is spread over multiple cores using &amp;quot;AsParallel()&amp;quot; and then later enumerated that some buffering of the results occurs. There are three options for the buffering behaviour:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;AutoBuffering&lt;/li&gt;&#xA;&lt;li&gt;FullyBuffered&lt;/li&gt;&#xA;&lt;li&gt;NotBuffered&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The default is &amp;quot;AutoBuffering&amp;quot; and the behaviour of this is that results are not available for enumeration as soon as the work items are completed - instead, the runtime determines a batch size that it thinks makes sense to buffer the results up for before making them available for looping through.&lt;/p&gt;&#xA;&lt;p&gt;To be completely honest, I don&#x27;t know enough about how it decides on this number or the full extent of the benefits of doing so (though I will hint at a way to find out more in the &amp;quot;Partitioner&amp;quot; section further down); I presume that there are some performance benefits to reducing how often execution jumps from one thread to another - because, as we saw earlier, as soon as enumeration commences, execution returns to the &amp;quot;primary thread&amp;quot; and hopping between threads can be a &lt;em&gt;relatively&lt;/em&gt; expensive operation.&lt;/p&gt;&#xA;&lt;p&gt;The second option (&amp;quot;FullyBuffered&amp;quot;) is simple to understand - enumeration will not commence until &lt;em&gt;all&lt;/em&gt; of the work items are completed; they will &lt;em&gt;all&lt;/em&gt; be added to a buffer first. This not only has the disadvantage that enumeration can&#x27;t start until the final item is completed but it also means that all of those results must be held in memory, which could be avoided (if it&#x27;s a concern) by having the results &amp;quot;stream&amp;quot; out as they become ready in the other buffering scenarios. This has the advantage of minimising &amp;quot;thread hops&amp;quot; but, even though the results are all buffered, it does not preserve the order of the work items when it comes to enumeration - despite what I&#x27;ve read elsewhere (you can see this yourself by running the code a little further down).&lt;/p&gt;&#xA;&lt;p&gt;The final option is &amp;quot;NotBuffered&amp;quot; and that, as you can probably tell from the name, doesn&#x27;t buffer results at all and makes the available for enumeration as soon as they have been processed (the disadvantage being the additional cost of changing thread context more frequently - ie. more &amp;quot;thread hops&amp;quot;).&lt;/p&gt;&#xA;&lt;p&gt;To override the default (&amp;quot;AutoBuffering&amp;quot;) behaviour, you may use the &amp;quot;WithMergeOptions&amp;quot; function like this -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var items = Enumerable&#xA;    .Range(0, 50)&#xA;    .AsParallel().WithMergeOptions(ParallelMergeOptions.FullyBuffered)&#xA;    .Select(i =&amp;gt;&#xA;    {&#xA;        LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;                &#xA;        Thread.Sleep(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;        LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;        return i;&#xA;    });&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message} &amp;quot; &#x2B; &#xA;                      $&amp;quot;(Thread {Thread.CurrentThread.ManagedThreadId})&amp;quot;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h4 id=&quot;cancellation&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp#cancellation&quot;&gt;Cancellation&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;Say you have many work items distributed over multiple cores in order to calculate something very expensive and parallelisable. Part way through, you might decide that actually you don&#x27;t want the result any more - maybe some of the data that it relies on has changed and a &amp;quot;stale&amp;quot; result will not be of any use. In this case, you will want to cancel the parallelised work.&lt;/p&gt;&#xA;&lt;p&gt;To enable this, there is a &amp;quot;WithCancellation&amp;quot; method that takes a &lt;strong&gt;CancellationToken&lt;/strong&gt; and will stop allocating work items to threads if the token is marked as cancelled - instead, it will throw an &lt;strong&gt;OperationCanceledException&lt;/strong&gt;. To imitate this, the code below has a token that will be set to be cancelled after 3s and the exception will be thrown during the enumeration:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var cts = new CancellationTokenSource();&#xA;cts.CancelAfter(TimeSpan.FromSeconds(3));&#xA;&#xA;var items = Enumerable&#xA;    .Range(0, 50)&#xA;    .AsParallel().WithCancellation(cts.Token)&#xA;    .Select(i =&amp;gt;&#xA;    {&#xA;        LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;                &#xA;        Thread.Sleep(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;        LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;        return i;&#xA;    });&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message} &amp;quot; &#x2B; &#xA;                      $&amp;quot;(Thread {Thread.CurrentThread.ManagedThreadId})&amp;quot;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It&#x27;s worth noting that &amp;quot;WithCancellation&amp;quot; can only cancel the &amp;quot;AsParallel&amp;quot; work of allocating items to threads, it doesn&#x27;t have any ability to cancel the individual work items themselves. If you want to do this - such that &lt;em&gt;all&lt;/em&gt; work is halted immediately as soon as the token is set to cancelled, then you would have to add cancellation-checking code to the work performed in each step - ie.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var cts = new CancellationTokenSource();&#xA;cts.CancelAfter(TimeSpan.FromSeconds(3));&#xA;&#xA;var items = Enumerable&#xA;    .Range(0, 50)&#xA;    .AsParallel().WithCancellation(cts.Token)&#xA;    .Select(i =&amp;gt;&#xA;    {&#xA;        LogWithTime($&amp;quot;Starting {i}&amp;quot;);&#xA;                &#xA;        cts.Token.ThrowIfCancellationRequested();&#xA;        Thread.Sleep(TimeSpan.FromSeconds((i % 6) &#x2B; 1));&#xA;&#xA;        LogWithTime($&amp;quot;Finished {i}&amp;quot;);&#xA;        return i;&#xA;    });&#xA;&#xA;foreach (var item in items)&#xA;{&#xA;    LogWithTime($&amp;quot;Received item {item}&amp;quot;);&#xA;}&#xA;&#xA;static void LogWithTime(string message) =&amp;gt;&#xA;    Console.WriteLine($&amp;quot;{DateTime.Now:HH:mm:ss.fff} {message} &amp;quot; &#x2B; &#xA;                      $&amp;quot;(Thread {Thread.CurrentThread.ManagedThreadId})&amp;quot;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;em&gt;(Granted, using &amp;quot;cts.Token.ThrowIfCancellationRequested&amp;quot; alongside &amp;quot;Thread.Sleep&amp;quot; isn&#x27;t a perfect example of how to deal with cancellation because you can&#x27;t cancel the &amp;quot;Thread.Sleep&amp;quot; call itself - but hopefully it demonstrates that if you want immediate cancellation of every work item then you need to incorporate cancellation support into each work item as well as calling &amp;quot;WithCancellation&amp;quot; on the &lt;strong&gt;ParallelQuery&lt;/strong&gt;)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;For more detailed information on &amp;quot;PLINQ (parallel LINQ) cancellation&amp;quot;, there is a great article by Reed Copsey Jr entitled &lt;a href=&quot;http://reedcopsey.com/2010/02/17/parallelism-in-net-part-10-cancellation-in-plinq-and-the-parallel-class/&quot;&gt;Parallelism in .NET &#x2013; Part 10, Cancellation in PLINQ and the Parallel class&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h4 id=&quot;partitionertsource&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp#partitionertsource&quot;&gt;Partitioner&amp;lt;TSource&amp;gt;&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;When an &amp;quot;AsParallel&amp;quot; call decides how to split up the work, it uses something called a &amp;quot;Partitioner&amp;quot;. This determines how big the buffer will be when &amp;quot;AutoBuffering&amp;quot; is used and it may even perform other optimisations (up to this point, I&#x27;ve said that &amp;quot;AsParallel&amp;quot; will &lt;em&gt;always&lt;/em&gt; spread the work over multiple cores - so long as you have multiple cores at your disposal and &amp;quot;WithDegreeOfParallelism&amp;quot; doesn&#x27;t specify a value of 1) but, actually, the partitioner could look at the work load and decide that parallelising the work would probably incur more overhead than performing it one step at a time on a single thread and so it &lt;em&gt;won&#x27;t&lt;/em&gt; actually use multiple cores.&lt;/p&gt;&#xA;&lt;p&gt;The .NET library will use its own default Partitioner unless it is told to use a custom one. This is a complex subject matter that:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;I don&#x27;t have a lot of knowledge about&lt;/li&gt;&#xA;&lt;li&gt;I don&#x27;t want to try to add to this article, lest it end up ginormous!&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;If you want to find out more, I recommend starting at the Microsoft documentation about it here: &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/custom-partitioners-for-plinq-and-tpl&quot;&gt;Custom Partitioners for PLINQ and TPL&lt;/a&gt; and also checking out &lt;a href=&quot;https://weblogs.asp.net/dixin/parallel-linq-2-partitioning&quot;&gt;Parallel LINQ in Depth (2) Partitioning&lt;/a&gt; from Dixin&#x27;s Blog (whose blog I also referenced under the &amp;quot;Further reading&amp;quot; section of my &lt;a href=&quot;https://www.productiverage.com/i-didnt-understand-why-people-struggled-with-nets-async&quot;&gt;I didn&#x27;t understand why people struggled with (.NET&#x27;s) async&lt;/a&gt; post).&lt;/p&gt;&#xA;&lt;h3 id=&quot;when-to-parallelise-work-and-when-to-not&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp#when-to-parallelise-work-and-when-to-not&quot;&gt;When to parallelise work (and when to not)&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Much of the time, there is no need for you to try to spread individual tasks over multiple threads. A very common model in this day and age for processing is a web server that is dealing with requests from many Users and most of the time is spent waiting for external caches, file system accesses, database retrievals, etc.. This is not the sort of heavy computation that would lead you to want to try to utilise multiple cores on that web server for any single request.&lt;/p&gt;&#xA;&lt;p&gt;Also, some computational work, even if it&#x27;s expensive, doesn&#x27;t lend itself to parallelisation - if you can&#x27;t split the work into clearly delineated and independent work items then it&#x27;s going to be awkward (if not impossible) to make the work parallelisable. For example, the &lt;a href=&quot;https://en.wikipedia.org/wiki/Fibonacci_number&quot;&gt;Fibonacci Sequence&lt;/a&gt; starts with the numbers 0 and 1 and each subsequent number is the sum of the previous two; so the third number is (0 &#x2B; 1) = 1, the fourth number is (1 &#x2B; 1) = 2, the fifth number is (1 &#x2B; 2) = 3, etc.. In case you&#x27;re not familiar with it and that description is a little confusing, maybe it will help to know that the first ten numbers in the sequence are:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;0&lt;/strong&gt;, &lt;strong&gt;1&lt;/strong&gt;, &lt;strong&gt;1&lt;/strong&gt; (=0&#x2B;1), &lt;strong&gt;2&lt;/strong&gt; (=1&#x2B;1), &lt;strong&gt;3&lt;/strong&gt; (=1&#x2B;2), &lt;strong&gt;5&lt;/strong&gt; (=2&#x2B;3), &lt;strong&gt;8&lt;/strong&gt; (=3&#x2B;5), &lt;strong&gt;13&lt;/strong&gt; (=5&#x2B;8), &lt;strong&gt;21&lt;/strong&gt; (=8&#x2B;13), &lt;strong&gt;34&lt;/strong&gt; (=13&#x2B;21)&lt;/p&gt;&#xA;&lt;p&gt;If you calculate the &lt;em&gt;nth&lt;/em&gt; number like this (based on the previous two) then it&#x27;s near impossible to split the work into big distinct chunks that you could run on different threads and so it wouldn&#x27;t be a good candidate for parallelisation*.&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(If you search Google then you will find that there are people proposing ways to calculate Fibonacci numbers using multiple threads but it&#x27;s much more complicated than working them out the simple way described above, so let&#x27;s forget about that for now so that the Fibonacci sequence works as an easily-understood example of when not to parallelise!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Another thing to bear in mind is that there &lt;em&gt;is&lt;/em&gt; some cost to having the runtime jump around multiple threads, to coordinate what work is done on which and to then join the results all back up on the original thread. For this reason, the ideal use cases are when the main task can be split into fairly large chunks so that the amount of time that each thread spends doing work makes the thread coordination time negligible in comparison.&lt;/p&gt;&#xA;&lt;p&gt;One example is the &lt;strong&gt;TF-IDF&lt;/strong&gt; class that I mentioned earlier where there are a list of documents (blog posts, in my use case) and there is analysis required on each one to look for &amp;quot;interesting&amp;quot; words:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;documents.AsParallel().ForAll(doc =&amp;gt; UpdateVocabulary(ExtractTokenHashes(doc)));&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Another example is something that I was tinkering with some months ago and which I&#x27;m hoping to write some blog posts about when I can motivate myself! A few years ago, I gave a tech talk to a local group that was recorded but the camera was out of focus for most of the video and so the slides are illegible. I&#x27;ve still got the slide deck that I prepared for the talk and so I can produce images of those in full resolution - which gave me the idea of analysing the frames of the original video and trying to determine which slide should be shown on which frame and then superimposing a clear version of the slide onto the blurry images (then creating a new version of the video with the original audio, the original blurry view of me but super-clear slide contents). Some of the steps involved in this are:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Load all of the original slide images and translate their pixel data into a form that will make comparisons easier for the code later on&lt;/li&gt;&#xA;&lt;li&gt;Look at every frame of the video and look for the brightest area on the image and hope that that is the projection of the slide (it will be a quadrilateral but not a rectangle, due to perspective of the wall onto which the slides were projected)&lt;/li&gt;&#xA;&lt;li&gt;Load every frame of the video, extract the content that is in the &amp;quot;brightest area&amp;quot; that appears most commonly throughout the slides (it varies a little from slide to slide, depending upon how out of focus the camera was at the time), stretch the area back into a simple rectangle (reversing the effect of perspective), translate the pixel data into the same format as the original slides were converted into earlier and then try to find the closest match&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Each of these steps lends itself to parallelisation because the work performed on each frame may be done in isolation and the work itself is sufficiently computationally expensive that the task of coordinating the work between threads can basically be considered to be zero in comparison.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(If you&#x27;re just absolutely desperate to know more about this still-slightly-rough-around-the-edges project, you can find it on my GitHub account under &lt;a href=&quot;https://github.com/ProductiveRage/NaivePerspectiveCorrection&quot;&gt;NaivePerspectiveCorrection&lt;/a&gt; - like I said, I hope to write some more posts about it in the coming months but, until then, you can see some sensible uses of &amp;quot;AsParallel()&amp;quot; in &lt;a href=&quot;https://github.com/ProductiveRage/NaivePerspectiveCorrection/blob/4cf5222282ea3c948d454af83eafcfdc274c155f/NaivePerspectiveCorrection/Program.cs&quot;&gt;Program.cs&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&quot;Related&quot;&gt;&lt;h3&gt;You may also be interested in:&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one&quot;&gt;Finding the brightest area in an image with C# (fixing a blurry presentation video - part one)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/i-didnt-understand-why-people-struggled-with-nets-async&quot;&gt;I didn&amp;#x27;t understand why people struggled with (.NET&amp;#x27;s) async&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/entity-framework-projections-to-immutable-types-ienumerable-vs-iqueryable&quot;&gt;Entity Framework projections to Immutable Types (IEnumerable vs IQueryable)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
                <pubDate>Tue, 10 Aug 2021 08:01:00 GMT</pubDate>
            </item>
            <item>
                <title>Automating &quot;suggested / related posts&quot; links for my blog posts - Part 2</title>
                <link>https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2</link>
                <guid>https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2</guid>
                <description>&lt;h3 id=&quot;tldr&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2#tldr&quot;&gt;TL;DR&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;By training another type of model from the open source .NET library that I&#x27;ve been using and combining its results with the similarity model from last time (see &lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;Automating &amp;quot;suggested / related posts&amp;quot; links for my blog posts&lt;/a&gt;), I&#x27;m going to improve the automatically-generated &amp;quot;you may be interested in&amp;quot; links that I&#x27;m adding to my blog.&lt;/p&gt;&#xA;&lt;p&gt;Improvement, in fact, sufficient such that I&#x27;ll start displaying the machine-suggested links at the bottom of each post.&lt;/p&gt;&#xA;&lt;h3 id=&quot;where-i-left-off-last-time&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2#where-i-left-off-last-time&quot;&gt;Where I left off last time&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;In my last post, I had trained a &lt;a href=&quot;https://en.wikipedia.org/wiki/FastText&quot;&gt;fastText&lt;/a&gt; model (as part of the &lt;a href=&quot;https://github.com/curiosity-ai/catalyst&quot;&gt;Catalyst .NET library&lt;/a&gt;) by having it read all of my blog posts so that it could predict which posts were most likely to be similar to which other posts.&lt;/p&gt;&#xA;&lt;p&gt;This came back with some excellent suggestions, like this:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Learning F# via some Machine Learning: The Single Layer Perceptron&lt;/strong&gt;&lt;br&gt;&#xA;How are barcodes read?? (Library-less image processing in C#)&lt;br&gt;&#xA;Writing F# to implement &#x27;The Single Layer Perceptron&#x27;&lt;br&gt;&#xA;Face or no face (finding faces in photos using C# and AccordNET)&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;.. but it also produced some less good selections, like this:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Simple TypeScript type definitions for AMD modules&lt;/strong&gt;&lt;br&gt;&#xA;STA ApartmentState with ASP.Net MVC&lt;br&gt;&#xA;WCF with JSON (and nullable types)&lt;br&gt;&#xA;The joys of AutoMapper&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I&#x27;m still not discounting the idea that I might be able to improve the results by tweaking hyperparameters on the training model (such as epoch, negative sampling rate and dimensions) or maybe even changing how it processes the blog posts - eg. it&#x27;s tackling the content as English language documents but there are large code segments in many of the posts and maybe that&#x27;s confusing it; maybe removing the code samples before processing would give better results?&lt;/p&gt;&#xA;&lt;p&gt;However, fiddling with those options and rebuilding over and over is a time-consuming process and there is no easy way to evaluate the &amp;quot;goodness&amp;quot; of the results - so I need to flick through them all myself and try to get a rough feel for whether I think the last run was an improvement or not.&lt;/p&gt;&#xA;&lt;h3 id=&quot;introducing-a-new-model&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2#introducing-a-new-model&quot;&gt;Introducing a new model&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The premise that I wil be experimenting with is to determine what words in my post titles are &amp;quot;interesting&amp;quot; and to then order the suggested-similar posts first by a score based upon how many interesting words they share &lt;em&gt;and then&lt;/em&gt; by the similarity score that I already have.&lt;/p&gt;&#xA;&lt;p&gt;The model that I&#x27;ll be training for this is called &amp;quot;TF-IDF&amp;quot; or &amp;quot;Term Frequency - Inverse Document Frequency&amp;quot; and it looks at every word in every blog post and considers how many times that word appears in the document (the more often, the more likely that the document relates to the word) and how many times it appears across multiple documents (the more often, the more common and less &amp;quot;specific&amp;quot; it&#x27;s likely to be).&lt;/p&gt;&#xA;&lt;p&gt;For each blog post that I&#x27;m looking for similar posts to, I&#x27;ll:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;take the words from its title&lt;/li&gt;&#xA;&lt;li&gt;take the words from another post&#x27;s title&lt;/li&gt;&#xA;&lt;li&gt;add together all of the TF-IDF scores for words that appear in both titles (the higher the score for each word, the greater the relevance)&lt;/li&gt;&#xA;&lt;li&gt;repeat until all other post titles have been compared&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Taking the example from above that didn&#x27;t have particularly good similar-post recommendations, the words in its title will have the following scores:&lt;/p&gt;&#xA;&lt;div class=&quot;TableScrollWrapper&quot;&gt;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Word&lt;/th&gt;&#xA;&lt;th&gt;Score&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Simple&lt;/td&gt;&#xA;&lt;td&gt;0.6618375&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;TypeScript&lt;/td&gt;&#xA;&lt;td&gt;4.39835453&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;type&lt;/td&gt;&#xA;&lt;td&gt;0.7873714&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;definitions&lt;/td&gt;&#xA;&lt;td&gt;2.60178781&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;for&lt;/td&gt;&#xA;&lt;td&gt;0&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;AMD&lt;/td&gt;&#xA;&lt;td&gt;3.81998682&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;modules&lt;/td&gt;&#xA;&lt;td&gt;3.96386051&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&lt;/div&gt;&#xA;&lt;p&gt;.. so it should be clear that any other titles that contain the word &amp;quot;TypeScript&amp;quot; will be given a boost.&lt;/p&gt;&#xA;&lt;p&gt;This is by no means a perfect system as there will often be posts whose main topics are similar but whose titles are not. The example from earlier that fastText generated really good similar-post suggestions for is a great illustration of this:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Learning F# via some Machine Learning: The Single Layer Perceptron&lt;/strong&gt;&lt;br&gt;&#xA;How are barcodes read?? (Library-less image processing in C#)&lt;br&gt;&#xA;Writing F# to implement &#x27;The Single Layer Perceptron&#x27;&lt;br&gt;&#xA;Face or no face (finding faces in photos using C# and AccordNET)&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;All of them are investigations into some form of machine learning or computer vision but the titles share very little in common. It&#x27;s likely that the prediction quality of this one will actually suffer a little with the change I&#x27;m introducing but I&#x27;m looking for an overall improvement, across the entire blog. I&#x27;m also not looking for a perfect general solution, I&#x27;m trying to find something that works well for &lt;em&gt;my&lt;/em&gt; data (again, bearing in mind that there is a relatively small quantity of it as there are only around 120 posts, which doesn&#x27;t give the computer a huge amount of data to work from).&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(It&#x27;s also worth noting that the way I implement this in my blog is that I maintain two lists - the manually-curated list that I had before that had links for about a dozen posts and a machine-generated list; if there are manual links present then they will be displayed and the auto-generated ones will be hidden - so if I find that I have a particularly awkward post where the machine can&#x27;t find nice matches then I can always tidy it up myself by manually creating the related-post links for that post)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;implementation&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2#implementation&quot;&gt;Implementation&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;Last time&lt;/a&gt;, I had code that was reading and parsing my blog posts into a &amp;quot;postsWithDocuments&amp;quot; list.&lt;/p&gt;&#xA;&lt;p&gt;After training the fastText model, I&#x27;ll train a TF-IDF model on all of the documents. I&#x27;ll then go back round each document again, have this new model &amp;quot;Process&amp;quot; them and retrieve Frequency values for each word. These values allow for a score to be generated - since the scores depend upon how often a word appears in a given document, the scores will vary from one blog post to another and so I&#x27;m taking an average score for each distinct word.&lt;/p&gt;&#xA;&lt;p&gt;(Confession: I&#x27;m not 100% sure that this averaging is the ideal approach here but it seems to be doing a good enough job and I&#x27;m only fiddling around with things, so &lt;em&gt;good enough&lt;/em&gt; should be all that I need)&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Console.WriteLine(&amp;quot;Training TF-IDF model..&amp;quot;);&#xA;var tfidf = new TFIDF(pipeline.Language, version: 0, tag: &amp;quot;&amp;quot;);&#xA;await tfidf.Train(postsWithDocuments.Select(postWithDocument =&amp;gt; postWithDocument.Document));&#xA;&#xA;Console.WriteLine(&amp;quot;Getting average TF-IDF weights per word..&amp;quot;);&#xA;var tokenValueTFIDF = new Dictionary&amp;lt;string, List&amp;lt;float&amp;gt;&amp;gt;(StringComparer.OrdinalIgnoreCase);&#xA;foreach (var doc in postsWithDocuments.Select(postWithDocument =&amp;gt; postWithDocument.Document))&#xA;{&#xA;    // Calling &amp;quot;Process&amp;quot; on the document updates data on the tokens within the document&#xA;    // (specifically, the token.Frequency value)&#xA;    tfidf.Process(doc);&#xA;    foreach (var sentence in doc)&#xA;    {&#xA;        foreach (var token in sentence)&#xA;        {&#xA;            if (!tokenValueTFIDF.TryGetValue(token.Value, out var freqs))&#xA;            {&#xA;                freqs = new();&#xA;                tokenValueTFIDF.Add(token.Value, freqs);&#xA;            }&#xA;            freqs.Add(token.Frequency);&#xA;        }&#xA;    }&#xA;}&#xA;var averagedTokenValueTFIDF = tokenValueTFIDF.ToDictionary(&#xA;    entry =&amp;gt; entry.Key,&#xA;    entry =&amp;gt; entry.Value.Average(), StringComparer.OrdinalIgnoreCase&#xA;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Now, with a couple of helper methods:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static float GetProximityByTitleTFIDF(&#xA;    string similarPostTitle,&#xA;    HashSet&amp;lt;string&amp;gt; tokenValuesInInitialPostTitle,&#xA;    Dictionary&amp;lt;string, float&amp;gt; averagedTokenValueTFIDF,&#xA;    Pipeline pipeline)&#xA;{&#xA;    return GetAllTokensForText(similarPostTitle, pipeline)&#xA;        .Where(token =&amp;gt; tokenValuesInInitialPostTitle.Contains(token.Value))&#xA;        .Sum(token =&amp;gt;&#xA;        {&#xA;            var tfidfValue = averagedTokenValueTFIDF.TryGetValue(token.Value, out var score)&#xA;                ? score&#xA;                : 0;&#xA;            if (tfidfValue &amp;lt;= 0)&#xA;            {&#xA;                // Ignore any tokens that report a negative impact (eg. punctuation or&#xA;                // really common words like &amp;quot;in&amp;quot;)&#xA;                return 0;&#xA;            }&#xA;            return tfidfValue;&#xA;        });&#xA;}&#xA;&#xA;private static IEnumerable&amp;lt;IToken&amp;gt; GetAllTokensForText(string text, Pipeline pipeline)&#xA;{&#xA;    var doc = new Document(text, pipeline.Language);&#xA;    pipeline.ProcessSingle(doc);&#xA;    return doc.SelectMany(sentence =&amp;gt; sentence);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;.. it&#x27;s possible, for any given post, to sort the titles of the other posts according to how many &amp;quot;interesting&amp;quot; words (and &lt;em&gt;how&lt;/em&gt; &amp;quot;interesting&amp;quot; they are) they have in common like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;// Post 82 on my blog is &amp;quot;Simple TypeScript type definitions for AMD modules&amp;quot;&#xA;var post82 = postsWithDocuments.Select(p =&amp;gt; p.Post).FirstOrDefault(p =&amp;gt; p.ID == 82);&#xA;var title = post82.Title;&#xA;&#xA;var tokenValuesInTitle =&#xA;    GetAllTokensForText(NormaliseSomeCommonTerms(title), pipeline)&#xA;        .Select(token =&amp;gt; token.Value)&#xA;        .ToHashSet(StringComparer.OrdinalIgnoreCase);&#xA;&#x9;&#x9;&#xA;var others = postsWithDocuments&#xA;    .Select(p =&amp;gt; p.Post)&#xA;    .Where(p =&amp;gt; p.ID != post82.ID)&#xA;    .Select(p =&amp;gt; new&#xA;    {&#xA;        Post = p,&#xA;        ProximityByTitleTFIDF = GetProximityByTitleTFIDF(&#xA;            NormaliseSomeCommonTerms(p.Title),&#xA;            tokenValuesInTitle,&#xA;            averagedTokenValueTFIDF,&#xA;            pipeline&#xA;        )&#xA;    })&#xA;    .OrderByDescending(similarResult =&amp;gt; similarResult.ProximityByTitleTFIDF);&#xA;&#x9;&#xA;foreach (var result in others)&#xA;    Console.WriteLine($&amp;quot;{result.ProximityByTitleTFIDF:0.000} {result.Post.Title}&amp;quot;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The top 11 scores (after which, everything has a TF-IDF proximity score of zero) are these:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;7.183 Parsing TypeScript definitions (functional-ly.. ish)&lt;br&gt;&#xA;4.544 TypeScript State Machines&lt;br&gt;&#xA;4.544 Writing React components in TypeScript&lt;br&gt;&#xA;4.544 TypeScript classes for (React) Flux actions&lt;br&gt;&#xA;4.544 TypeScript / ES6 classes for React components - without the hacks!&lt;br&gt;&#xA;4.544 Writing a Brackets extension in TypeScript, in Brackets&lt;br&gt;&#xA;0.796 A static type system is a wonderful message to the present and future&lt;br&gt;&#xA;0.796 A static type system is a wonderful message to the present and future - Supplementary&lt;br&gt;&#xA;0.796 Type aliases in Bridge.NET (C#)&lt;br&gt;&#xA;0.796 Hassle-free immutable type updates in C#&lt;br&gt;&#xA;0.000 I love Immutable Data&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;So the idea is to then use the fastText similarity score when deciding which of these matches is best.&lt;/p&gt;&#xA;&lt;p&gt;There are all sorts of ways that these two scoring mechanisms could be combined - eg. I could take the 20 titles with the greatest TF-IDF proximity scores and then order them by similarity (ie. which results the fastText model thinks are best) or I could reverse it and take the 20 titles that fastText thought were best and &lt;em&gt;then&lt;/em&gt; take the three with the greatest TF-IDF proximity scores from within those. For now, I&#x27;m using the simplest approach and ordering by the TF-IDF scores first and then by the fastText similarity model. So, from the above list, the 7.183-scoring post will be taken first and then 2 out of the 5 posts that have a TF-IDF score of 4.544 will be taken, according to which ones the fastText model thought were more similar.&lt;/p&gt;&#xA;&lt;p&gt;Again, there are lots of things that could be tweaked and fiddled with - and I imagine that I will experiment with them at some point. The main problem is that I have enough data across my posts that it&#x27;s tedious looking through the output to try to decide if I&#x27;ve improved things each time I make change but there &lt;em&gt;isn&#x27;t&lt;/em&gt; enough data that the algorithms have a huge pile of information to work on. Coupled with the fact that training takes a few minutes to run and I have recipe for frustration if I obsess too much about it. Right now, I&#x27;m happy enough with the suggestions and any that I want to manually override, I can do so easily.&lt;/p&gt;&#xA;&lt;h3 id=&quot;trying-the-code-yourself&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2#trying-the-code-yourself&quot;&gt;Trying the code yourself&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;If you want to try out the code, you can find a complete sample in the &amp;quot;SimilarityWithTitleTFIDF&amp;quot; project in the solution of this repo: &lt;a href=&quot;https://github.com/ProductiveRage/BlogPostSimilarity&quot;&gt;BlogPostSimilarity&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&quot;has-it-helped&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2#has-it-helped&quot;&gt;Has it helped?&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Let&#x27;s return to those examples that I started with.&lt;/p&gt;&#xA;&lt;p&gt;Good suggestions from last time:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Learning F# via some Machine Learning: The Single Layer Perceptron&lt;/strong&gt;&lt;br&gt;&#xA;How are barcodes read?? (Library-less image processing in C#)&lt;br&gt;&#xA;Writing F# to implement &#x27;The Single Layer Perceptron&#x27;&lt;br&gt;&#xA;Face or no face (finding faces in photos using C# and AccordNET)&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;em&gt;Less&lt;/em&gt; good suggestions:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Simple TypeScript type definitions for AMD modules&lt;/strong&gt;&lt;br&gt;&#xA;STA ApartmentState with ASP.Net MVC&lt;br&gt;&#xA;WCF with JSON (and nullable types)&lt;br&gt;&#xA;The joys of AutoMapper&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Now, the not-very-good one has improved and has these offered:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Simple TypeScript type definitions for AMD modules&lt;/strong&gt;&lt;br&gt;&#xA;Parsing TypeScript definitions (functional-ly.. ish)&lt;br&gt;&#xA;TypeScript State Machines&lt;br&gt;&#xA;Writing a Brackets extension in TypeScript, in Brackets&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;.. but, as I said before, the good suggestions are now not &lt;em&gt;as&lt;/em&gt; good as they were:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;How are barcodes read?? (Library-less image processing in C#)&lt;/strong&gt;&lt;br&gt;&#xA;Face or no face (finding faces in photos using C# and Accord.NET)&lt;br&gt;&#xA;Implementing F#-inspired &amp;quot;with&amp;quot; updates for immutable classes in C#&lt;br&gt;&#xA;A follow-up to &amp;quot;Implementing F#-inspired &#x27;with&#x27; updates in C#&amp;quot;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;There are lots of suggestions that &lt;em&gt;are&lt;/em&gt; still very good - eg.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Creating a C# (&amp;quot;Roslyn&amp;quot;) Analyser - For beginners by a beginner&lt;/strong&gt;&lt;br&gt;&#xA;Using Roslyn to identify unused and undeclared variables in VBScript WSC components&lt;br&gt;&#xA;Locating TODO comments with Roslyn&lt;br&gt;&#xA;Using Roslyn code fixes to make the &amp;quot;Friction-less immutable objects in Bridge&amp;quot; even easier&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Migrating my Full Text Indexer to .NET Core (supporting multi-target NuGet packages)&lt;/strong&gt;&lt;br&gt;&#xA;Revisiting .NET Core tooling (Visual Studio 2017)&lt;br&gt;&#xA;The Full Text Indexer Post Round-up&lt;br&gt;&#xA;The NeoCities Challenge! aka The Full Text Indexer goes client-side!&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Dependency Injection with a WCF Service&lt;/strong&gt;&lt;br&gt;&#xA;Ramping up WCF Web Service Request Handling.. on IIS 6 with .Net 4.0&lt;br&gt;&#xA;Consuming a WCF Web Service from PHP&lt;br&gt;&#xA;WCF with JSON (and nullable types)&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Translating VBScript into C#&lt;/strong&gt;&lt;br&gt;&#xA;VBScript is DIM&lt;br&gt;&#xA;Using Roslyn to identify unused and undeclared variables in VBScript WSC components&lt;br&gt;&#xA;If you can keep your head when all about you are losing theirs and blaming it on VBScript&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;.. but still some less-good suggestions, like:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Auto-releasing Event Listeners&lt;/strong&gt;&lt;br&gt;&#xA;Writing React apps using Bridge.NET - The Dan Way (Part Three)&lt;br&gt;&#xA;Persistent Immutable Lists - Extended&lt;br&gt;&#xA;Extendable LINQ-compilable Mappers&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problems in Immutability-land&lt;/strong&gt;&lt;br&gt;&#xA;Language detection and words-in-sentence classification in C#&lt;br&gt;&#xA;Using Roslyn to identify unused and undeclared variables in VBScript WSC components&lt;br&gt;&#xA;Writing a Brackets extension in TypeScript, in Brackets&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;However, having just looked through the matches to try to find any really awful suggestions, there aren&#x27;t many that jump out at me. And, informal as that may be as a measure of success, I&#x27;m fairly happy with that!&lt;/p&gt;&#xA;&lt;div class=&quot;Related&quot;&gt;&lt;h3&gt;You may also be interested in (see &lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;here&lt;/a&gt; for information about how these are generated):&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;Automating &amp;quot;suggested / related posts&amp;quot; links for my blog posts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/the-full-text-indexer-automating-index-generation&quot;&gt;The Full Text Indexer - Automating Index Generation&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/dynamically-applying-interfaces-to-objects-part-2&quot;&gt;Dynamically applying interfaces to objects - Part 2&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
                <pubDate>Wed, 28 Apr 2021 21:56:00 GMT</pubDate>
            </item>
            <item>
                <title>Automating &quot;suggested / related posts&quot; links for my blog posts</title>
                <link>https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts</link>
                <guid>https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts</guid>
                <description>&lt;h3 id=&quot;tldr&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts#tldr&quot;&gt;TL;DR&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Using the same open source .NET library as I did in my last post (&lt;a href=&quot;https://www.productiverage.com/language-detection-and-wordsinsentence-classification-in-c-sharp&quot;&gt;Language detection and words-in-sentence classification in C#&lt;/a&gt;), I use some of its other machine learning capabilities to automatically generate &amp;quot;you may also be interested in&amp;quot; links to similar posts for any given post on this blog.&lt;/p&gt;&#xA;&lt;h3 id=&quot;the-current-you-may-also-be-interested-in-functionality&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts#the-current-you-may-also-be-interested-in-functionality&quot;&gt;The current &amp;quot;You may also be interested in&amp;quot; functionality&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;This site has always had a way for me to link related posts together - for example, if you scroll to the bottom of &amp;quot;&lt;a href=&quot;https://www.productiverage.com/learning-f-sharp-via-some-machine-learning-the-single-layer-percepton&quot;&gt;Learning F# via some Machine Learning: The Single Layer Perceptron&lt;/a&gt;&amp;quot; then it suggests a link to &amp;quot;&lt;a href=&quot;https://www.productiverage.com/face-or-no-face-finding-faces-in-photos-using-c-sharp-and-accordnet&quot;&gt;Face or no face (finding faces in photos using C# and Accord.NET)&lt;/a&gt;&amp;quot; on the basis that you might be super-excited into my fiddlings with computers being trained how to make decisions on their own. But there aren&#x27;t many of these links because they&#x27;re something that I have to maintain manually. Firstly, that means that I have to remember / consider every previous post and decide whether it might be worth linking to the new post that I&#x27;ve just finished writing and, secondly, I often just forget.&lt;/p&gt;&#xA;&lt;p&gt;There are models in the &lt;a href=&quot;https://github.com/curiosity-ai/catalyst&quot;&gt;Catalyst&lt;/a&gt; library* that make this possible and so I thought that I would see whether I could train it with my blog post data and then incorporate the suggestions into the final content.&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(Again, see my &lt;a href=&quot;https://www.productiverage.com/language-detection-and-wordsinsentence-classification-in-c-sharp&quot;&gt;last post&lt;/a&gt; for more details on this library and a little blurb about my previous employers who are doing exciting things in the Enterprise Search space)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Specifically, I&#x27;ll be using the &lt;a href=&quot;https://en.wikipedia.org/wiki/FastText&quot;&gt;fastText&lt;/a&gt; model that was published by &lt;a href=&quot;https://github.com/facebookresearch/fastText&quot;&gt;Facebook&#x27;s AI Research lab&lt;/a&gt; in 2015 and then &lt;a href=&quot;https://github.com/curiosity-ai/catalyst/tree/master/Catalyst/src/Models/Embeddings/FastText&quot;&gt;rewritten in C#&lt;/a&gt; as part of the Catalyst library.&lt;/p&gt;&#xA;&lt;h3 id=&quot;getting-my-blog-post-articles&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts#getting-my-blog-post-articles&quot;&gt;Getting my blog post articles&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;When I first launched my blog (just over a decade ago), I initially hosted it somewhere as an ASP.NET MVC application. Largely because I wanted to try my hand at writing an MVC app from scratch and fiddling with various settings, I think.. and partly because it felt like the &amp;quot;natural&amp;quot; thing to do, seeing as I was employed as a .NET Developer at the time!&lt;/p&gt;&#xA;&lt;p&gt;To keep things simple, I had a single text file for each blog post and the filenames were of a particular format containing a unique post ID, date and time of publishing, whether it should appear in the &amp;quot;Highlights&amp;quot; column and any tags that should be associated with it. Like this:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;1,2011,3,14,20,14,2,0,Immutability.txt&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;That&#x27;s the very first post (it has ID 1), it was published on 2011-03-14 at 20:14:02 and it is not shown in the Highlights column (hence the final zero). It has a single tag of &amp;quot;Immutability&amp;quot;. Although it has a &amp;quot;.txt&amp;quot; extension, it&#x27;s actually markdown content, so &amp;quot;.md&amp;quot; would have been more logical (the reason why I chose &amp;quot;.txt&amp;quot; over &amp;quot;.md&amp;quot; will likely remain forever lost in the mists of time!)&lt;/p&gt;&#xA;&lt;p&gt;A couple of years later, I came across the project &lt;a href=&quot;https://neocities.org/&quot;&gt;neocities.org&lt;/a&gt; and thought that it was a cool idea and did some (perhaps slightly hacky) work to make things work as a static site (including pushing the search logic entirely to the client) as described in &lt;a href=&quot;https://www.productiverage.com/the-neocities-challenge-aka-the-full-text-indexer-goes-clientside&quot;&gt;The NeoCities Challenge!&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Some &lt;em&gt;more&lt;/em&gt; years later, GitHub Pages started supporting custom domains over HTTPS (in May 2018 according to &lt;a href=&quot;https://github.blog/2018-05-01-github-pages-custom-domains-https/&quot;&gt;this&lt;/a&gt;) and so, having already moved web hosts once due to wildly inconsistent performance from the first provider, I decided to use this to-static-site logic and start publishing via GitHub Pages.&lt;/p&gt;&#xA;&lt;p&gt;This is a long-winded way of saying that, although I publish my content these days as a static site, I write new content by running the original blog app locally and then turning it into static content later. Meaning that the original individual post files are available in the ASP.NET MVC Blog GitHub repo here:&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&quot;https://github.com/ProductiveRage/Blog/tree/master/Blog/App_Data/Posts&quot;&gt;github.com/ProductiveRage/Blog/tree/master/Blog/App_Data/Posts&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Therefore, if you were sufficiently curious and wanted to play along at home, you can also access the original markdown files for my blog posts and see if you can reproduce my results.&lt;/p&gt;&#xA;&lt;p&gt;Following shortly is some code to do just that. GitHub has an API that allows you to query folder contents and so we can get a list of blog post files without having to do anything arduous like clone the entire repo or trying to scrape the information from the site or even creating an authenticated API access application because GitHub allows us rate-limited non-authenticated access for free! Once we have the list of files, each will have a &amp;quot;download_url&amp;quot; that we can retrieve the raw content from.&lt;/p&gt;&#xA;&lt;p&gt;To get the list of blog post files, you would call:&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&quot;https://api.github.com/repos/ProductiveRage/Blog/contents/Blog/App_Data/Posts?ref=master&quot;&gt;api.github.com/repos/ProductiveRage/Blog/contents/Blog/App_Data/Posts?ref=master&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;.. and get results that look like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[&#xA;  {&#xA;    &amp;quot;name&amp;quot;: &amp;quot;1,2011,3,14,20,14,2,0,Immutability.txt&amp;quot;,&#xA;    &amp;quot;path&amp;quot;: &amp;quot;Blog/App_Data/Posts/1,2011,3,14,20,14,2,0,Immutability.txt&amp;quot;,&#xA;    &amp;quot;sha&amp;quot;: &amp;quot;b243ea15c891f73550485af27fa06dd1ccb8bf45&amp;quot;,&#xA;    &amp;quot;size&amp;quot;: 18965,&#xA;    &amp;quot;url&amp;quot;: &amp;quot;https://api.github.com/repos/ProductiveRage/Blog/contents/Blog/App_Data/Posts/1,2011,3,14,20,14,2,0,Immutability.txt?ref=master&amp;quot;,&#xA;    &amp;quot;html_url&amp;quot;: &amp;quot;https://github.com/ProductiveRage/Blog/blob/master/Blog/App_Data/Posts/1,2011,3,14,20,14,2,0,Immutability.txt&amp;quot;,&#xA;    &amp;quot;git_url&amp;quot;: &amp;quot;https://api.github.com/repos/ProductiveRage/Blog/git/blobs/b243ea15c891f73550485af27fa06dd1ccb8bf45&amp;quot;,&#xA;    &amp;quot;download_url&amp;quot;: &amp;quot;https://raw.githubusercontent.com/ProductiveRage/Blog/master/Blog/App_Data/Posts/1%2C2011%2C3%2C14%2C20%2C14%2C2%2C0%2CImmutability.txt&amp;quot;,&#xA;    &amp;quot;type&amp;quot;: &amp;quot;file&amp;quot;,&#xA;    &amp;quot;_links&amp;quot;: {&#xA;      &amp;quot;self&amp;quot;: &amp;quot;https://api.github.com/repos/ProductiveRage/Blog/contents/Blog/App_Data/Posts/1,2011,3,14,20,14,2,0,Immutability.txt?ref=master&amp;quot;,&#xA;      &amp;quot;git&amp;quot;: &amp;quot;https://api.github.com/repos/ProductiveRage/Blog/git/blobs/b243ea15c891f73550485af27fa06dd1ccb8bf45&amp;quot;,&#xA;      &amp;quot;html&amp;quot;: &amp;quot;https://github.com/ProductiveRage/Blog/blob/master/Blog/App_Data/Posts/1,2011,3,14,20,14,2,0,Immutability.txt&amp;quot;&#xA;    }&#xA;  },&#xA;  {&#xA;    &amp;quot;name&amp;quot;: &amp;quot;10,2011,8,30,19,06,0,0,Mercurial.txt&amp;quot;,&#xA;    &amp;quot;path&amp;quot;: &amp;quot;Blog/App_Data/Posts/10,2011,8,30,19,06,0,0,Mercurial.txt&amp;quot;,&#xA;    &amp;quot;sha&amp;quot;: &amp;quot;ab6cf2fc360948212e29c64d9c886b3dbfe0d6fc&amp;quot;,&#xA;    &amp;quot;size&amp;quot;: 3600,&#xA;    &amp;quot;url&amp;quot;: &amp;quot;https://api.github.com/repos/ProductiveRage/Blog/contents/Blog/App_Data/Posts/10,2011,8,30,19,06,0,0,Mercurial.txt?ref=master&amp;quot;,&#xA;    &amp;quot;html_url&amp;quot;: &amp;quot;https://github.com/ProductiveRage/Blog/blob/master/Blog/App_Data/Posts/10,2011,8,30,19,06,0,0,Mercurial.txt&amp;quot;,&#xA;    &amp;quot;git_url&amp;quot;: &amp;quot;https://api.github.com/repos/ProductiveRage/Blog/git/blobs/ab6cf2fc360948212e29c64d9c886b3dbfe0d6fc&amp;quot;,&#xA;    &amp;quot;download_url&amp;quot;: &amp;quot;https://raw.githubusercontent.com/ProductiveRage/Blog/master/Blog/App_Data/Posts/10%2C2011%2C8%2C30%2C19%2C06%2C0%2C0%2CMercurial.txt&amp;quot;,&#xA;    &amp;quot;type&amp;quot;: &amp;quot;file&amp;quot;,&#xA;    &amp;quot;_links&amp;quot;: {&#xA;      &amp;quot;self&amp;quot;: &amp;quot;https://api.github.com/repos/ProductiveRage/Blog/contents/Blog/App_Data/Posts/10,2011,8,30,19,06,0,0,Mercurial.txt?ref=master&amp;quot;,&#xA;      &amp;quot;git&amp;quot;: &amp;quot;https://api.github.com/repos/ProductiveRage/Blog/git/blobs/ab6cf2fc360948212e29c64d9c886b3dbfe0d6fc&amp;quot;,&#xA;      &amp;quot;html&amp;quot;: &amp;quot;https://github.com/ProductiveRage/Blog/blob/master/Blog/App_Data/Posts/10,2011,8,30,19,06,0,0,Mercurial.txt&amp;quot;&#xA;    }&#xA;  },&#xA;  ..&#xA;  &#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;While the API is rate-limited, retrieving content via the &amp;quot;download_url&amp;quot; locations is not - so we can make a single API call for the list and then download all of the individual files that we want.&lt;/p&gt;&#xA;&lt;p&gt;Note that there are a couple of files in that folders that are NOT blog posts (such as the &amp;quot;RelatedPosts.txt&amp;quot; file, which is the way that I manually associate &amp;quot;You may also be interested in&amp;quot; post) and so each filename will have to be checked to ensure that it matches the format shown above.&lt;/p&gt;&#xA;&lt;p&gt;The title of the blog post is not in the file name, it is always the first line of the content in the file (to obtain it, we&#x27;ll need to process the file as markdown content, convert it to plain text and then look at that first line).&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static async Task&amp;lt;IEnumerable&amp;lt;BlogPost&amp;gt;&amp;gt; GetBlogPosts()&#xA;{&#xA;    // Note: The GitHub API is rate limited quite severely for non-authenticated apps, so we just&#xA;    // call it once for the list of files and then retrieve them all further down via the Download&#xA;    // URLs (which don&#x27;t count as API calls). Still, if you run this code repeatedly and start&#xA;    // getting 403 &amp;quot;rate limited&amp;quot; responses then you might have to hold off for a while.&#xA;    string namesAndUrlsJson;&#xA;    using (var client = new WebClient())&#xA;    {&#xA;        // The API refuses requests without a User Agent, so set one before calling (see&#xA;        // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required)&#xA;        client.Headers.Add(HttpRequestHeader.UserAgent, &amp;quot;ProductiveRage Blog Post Example&amp;quot;);&#xA;        namesAndUrlsJson = await client.DownloadStringTaskAsync(new Uri(&#xA;            &amp;quot;https://api.github.com/repos/ProductiveRage/Blog/contents/Blog/App_Data/Posts?ref=master&amp;quot;&#xA;        ));&#xA;    }&#xA;&#xA;    // Deserialise the response into an array of entries that have Name and Download_Url properties&#xA;    var namesAndUrls = JsonConvert.DeserializeAnonymousType(&#xA;        namesAndUrlsJson,&#xA;        new[] { new { Name = &amp;quot;&amp;quot;, Download_Url = (Uri)null } }&#xA;    );&#xA;&#xA;    return await Task.WhenAll(namesAndUrls&#xA;        .Select(entry =&amp;gt;&#xA;        {&#xA;            var fileNameSegments = Path.GetFileNameWithoutExtension(entry.Name).Split(&amp;quot;,&amp;quot;);&#xA;            if (fileNameSegments.Length &amp;lt; 8)&#xA;                return default;&#xA;            if (!int.TryParse(fileNameSegments[0], out var id))&#xA;                return default;&#xA;            var dateContent = string.Join(&amp;quot;,&amp;quot;, fileNameSegments.Skip(1).Take(6));&#xA;            if (!DateTime.TryParseExact(dateContent, &amp;quot;yyyy,M,d,H,m,s&amp;quot;, default, default, out var date))&#xA;                return default;&#xA;            return (PostID: id, PublishedAt: date, entry.Download_Url);&#xA;        })&#xA;        .Where(entry =&amp;gt; entry != default)&#xA;        .Select(async entry =&amp;gt;&#xA;        {&#xA;            // Read the file content as markdown and parse into plain text (the first line of which&#xA;            // will be the title of the post)&#xA;            string markdown;&#xA;            using (var client = new WebClient())&#xA;            {&#xA;                markdown = await client.DownloadStringTaskAsync(entry.Download_Url);&#xA;            }&#xA;            var plainText = Markdown.ToPlainText(markdown);&#xA;            var title = plainText.Replace(&amp;quot;\r\n&amp;quot;, &amp;quot;\n&amp;quot;).Replace(&#x27;\r&#x27;, &#x27;\n&#x27;).Split(&#x27;\n&#x27;).First();&#xA;            return new BlogPost(entry.PostID, title, plainText, entry.PublishedAt);&#xA;        })&#xA;    );&#xA;}&#xA;&#xA;private sealed class BlogPost&#xA;{&#xA;    public BlogPost(int id, string title, string plainTextContent, DateTime publishedAt)&#xA;    {&#xA;        ID = id;&#xA;        Title = !string.IsNullOrWhiteSpace(title)&#xA;            ? title&#xA;            : throw new ArgumentException(&amp;quot;may not be null, blank or whitespace-only&amp;quot;);&#xA;        PlainTextContent = !string.IsNullOrWhiteSpace(plainTextContent)&#xA;            ? plainTextContent&#xA;            : throw new ArgumentException(&amp;quot;may not be null, blank or whitespace-only&amp;quot;);&#xA;        PublishedAt = publishedAt;&#xA;    }&#xA;&#xA;    public int ID{ get; }&#xA;    public string Title { get; }&#xA;    public string PlainTextContent { get; }&#xA;    public DateTime PublishedAt { get; }&#xA;}    &#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;em&gt;(Note: I use the &lt;a href=&quot;https://github.com/xoofx/markdig&quot;&gt;Markdig&lt;/a&gt; library to process markdown)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;training-a-fasttext-model&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts#training-a-fasttext-model&quot;&gt;Training a FastText model&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;This raw blog post content needs to transformed into Catalyst &amp;quot;documents&amp;quot;, then tokenised (split into individual sentences and words), then fed into a FastText model trainer.&lt;/p&gt;&#xA;&lt;p&gt;Before getting to the code, I want to discuss a couple of oddities coming up. Firstly, Catalyst documents are required to train the FastText model and each document instance must be uniquely identified by a &lt;strong&gt;UID128&lt;/strong&gt; value, which is fine because we can generate them from the Title text of each blog post using the &amp;quot;Hash128()&amp;quot; extension method in Catalyst. However, (as we&#x27;ll see a bit further down), when you ask for vectors* from the FastText model for the processed documents, each vector comes with a &amp;quot;Token&amp;quot; string that is the ID of the source document - so that has to be parsed &lt;em&gt;back&lt;/em&gt; into a &lt;strong&gt;UID128&lt;/strong&gt;. I&#x27;m not quite sure why the &amp;quot;Token&amp;quot; value isn&#x27;t also a &lt;strong&gt;UID128&lt;/strong&gt; but it&#x27;s no massive deal.&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(Vectors are just 1D arrays of floating point values - the FastText algorithm does magic to produce vectors that represent the text of the documents such that the distance between them can be compared; the length of these arrays is determined by the &amp;quot;Dimensions&amp;quot; option shown below and shorter distances between vectors suggest more similar content)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Next, there are the FastText settings that I&#x27;ve used. The &lt;a href=&quot;https://github.com/curiosity-ai/catalyst&quot;&gt;Catalyst README&lt;/a&gt; has some code near the bottom for training a FastText embedding model but I didn&#x27;t have much luck with the default options. Firstly, when I used the &amp;quot;FastText.ModelType.CBow&amp;quot; option then I didn&#x27;t get any vectors generated and so I tried changing it to &amp;quot;FastText.ModelType.PVDM&amp;quot; and things started looked promising. Then I fiddled with some of the other settings. Some of which I have a rough idea what they mean and some, erm.. not so much.&lt;/p&gt;&#xA;&lt;p&gt;The settings that I ended up using are these:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var fastText = new FastText(language, version: 0, tag: &amp;quot;&amp;quot;);&#xA;fastText.Data.Type = FastText.ModelType.PVDM;&#xA;fastText.Data.Loss = FastText.LossType.NegativeSampling;&#xA;fastText.Data.IgnoreCase = true;&#xA;fastText.Data.Epoch = 50;&#xA;fastText.Data.Dimensions = 512;&#xA;fastText.Data.MinimumCount = 1;&#xA;fastText.Data.ContextWindow = 10;&#xA;fastText.Data.NegativeSamplingCount = 20;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I already mentioned changing the Data.Type / ModelType and the LossType (&amp;quot;NegativeSampling&amp;quot;) is the value shown in the README. Then I felt like an obvious one to change was IgnoreCase, since that defaults to false and I think that I want it to be true - I don&#x27;t care about the casing in any words when it&#x27;s parsing my posts&#x27; content.&lt;/p&gt;&#xA;&lt;p&gt;Now the others.. well, this library is built to work with systems with 10s or 100s of 1,000s of documents and that is a LOT more data than I have (currently around 120 blog posts) and so I made a few tweaks based on that. The &amp;quot;Epoch&amp;quot; count is the number of iterations that the training process will go through when constructing its model - by default, this is only 5 but I have limited data (meaning there&#x27;s less for it to learn from but also that it&#x27;s faster to complete each iteration) and so I bumped that up to 50. Then &amp;quot;Dimensions&amp;quot; is the size of the vectors generated - again, I figured that with limited data I would want a higher value and so I picked 512 (a nice round number if you&#x27;re geeky enough) over the default 200. The &amp;quot;MinimumCount&amp;quot;, I believe, relates to how often a word may appear and it defaults to 5 so I pulled it down to 1. The &amp;quot;ContextWindow&amp;quot; is (again, I &lt;em&gt;think&lt;/em&gt;) how far to either side of any word that the process will look at in order to determine context - the larger the value, the more expensive the calculation; I bumped this from the default 5 up to 10. Then there&#x27;s the &amp;quot;NegativeSamplingCount&amp;quot; value.. I have to just put my hands up and say that I have no idea what that actually does, only that I seemed to be getting better results with a value of 20 than I was with the default of 10.&lt;/p&gt;&#xA;&lt;p&gt;With machine learning, there is almost always going to be some value to tweaking options (the &amp;quot;hyperparameters&amp;quot;, if we&#x27;re being all fancy) like this when building a model. Depending upon the model and the library, the defaults can be good for the general case but my tiny data set is not really what this library was intended for. Of course, machine learning &lt;em&gt;experts&lt;/em&gt; have more idea &lt;em&gt;what&lt;/em&gt; they&#x27;re tweaking and (sometimes, at least) hopefully what results they&#x27;ll get.. but I&#x27;m happy enough with where I&#x27;ve ended up with these.&lt;/p&gt;&#xA;&lt;p&gt;This talk about what those machine learning experts do brings me on to the final thing that I wanted to talk about before showing the code; a little pre-processing / data-massaging. The better the data is that goes in, generally the better the results that come out will be. So another less glamorous part of the life of a Data Scientist is cleaning up data for training models.&lt;/p&gt;&#xA;&lt;p&gt;In my case, that only extended to noticing that a few terms didn&#x27;t seem to be getting recognised as essentially being the same thing and so I wanted to give it a little hand - for example, a fair number of my posts are about my &amp;quot;Full Text Indexer&amp;quot; project and so it probably makes sense to replace any instances of that string with a single concatenated word &amp;quot;FullTextIndexer&amp;quot;. And I have a range of posts about React but I didn&#x27;t want it to get confused with the verb &amp;quot;react&amp;quot; and so I replaced any &amp;quot;React&amp;quot; occurrence with &amp;quot;ReactJS&amp;quot; (now, this probably means that some &amp;quot;React&amp;quot; verb occurrences were incorrectly changed but I made the replacements of this word in a case-sensitive manner and felt like I would have likely used it as the noun more often than a verb with a capital letter due to the nature of my posts).&lt;/p&gt;&#xA;&lt;p&gt;So I have a method to tidy up the plain text content a little:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static string NormaliseSomeCommonTerms(string text) =&amp;gt; text&#xA;    .Replace(&amp;quot;.NET&amp;quot;, &amp;quot;NET&amp;quot;, StringComparison.OrdinalIgnoreCase)&#xA;    .Replace(&amp;quot;Full Text Indexer&amp;quot;, &amp;quot;FullTextIndexer&amp;quot;, StringComparison.OrdinalIgnoreCase)&#xA;    .Replace(&amp;quot;Bridge.net&amp;quot;, &amp;quot;BridgeNET&amp;quot;, StringComparison.OrdinalIgnoreCase)&#xA;    .Replace(&amp;quot;React&amp;quot;, &amp;quot;ReactJS&amp;quot;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Now let&#x27;s get training!&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Console.WriteLine(&amp;quot;Reading posts from GitHub repo..&amp;quot;);&#xA;var posts = await GetBlogPosts();&#xA;&#xA;Console.WriteLine(&amp;quot;Parsing documents..&amp;quot;);&#xA;Storage.Current = new OnlineRepositoryStorage(new DiskStorage(&amp;quot;catalyst-models&amp;quot;));&#xA;var language = Language.English;&#xA;var pipeline = Pipeline.For(language);&#xA;var postsWithDocuments = posts&#xA;    .Select(post =&amp;gt;&#xA;    {&#xA;        var document = new Document(NormaliseSomeCommonTerms(post.PlainTextContent), language)&#xA;        {&#xA;            UID = post.Title.Hash128()&#xA;        };&#xA;        pipeline.ProcessSingle(document);&#xA;        return (Post: post, Document: document);&#xA;    })&#xA;    .ToArray(); // Call ToArray to force evaluation of the document processing now&#xA;&#xA;Console.WriteLine(&amp;quot;Training FastText model..&amp;quot;);&#xA;var fastText = new FastText(language, version: 0, tag: &amp;quot;&amp;quot;);&#xA;fastText.Data.Type = FastText.ModelType.PVDM;&#xA;fastText.Data.Loss = FastText.LossType.NegativeSampling;&#xA;fastText.Data.IgnoreCase = true;&#xA;fastText.Data.Epoch = 50;&#xA;fastText.Data.Dimensions = 512;&#xA;fastText.Data.MinimumCount = 1;&#xA;fastText.Data.ContextWindow = 10;&#xA;fastText.Data.NegativeSamplingCount = 20;&#xA;fastText.Train(&#xA;    postsWithDocuments.Select(postWithDocument =&amp;gt; postWithDocument.Document),&#xA;    trainingStatus: update =&amp;gt; Console.WriteLine($&amp;quot; Progress: {update.Progress}, Epoch: {update.Epoch}&amp;quot;)&#xA;);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h3 id=&quot;identifying-similar-documents-using-the-model&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts#identifying-similar-documents-using-the-model&quot;&gt;Identifying similar documents using the model&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Now that a model has been built that can represent all of my blog posts as vectors, we need to go through those post / vector combinations and identify others that are similar to it.&lt;/p&gt;&#xA;&lt;p&gt;This will be achieved by using the &lt;a href=&quot;https://github.com/curiosity-ai/hnsw-sharp&quot;&gt;HNSW.NET&lt;/a&gt; NuGet package that enables K-Nearest Neighbour (k-NN) searches over &amp;quot;high-dimensional space&amp;quot;*.&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(This just means that the vectors are relatively large; 512 in this case - two dimensions would be a point on a flat plane, three dimensions would be a physical point in space, anything with more dimensions that that is in &amp;quot;higher-dimensional space&amp;quot;.. though that&#x27;s not to say that any more than three dimensions is definitely a bad fit for a regular k-NN search but 512 dimensions IS going to be a bad fit and the HNSW approach will be much more efficient)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;There are useful examples on the &lt;a href=&quot;https://github.com/curiosity-ai/hnsw-sharp#how-to-build-a-graph&quot;&gt;README&lt;/a&gt; about &amp;quot;&lt;strong&gt;How to build a graph?&lt;/strong&gt;&amp;quot; and &amp;quot;&lt;strong&gt;How to run k-NN search?&lt;/strong&gt;&amp;quot; and tweaking those for the data that I have so far leads to this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Console.WriteLine(&amp;quot;Building recommendations..&amp;quot;);&#xA;&#xA;// Combine the blog post data with the FastText-generated vectors&#xA;var results = fastText&#xA;    .GetDocumentVectors()&#xA;    .Select(result =&amp;gt;&#xA;    {&#xA;        // Each document vector instance will include a &amp;quot;token&amp;quot; string that may be mapped back to the&#xA;        // UID of the document for each blog post. If there were a large number of posts to deal with&#xA;        // then a dictionary to match UIDs to blog posts would be sensible for performance but I only&#xA;        // have a 100&#x2B; and so a LINQ &amp;quot;First&amp;quot; scan over the list will suffice.&#xA;        var uid = UID128.Parse(result.Token);&#xA;        var postForResult = postsWithDocuments.First(&#xA;            postWithDocument =&amp;gt; postWithDocument.Document.UID == uid&#xA;        );&#xA;        return (UID: uid, result.Vector, postForResult.Post);&#xA;    })&#xA;    .ToArray(); // ToArray since we enumerate multiple times below&#xA;&#xA;// Construct a graph to search over, as described at&#xA;// https://github.com/curiosity-ai/hnsw-sharp#how-to-build-a-graph&#xA;var graph = new SmallWorld&amp;lt;(UID128 UID, float[] Vector, BlogPost Post), float&amp;gt;(&#xA;    distance: (to, from) =&amp;gt; CosineDistance.NonOptimized(from.Vector, to.Vector),&#xA;    DefaultRandomGenerator.Instance,&#xA;    new() { M = 15, LevelLambda = 1 / Math.Log(15) }&#xA;);&#xA;graph.AddItems(results);&#xA;&#xA;// For every post, use the &amp;quot;KNNSearch&amp;quot; method on the graph to find the three most similar posts&#xA;const int maximumNumberOfResultsToReturn = 3;&#xA;var postsWithSimilarResults = results&#xA;    .Select(result =&amp;gt;&#xA;    {&#xA;        // Request one result too many from the KNNSearch call because it&#x27;s expected that the original&#xA;        // post will come back as the best match and we&#x27;ll want to exclude that&#xA;        var similarResults = graph&#xA;            .KNNSearch(result, maximumNumberOfResultsToReturn &#x2B; 1)&#xA;            .Where(similarResult =&amp;gt; similarResult.Item.UID != result.UID)&#xA;            .Take(maximumNumberOfResultsToReturn); // Just in case the original post wasn&#x27;t included&#xA;&#xA;        return new&#xA;        {&#xA;            result.Post,&#xA;            Similar = similarResults&#xA;                .Select(similarResult =&amp;gt; new&#xA;                {&#xA;                    similarResult.Id,&#xA;                    similarResult.Item.Post,&#xA;                    similarResult.Distance&#xA;                })&#xA;                .ToArray()&#xA;        };&#xA;    })&#xA;    .OrderBy(result =&amp;gt; result.Post.Title, StringComparer.OrdinalIgnoreCase)&#xA;    .ToArray();&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;And with that, there is a list of every post from my blog and a list of the three blog posts most similar to it!&lt;/p&gt;&#xA;&lt;p&gt;Well, &amp;quot;most similar&amp;quot; according to the model that we trained and the hyperparameters that we used to do so. As with many machine learning algorithms, it will have started from a random state and tweaked and tweaked until it&#x27;s time for it to stop (based upon the &amp;quot;Epoch&amp;quot; value in this FastText case) and so the results each time may be a little different.&lt;/p&gt;&#xA;&lt;p&gt;However, if we inspect the results like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;foreach (var postWithSimilarResults in postsWithSimilarResults)&#xA;{&#xA;    Console.WriteLine();&#xA;    Console.WriteLine(postWithSimilarResults.Post.Title);&#xA;    foreach (var similarResult in postWithSimilarResults.Similar.OrderBy(other =&amp;gt; other.Distance))&#xA;        Console.WriteLine($&amp;quot;{similarResult.Distance:0.000} {similarResult.Post.Title}&amp;quot;);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;.. then there are some good results to be found! Like these:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Learning F# via some Machine Learning: The Single Layer Perceptron&lt;/strong&gt;&lt;br&gt;&#xA;0.229 How are barcodes read?? (Library-less image processing in C#)&lt;br&gt;&#xA;0.236 Writing F# to implement &#x27;The Single Layer Perceptron&#x27;&lt;br&gt;&#xA;0.299 Face or no face (finding faces in photos using C# and AccordNET)&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Translating VBScript into C#&lt;/strong&gt;&lt;br&gt;&#xA;0.257 VBScript is DIM&lt;br&gt;&#xA;0.371 If you can keep your head when all about you are losing theirs and blaming it on VBScript&lt;br&gt;&#xA;0.384 Using Roslyn to identify unused and undeclared variables in VBScript WSC components&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Writing React components in TypeScript&lt;/strong&gt;&lt;br&gt;&#xA;0.376 TypeScript classes for (React) Flux actions&lt;br&gt;&#xA;0.378 React and Flux with DuoCode&lt;br&gt;&#xA;0.410 React (and Flux) with Bridge.net&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;However, there are also some &lt;em&gt;less&lt;/em&gt; good ones - like these:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;A static type system is a wonderful message to the present and future&lt;/strong&gt;&lt;br&gt;&#xA;0.271 STA ApartmentState with ASP.Net MVC&lt;br&gt;&#xA;0.291 CSS Minification Regular Expressions&lt;br&gt;&#xA;0.303 Publishing RSS&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Simple TypeScript type definitions for AMD modules&lt;/strong&gt;&lt;br&gt;&#xA;0.162 STA ApartmentState with ASP.Net MVC&lt;br&gt;&#xA;0.189 WCF with JSON (and nullable types)&lt;br&gt;&#xA;0.191 The joys of AutoMapper&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Supporting IDispatch through the COMInteraction wrapper&lt;/strong&gt;&lt;br&gt;&#xA;0.394 A static type system is a wonderful message to the present and future&lt;br&gt;&#xA;0.411 TypeScript State Machines&lt;br&gt;&#xA;0.414 Simple TypeScript type definitions for AMD modules&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&quot;improving-the-results&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts#improving-the-results&quot;&gt;Improving the results&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I&#x27;d like to get this good enough that I can include auto-generated recommendations on my blog and I don&#x27;t feel like the consistency in quality is there yet. If they were all like the good examples then I&#x27;d be ploughing ahead right now with enabling it! But there are mediocre examples as well as those poorer ones above.&lt;/p&gt;&#xA;&lt;p&gt;It&#x27;s quite possible that I could get closer by experimenting with the hyperparameters more but that does tend to get tedious when you have to analyse the output of each run manually - looking through all the 120-ish post titles and deciding whether the supposed best matches are good or not. It would be lovely if I could concoct some sort of metric of &amp;quot;goodness&amp;quot; and then have the computer try lots of variations of parameters but one of the downsides of having relatively little data is that that is difficult*.&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(On the flip side, if I had &lt;strong&gt;1,000s&lt;/strong&gt; of blog posts as source data then the difficult part would be manually labelling enough of them as &amp;quot;quite similar&amp;quot; in numbers sufficient for the computer to know if it&#x27;s done better or done worse with each experiment)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Fortunately, I have another trick up my sleeve - but I&#x27;m going to leave that for next time! This post is already more than long enough, I think. The plan is to combine results from &lt;em&gt;another&lt;/em&gt; model in the Catalyst with the FastText results and see if I can encourage things to look a bit neater.&lt;/p&gt;&#xA;&lt;h3 id=&quot;trying-the-code-if-youre-lazy&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts#trying-the-code-if-youre-lazy&quot;&gt;Trying the code if you&#x27;re lazy&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;If you want to try fiddling with this code but don&#x27;t want to copy-paste the sections above into a new project, you can find the complete sample in the &amp;quot;Similarity&amp;quot; project in the solution of this repo: &lt;a href=&quot;https://github.com/ProductiveRage/BlogPostSimilarity&quot;&gt;BlogPostSimilarity&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;div class=&quot;Related&quot;&gt;&lt;h3&gt;You may also be interested in (see &lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;here&lt;/a&gt; for information about how these are generated):&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2&quot;&gt;Automating &amp;quot;suggested / related posts&amp;quot; links for my blog posts - Part 2&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/private-local-c-sharp-analysers-without-nuget&quot;&gt;Private / local C# analysers (without NuGet)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/the-full-text-indexer-automating-index-generation&quot;&gt;The Full Text Indexer - Automating Index Generation&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
                <pubDate>Wed, 07 Apr 2021 22:21:00 GMT</pubDate>
            </item>
            <item>
                <title>Language detection and words-in-sentence classification in C#</title>
                <link>https://www.productiverage.com/language-detection-and-wordsinsentence-classification-in-c-sharp</link>
                <guid>https://www.productiverage.com/language-detection-and-wordsinsentence-classification-in-c-sharp</guid>
                <description>&lt;h3 id=&quot;tlbgdr&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/language-detection-and-wordsinsentence-classification-in-c-sharp#tlbgdr&quot;&gt;TL;(BG)DR&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Using an open source .NET library, it&#x27;s easy to determine what language a sentence / paragraph / document is written in and to then classify the words in each sentence into verbs, nouns, etc..&lt;/p&gt;&#xA;&lt;h3 id=&quot;what-library&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/language-detection-and-wordsinsentence-classification-in-c-sharp#what-library&quot;&gt;What library?&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I recently parted ways on very good terms with my last employers (and friends!) at &lt;a href=&quot;https://curiosity.ai/&quot;&gt;Curiosity AI&lt;/a&gt; but that doesn&#x27;t mean that I&#x27;m not still excited by their technology, some really useful aspects of which they have released as open source*.&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(For the full service, ask yourself if your team or your company have ever struggled to find some information that you know exists somewhere but that might be in one of your network drives containing 10s of 1,000s of files &lt;strong&gt;or&lt;/strong&gt; in your emails &lt;strong&gt;or&lt;/strong&gt; in Sharepoint &lt;strong&gt;or&lt;/strong&gt; GDrive somewhere - with Curiosity, you can set up a system that will index all that data so that it&#x27;s searchable in one place, as well as learning synonyms and abbreviations in case you can&#x27;t conjure up the precise terms to search for. It can even find similar documents for those case where have one document to hand and just know that there&#x27;s another related to it but are struggling to find it - plus it has an ingrained permissions model so that your team could all index their emails and GDrive files and be secure in the knowledge that only they and people that they&#x27;ve shared the files with can see them; they don&#x27;t get pulled in in such a way that your private, intimate, confidential emails are now visible to everyone!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;I have a little time off between jobs and so I wanted to write a little bit about some of the open-sourced projects that they released that I think are cool.&lt;/p&gt;&#xA;&lt;p&gt;This first one is a really simple example but I think that it demonstrates how easily you can access capabilities that are pretty impressive.&lt;/p&gt;&#xA;&lt;p&gt;This is my cat Cass:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&quot;https://www.productiverage.com/Content/Images/Posts/Cass.jpg&quot; alt=&quot;Cute little girl&quot; title=&quot;Cute little girl&quot;&gt;&lt;/p&gt;&#xA;&lt;p&gt;She looks so cute that you&#x27;d think butter wouldn&#x27;t melt. But, of my three cats, she is the prime suspect for the pigeon carcus that was recently dragged through the cat flap one night, up a flight of stairs and deposited outside my home office - and, perhaps not coincidentally, a mere six feet away from where she&#x27;d recently made herself a cosy bed in a duvet cover that I&#x27;d left out to remind myself to wash.&lt;/p&gt;&#xA;&lt;p&gt;I think that it&#x27;s a fair conclusion to draw that:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;My cat Cass is a lovely fluffy little pigeon-killer!&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Now you and I can easily see that that is a sentence written in English. But if you wanted a computer to work it out, how would you go about it?&lt;/p&gt;&#xA;&lt;p&gt;Well, one way would be to install the &lt;a href=&quot;https://github.com/curiosity-ai/catalyst&quot;&gt;Catalyst&lt;/a&gt; &lt;a href=&quot;https://www.nuget.org/packages/Catalyst&quot;&gt;NuGet package&lt;/a&gt; and write the following code:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;using System;&#xA;using System.IO;&#xA;using System.Threading.Tasks;&#xA;using Catalyst;&#xA;using Catalyst.Models;&#xA;using Mosaik.Core;&#xA;using Version = Mosaik.Core.Version;&#xA;&#xA;namespace CatalystExamples&#xA;{&#xA;    internal static class Program&#xA;    {&#xA;        private static async Task Main()&#xA;        {&#xA;            const string text = &amp;quot;My cat Cass is a lovely fluffy little pigeon-killer!&amp;quot;;&#xA;&#xA;            Console.WriteLine(&amp;quot;Downloading/reading language detection models..&amp;quot;);&#xA;            const string modelFolderName = &amp;quot;catalyst-models&amp;quot;;&#xA;            if (!new DirectoryInfo(modelFolderName).Exists)&#xA;                Console.WriteLine(&amp;quot;- Downloading for the first time, so this may take a little while&amp;quot;);&#xA;            &#xA;            Storage.Current = new OnlineRepositoryStorage(new DiskStorage(modelFolderName));&#xA;            var languageDetector = await FastTextLanguageDetector.FromStoreAsync(&#xA;                Language.Any,&#xA;                Version.Latest,&#xA;                &amp;quot;&amp;quot;&#xA;            );&#xA;            Console.WriteLine();&#xA;&#xA;            var doc = new Document(text);&#xA;            languageDetector.Process(doc);&#xA;&#xA;            Console.WriteLine(text);&#xA;            Console.WriteLine($&amp;quot;Detected language: {doc.Language}&amp;quot;);&#xA;        }&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Running this code will print the following to the console:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Downloading/reading language detection models..&#xA;- Downloading for the first time, so this may take a little while&#xA;&#xA;My cat Cass is a lovely fluffy little pigeon-killer!&#xA;Detected language: English&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Just to prove that it doesn&#x27;t &lt;em&gt;only&lt;/em&gt; detect English, I ran the sentence through Google Translate to get a German version (unfortunately, the languages I&#x27;m fluent in are only English and a few computer languages and so Google Translate was very much needed!) - thus changing the &amp;quot;text&amp;quot; definition to:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;const string text = &amp;quot;Meine Katze Cass ist eine sch&#xF6;ne flauschige kleine Taubenm&#xF6;rderin!&amp;quot;;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Running the altered program results in the following console output:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Downloading/reading language detection models..&#xA;&#xA;Meine Katze Cass ist eine wundersch&#xF6;ne, flauschige kleine Taubenm&#xF6;rderin!&#xA;Detected language: German&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Great success!&lt;/p&gt;&#xA;&lt;p&gt;The next thing that we can do is analyse the grammatical constructs of the sentence. I&#x27;m going to return to the English version for this because it will be easier for me to be confident that the word classifications are correct.&lt;/p&gt;&#xA;&lt;p&gt;Add the following code immediately after the Console.WriteLine calls in the Main method from earlier:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Console.WriteLine();&#xA;Console.WriteLine($&amp;quot;Downloading/reading part-of-speech model for {doc.Language}..&amp;quot;);&#xA;var pipeline = await Pipeline.ForAsync(doc.Language);&#xA;pipeline.ProcessSingle(doc);&#xA;foreach (var sentence in doc)&#xA;{&#xA;    foreach (var token in sentence)&#xA;        Console.WriteLine($&amp;quot;{token.Value}{new string(&#x27; &#x27;, 20 - token.Value.Length)}{token.POS}&amp;quot;);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The program will now write the following to the console:&lt;/p&gt;&#xA;&lt;p&gt;Downloading/reading language detection models..&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;My cat Cass is a lovely fluffy little pigeon-killer!&#xA;Detected language: English&#xA;&#xA;Downloading/reading part-of-speech model for English..&#xA;My                  PRON&#xA;cat                 NOUN&#xA;Cass                PROPN&#xA;is                  AUX&#xA;a                   DET&#xA;lovely              ADJ&#xA;fluffy              ADJ&#xA;little              ADJ&#xA;pigeon-killer       NOUN&#xA;!                   PUNCT&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &amp;quot;Part of Speech&amp;quot; (PoS) categories shown above are (as quoted from &lt;a href=&quot;https://universaldependencies.org/u/pos/all.html&quot;&gt;universaldependencies.org/u/pos/all.html&lt;/a&gt;) -&lt;/p&gt;&#xA;&lt;div class=&quot;TableScrollWrapper&quot;&gt;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;Word(s)&lt;/th&gt;&#xA;&lt;th&gt;Code&lt;/th&gt;&#xA;&lt;th&gt;Name&lt;/th&gt;&#xA;&lt;th&gt;Description&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;My&lt;/td&gt;&#xA;&lt;td&gt;PRON&lt;/td&gt;&#xA;&lt;td&gt;Pronoun&lt;/td&gt;&#xA;&lt;td&gt;words that substitute for nouns or noun phrases, whose meaning is recoverable from the linguistic or extralinguistic context&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;cat, pigeon-killer&lt;/td&gt;&#xA;&lt;td&gt;NOUN&lt;/td&gt;&#xA;&lt;td&gt;Noun&lt;/td&gt;&#xA;&lt;td&gt;a part of speech typically denoting a person, place, thing, animal or idea&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;Cass&lt;/td&gt;&#xA;&lt;td&gt;PNOUN&lt;/td&gt;&#xA;&lt;td&gt;Proper Noun&lt;/td&gt;&#xA;&lt;td&gt;a noun (or nominal content word) that is the name (or part of the name) of a specific individual, place, or object&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;is&lt;/td&gt;&#xA;&lt;td&gt;AUX&lt;/td&gt;&#xA;&lt;td&gt;Auxillary Verb&lt;/td&gt;&#xA;&lt;td&gt;a function word that accompanies the lexical verb of a verb phrase and expresses grammatical distinctions not carried by the lexical verb, such as person, number, tense, mood, aspect, voice or evidentiality&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;a&lt;/td&gt;&#xA;&lt;td&gt;DET&lt;/td&gt;&#xA;&lt;td&gt;Determiner&lt;/td&gt;&#xA;&lt;td&gt;words that modify nouns or noun phrases and express the reference of the noun phrase in context&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;lovely, fluffy, little&lt;/td&gt;&#xA;&lt;td&gt;ADJ&lt;/td&gt;&#xA;&lt;td&gt;Adjective&lt;/td&gt;&#xA;&lt;td&gt;words that typically modify nouns and specify their properties or attributes&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;!&lt;/td&gt;&#xA;&lt;td&gt;PUNCT&lt;/td&gt;&#xA;&lt;td&gt;Punctuation&lt;/td&gt;&#xA;&lt;td&gt;non-alphabetical characters and character groups used in many languages to delimit linguistic units in printed text&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&lt;/div&gt;&#xA;&lt;p&gt;How easy was that?! There are a myriad of uses for this sort of analysis (one of the things that the full Curiosity system uses it for is identifying nouns throughout documents and creating tags that any documents sharing a given noun are linked via; so if you found one document about &amp;quot;Flux Capacitors&amp;quot; then you could easily identify all of the other documents / emails / memos that mentioned it - though that really is just the tip of the iceberg).&lt;/p&gt;&#xA;&lt;h3 id=&quot;very-minor-caveats&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/language-detection-and-wordsinsentence-classification-in-c-sharp#very-minor-caveats&quot;&gt;Very minor caveats&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I have only a couple of warnings before signing off this post. I&#x27;ve seen the sentence detector get confused if it has very little data to work with (a tiny segment fragment, for example) or if there is a document that has different sections written in multiple languages - but I don&#x27;t think that either case is unreasonable, the library is very clever but it can&#x27;t perform magic!&lt;/p&gt;&#xA;&lt;h3 id=&quot;coming-soon&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/language-detection-and-wordsinsentence-classification-in-c-sharp#coming-soon&quot;&gt;Coming soon&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I&#x27;ve got another post relating to their open-sourced libraries in the pipeline, hopefully I&#x27;ll get that out this week! Let&#x27;s just say that I&#x27;m hoping that my days of having to manually maintain the &amp;quot;you may also be interested&amp;quot; links between my posts will soon be behind me!&lt;/p&gt;&#xA;&lt;div class=&quot;Related&quot;&gt;&lt;h3&gt;You may also be interested in (see &lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;here&lt;/a&gt; for information about how these are generated):&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one&quot;&gt;Finding the brightest area in an image with C# (fixing a blurry presentation video - part one)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp&quot;&gt;How are barcodes read?? (Library-less image processing in C#)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/the-c-sharp-css-parser-in-javascript&quot;&gt;The C# CSS Parser in JavaScript&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
                <pubDate>Tue, 09 Mar 2021 19:52:00 GMT</pubDate>
            </item>
            <item>
                <title>Monitoring my garden&#x27;s limited sunlight time period with an Arduino (and some tupperware)</title>
                <link>https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware</link>
                <guid>https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware</guid>
                <description>&lt;p&gt;My house has a lovely little garden out front. The house and garden itself are elevated one storey above the street (and so my basement is really more of a bizarre ground floor because it has natural light windows but is full of dust and my life-accumulated rubbish is in one room of it while my covid-times &amp;quot;trying to stay fit, not fat&amp;quot; home gym is in the other) and there was no fence around it when I moved in. Meaning that that the &lt;em&gt;interesting characters&lt;/em&gt; that amble past (suffice to say that I went for a nicer house in a slightly on-the-cusp between classy and rougher neighbourhoods as opposed to a less nice house in a posh place) could see in and converse between sips on their 9am double-strength lager. Once fenced off, kitted out with a cute little table and chairs that my friendly neighbours found at a tip and with some lovely raised flower beds installed, it is a &lt;em&gt;delight&lt;/em&gt; in Summer.. only problem is that my house faces the wrong way and so only gets direct sunlight at certain hours of the day. And this time period varies greatly depending upon the time of year - in March, it might not get the light until almost 5pm whilst in July and August it&#x27;s getting warm and light and beautiful (well, on the days that English weather allows) more in time for a late lunch.&lt;/p&gt;&#xA;&lt;p&gt;The problem is that, even after four years here, I still don&#x27;t really have any idea when it&#x27;s going to be sunny there for a given time of year and I want to be able to plan opportunities around it - late evening drinks outside with friends, lunch time warm weather meals for myself, just any general chance top up my vitamin D!&lt;/p&gt;&#xA;&lt;img alt=&quot;Rare English sun in my garden (plus cats)&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/SunnyGardenAndCats.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Rare English sun in my garden (plus cats)&quot;&gt;&#xA;&lt;p&gt;I guess that one way to sort this out would be to just keep an eye out on sunny days and take the opportunity whenever it strikes. A more organised plan would be to start a little diary and mark down every fortnight or so through the year when the sun hits the garden and when it leaves.&lt;/p&gt;&#xA;&lt;p&gt;But I work in technology, damnnit, and so I expect to be able to solve this using that electronics and magic! (Cue comments about everything looking like a nail when you&#x27;re holding a hammer).&lt;/p&gt;&#xA;&lt;p&gt;To be &lt;em&gt;really&lt;/em&gt; honest, maybe I&#x27;m describing this situation back to front. My friend gave me an &lt;a href=&quot;https://store.arduino.cc/arduino-uno-rev3&quot;&gt;Arduino UNO r3&lt;/a&gt; because he had a kit spare from the coding club that he runs for kids locally and I&#x27;d been looking for a use for it.. and this seemed like it!&lt;/p&gt;&#xA;&lt;h3 id=&quot;what-i-needed&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware#what-i-needed&quot;&gt;What I needed&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Being a total Arduino noob (and, since my Electronics GCSE was over 20 years ago now, I&#x27;m basically a total hardware noob.. you should have seen the trouble that I had trying to build a custom PC a few years ago; I swear it was easier when I was 14!) I wanted something nice and simple to begin with.&lt;/p&gt;&#xA;&lt;p&gt;So I had the starter kit, which included the Arduino board and some jumper cables, a prototyping breadboard and some common components (including, essentially, a photoresistor) and so I figured that all I&#x27;d then need is a way to record the light levels periodically, a power source and some sort of container for when it rains (again; England).&lt;/p&gt;&#xA;&lt;p&gt;I considered having some sort of fancy wifi server in it that would record the values somehow and let me either poll it from somewhere else or have it push those results to the cloud somewhere but eventually decided to go for what seemed like a simpler, more robust and (presumably) more power efficient mechanism of storing the light values throughout the day - using an SD card. Because I&#x27;d got the kit for free (on the agreement that I would try to do something useful with it), I was looking for something cheap to write to an SD card that I&#x27;d had lying around since.. well, I guess since whenever SD cards were useful. Could it have been a digital camera? The very concept seems absurd these days, with the quality of camera that even phones from three or four generations ago have.&lt;/p&gt;&#xA;&lt;p&gt;I came across something called a &amp;quot;&lt;strong&gt;Deek Robot SD/RTC datalogging shield&lt;/strong&gt;&amp;quot; that would not only write to an SD card but would also keep time due to a small battery mounted on it.&lt;/p&gt;&#xA;&lt;p&gt;These are cheap (mine was less than &#xA3;5 delivered, new from eBay) but documentation is somewhat.. spotty. There is a lot of documentation for the &amp;quot;Adafruit Assembled Data Logging shield&amp;quot; but they cost more like &#xA3;13&#x2B; and I was looking for the cheap option. Considering how much time I spent trying to make it work and find good information, it probably would have made more sense to buy a better supported shield than a knock-off from somewhere.. but I &lt;em&gt;did&lt;/em&gt; get it working eventually, so I&#x27;ll share all the code throughout this post!&lt;/p&gt;&#xA;&lt;img alt=&quot;The Arduino UNO r3 with a Deek Robot SD/RTC shield installed&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/ArduinoAndShield.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;The Arduino UNO r3 with a Deek Robot SD/RTC shield installed&quot;&gt;&#xA;&lt;p&gt;&lt;em&gt;Note: I found a warning that when using this particular shield, &amp;quot;If you have a UNO with a USB type B connector this shield may NOT WORK because the male pins are NOT LONG ENOUGH&amp;quot; on a &lt;a href=&quot;https://forum.arduino.cc/index.php?topic=649395.0&quot;&gt;forum page&lt;/a&gt; - my UNO r3 does have the USB B connector but I&#x27;ve not had this problem.. though if you do encounter this problem then maybe some sort of pin extenders or raisers would fix it.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;step-1-writing-to-the-sd-card&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware#step-1-writing-to-the-sd-card&quot;&gt;Step 1: Writing to the SD card&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;After reading around, I settled on a library called &lt;a href=&quot;https://github.com/greiman/SdFat&quot;&gt;SdFat&lt;/a&gt; that should handle the disk access for me. I downloaded it from the Github repo and followed the &amp;quot;Importing a .zip Library&amp;quot; instructions on the &lt;a href=&quot;https://www.arduino.cc/en/guide/libraries&quot;&gt;Installing Additional Arduino Libraries&lt;/a&gt; page.&lt;/p&gt;&#xA;&lt;p&gt;This allowed me to stack the data logging shield on top of the UNO, put an SD card into the shield, connect the UNO to my PC via a USB lead and upload the following code -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;#include &amp;lt;SdFat.h&amp;gt; // https://github.com/greiman/SdFat&#xA;&#xA;// chipSelect = 10 according to &amp;quot;Item description&amp;quot; section of&#xA;// https://www.play-zone.ch/de/dk-data-logging-shield-v1-0.html&#xA;#define SD_CHIP_SELECT 10&#xA;&#xA;void setup() {&#xA;  Serial.begin(9600);&#xA;&#xA;  // See &amp;quot;Note 1&amp;quot; further down about SPI_HALF_SPEED&#xA;  SdFat sd;&#xA;  if (!sd.begin(SD_CHIP_SELECT, SPI_HALF_SPEED)) {&#xA;    Serial.println(&amp;quot;ERROR: sd.begin() failed&amp;quot;);&#xA;  }&#xA;  else {&#xA;    SdFile file;&#xA;    if (!file.open(&amp;quot;TestData.txt&amp;quot;, O_WRITE | O_APPEND | O_CREAT)) {&#xA;      Serial.println(&amp;quot;ERROR: file.open() failed - unable to write&amp;quot;);&#xA;    }&#xA;    else {&#xA;      file.println(&amp;quot;Hi!&amp;quot;);&#xA;      file.close();&#xA;      Serial.println(&amp;quot;Successfully wrote to file!&amp;quot;);&#xA;    }&#xA;  }&#xA;}&#xA;&#xA;void loop() { }&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The Arduino IDE has an option to view the serial output (the messages written to &amp;quot;Serial.println&amp;quot;) by going to Tools / Serial Monitor. Ensure that the baud rate shown near the bottom right of the window is set to 9600 to match the setting in the code above.&lt;/p&gt;&#xA;&lt;p&gt;This happily showed&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Successfully wrote to file!&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;in the Serial Monitor&#x27;s output and when I yanked the card out and put it into my laptop to see if it had worked, it did indeed have a file on it called &amp;quot;TestData.txt&amp;quot; with a single line saying &amp;quot;Hi!&amp;quot; - an excellent start!&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;Note 1: In the &amp;quot;sd.begin&amp;quot; call, I specify &lt;strong&gt;SPI_HALF_SPEED&lt;/strong&gt; primarily because that&#x27;s what most of the examples that I&#x27;ve found use - there is an option &lt;strong&gt;SPI_FULL_SPEED&lt;/strong&gt; but I read in &lt;a href=&quot;https://community.particle.io/t/has-anyone-had-success-hooking-up-an-sd-card-to-the-photon-and-writing-reading-data/24026/41&quot;&gt;an Arduino forum thread&lt;/a&gt; that: &amp;quot;You should be able to use &lt;strong&gt;SPI_FULL_SPEED&lt;/strong&gt; instead, but if that produces communication errors you can use SD_SCK_HZ(4 * MHZ) instead of &lt;strong&gt;SPI_HALF_SPEED&lt;/strong&gt;&amp;quot; and I&#x27;m not sure what might be the limiting factor with said communication errors; whether it&#x27;s the card or the shield or something else and I&#x27;m only going to be writing small amounts of data at relatively infrequent intervals and so I thought that I would err on the safe side and stick with &lt;strong&gt;SPI_HALF_SPEED&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;Note 2: In a lot of code samples, in the &amp;quot;setup&amp;quot; method you will see code after the &amp;quot;Serial.begin(..)&amp;quot; call that looks like this:&lt;/em&gt;&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;while (!Serial) {&#xA;  // wait for serial port to connect - needed for native USB&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;em&gt;^ This is only needed for particular variants of the Arduino - the &amp;quot;Leonardo&amp;quot;, I believe - and is not required for the UNO and so I haven&#x27;t included it in my code.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Gotcha One:&lt;/strong&gt; Initially, I had formatted my SD card (branded as &amp;quot;Elgetec&amp;quot;, who I can&#x27;t remember ever hearing of other than on this card) on my Windows laptop - doing a full format, to make absolutely sure that it was as ready for action as possible. However, not only did that full format take a long time, I found that when I left my Arduino shield writing files over a period of a few hours then it would often get reported as being corrupted when I tried to read it. I&#x27;ve found that if the &lt;a href=&quot;https://github.com/greiman/SdFat/blob/master/examples/SdFormatter/SdFormatter.ino&quot;&gt;SdFormatter.ino&lt;/a&gt; (from the examples folder of the SdFat GitHub repo) is used then these corruption problems have stopped occurring (and the formatting is much faster!).&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Gotcha Two:&lt;/strong&gt; While I was fiddling around with writing to the SD card, particularly when connected to a battery instead of the USB port (where I could use the Serial Monitor to see what was happening), I tried setting the LED_BUILTIN to be on while writing and then go off again when the file was closed. This didn&#x27;t work. And it &lt;em&gt;can&#x27;t&lt;/em&gt; work, though it took me a lot of reading to find out why. It turns out that the SPI (the &lt;a href=&quot;https://www.arduino.cc/en/reference/SPI&quot;&gt;Serial Peripheral Library&lt;/a&gt;) connection from the Arduino to the Deek Robot shield will use IO pins 10, 11, 12 and 13 for its own communications. 13 happens to be the output used to set the LED_BUILTIN state and so you lose access to setting that built-in LED while this shield is connected. Specifically, &amp;quot;&lt;a href=&quot;https://forum.arduino.cc/index.php?topic=533606.msg3637549#msg3637549&quot;&gt;pin 13 is the SPI clock. Pin 13 is also the built-in led and hence you have a conflict&lt;/a&gt;&amp;quot;.&lt;/p&gt;&#xA;&lt;h3 id=&quot;step-2-keeping-time&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware#step-2-keeping-time&quot;&gt;Step 2: Keeping time&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Since I want to record light levels throughout the day, it&#x27;s important to know at what time the recording is being made. The shield that I&#x27;m using also includes an &amp;quot;RTC&amp;quot; (a real-time clock) and so I needed to work out how to set that once and then read from it each time I took a light level reading.&lt;/p&gt;&#xA;&lt;p&gt;The UNO board itself can do some basic form of time keeping, such as telling you how long it&#x27;s been since the board started / was last reset (via the &lt;a href=&quot;https://www.arduino.cc/reference/en/language/functions/time/millis/&quot;&gt;millis()&lt;/a&gt; function) but there are a few limitations with this. You can bake into the compiled code the time at which it was compiled and you &lt;em&gt;could&lt;/em&gt; then use that, in combination with &amp;quot;millis()&amp;quot;, to work out the current time but you will hit problems if power is temporarily lost or if the board is reset (because &amp;quot;millis()&amp;quot; will start from zero again and timing will start again from that baked-in &amp;quot;compiled at&amp;quot; time).&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Gotcha Three:&lt;/strong&gt; I didn&#x27;t realise when I was first fiddling with this that any time you connected the USB lead, it would reset the board and the program (the &amp;quot;sketch&amp;quot;, in Arduino-speak) would start all over again. (This will only make a difference if you&#x27;re using an external power source because otherwise the program would &lt;em&gt;stop&lt;/em&gt; whenever you disconnected the USB lead and there would be nothing running to reset when plugging the USB lead back in! I&#x27;ll be talking about external power supplies further down).&lt;/p&gt;&#xA;&lt;p&gt;So the next step was using the clock on the shield that I had bought, instead of relying on the clock on the Arduino board itself. To do this, I&#x27;d inserted a CR1220 battery and then tested with the following code:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;#include &amp;lt;Wire.h&amp;gt;&#xA;#include &amp;lt;RTClib.h&amp;gt; // https://github.com/adafruit/RTClib&#xA;&#xA;RTC_DS1307 rtc;&#xA;&#xA;void setup() {&#xA;  // The clock won&#x27;t work with this (thanks https://arduino.stackexchange.com/a/44305!)&#xA;  Wire.begin();&#xA;&#xA;  bool rtcWasAlreadyConfigured;&#xA;  if (rtc.isrunning()) {&#xA;    rtcWasAlreadyConfigured = true;&#xA;  }&#xA;  else {&#xA;    rtc.adjust(DateTime(__DATE__, __TIME__));&#xA;    rtcWasAlreadyConfigured = false;&#xA;  }&#xA;&#xA;  Serial.begin(9600);&#xA;&#xA;  if (rtcWasAlreadyConfigured) {&#xA;    Serial.println(&amp;quot;setup: RTC is already running&amp;quot;);&#xA;  }&#xA;  else {&#xA;    Serial.println(&amp;quot;setup: RTC was not running, so it was set to the time of compilation&amp;quot;);&#xA;  }&#xA;}&#xA;&#xA;void loop() {&#xA;  DateTime now = rtc.now();&#xA;  Serial.print(&amp;quot;Year: &amp;quot;);&#xA;  Serial.print(now.year());&#xA;  Serial.print(&amp;quot; Month: &amp;quot;);&#xA;  Serial.print(now.month());&#xA;  Serial.print(&amp;quot; Day: &amp;quot;);&#xA;  Serial.print(now.day());&#xA;  Serial.print(&amp;quot; Hour: &amp;quot;);&#xA;  Serial.print(now.hour());&#xA;  Serial.print(&amp;quot; Minutes: &amp;quot;);&#xA;  Serial.print(now.minute());&#xA;  Serial.print(&amp;quot; Seconds: &amp;quot;);&#xA;  Serial.print(now.second());&#xA;  Serial.println();&#xA;&#xA;  delay(1000);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The first time you run this, you&#x27;ll see the first line say:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;setup: RTC was not running, so it was set to the time of compilation&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;.. and then you&#x27;ll see the date and time shown every second.&lt;/p&gt;&#xA;&lt;p&gt;If you remove the USB cable and then re-insert it then you&#x27;ll see the message:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;setup: RTC is already running&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;.. and then the date and time will continue to show every second &lt;em&gt;and it will be the correct date and time&lt;/em&gt; (it won&#x27;t have reset each time that the USB cable is connected and the &amp;quot;setup&amp;quot; function is run again).&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Gotcha Four:&lt;/strong&gt; When disconnecting and reconnecting the USB lead, sometimes (if not always) I need to close the Serial Monitor and then re-open it otherwise it won&#x27;t update and it will say that the COM port is busy if I try to upload a sketch to the board.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Gotcha Five:&lt;/strong&gt; I&#x27;ve seen a lot of examples use &amp;quot;RTC_Millis&amp;quot; instead of &amp;quot;RTC_DS1307&amp;quot; in timing code samples. This is &lt;em&gt;not&lt;/em&gt; what we want! That is a timer that is simulated by the board and it just uses the &amp;quot;millis()&amp;quot; function to track time which, as I explained earlier, is no good for persisting time across resets. We want to use &amp;quot;RTC_DS1307&amp;quot; because that uses the RTC on the shield, which &lt;em&gt;will&lt;/em&gt; maintain the time between power cycles due to the battery on the board.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Gotcha Six:&lt;/strong&gt; If you don&#x27;t include &amp;quot;Wire.h&amp;quot; and call &amp;quot;Wire.begin();&amp;quot; at the start of setup then the RTC won&#x27;t work properly and you will always get the same weird date displayed when you read it:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Year: 2165 Month: 165 Day: 165 Hour: 165 Minutes: 165 Seconds: 85&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&quot;step-3-an-external-power-source&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware#step-3-an-external-power-source&quot;&gt;Step 3: An external power source&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;So far, the board has only been powered up when connected to the USB lead but this is not the only option. There are a few approaches that you can take; a regulated 5V input, the barrel-shaped power jack and the option of applying power to the vin and gnd pins on the board.&lt;/p&gt;&#xA;&lt;p&gt;The power jack makes most sense when you are connecting to some sort of wall wart but I want a &amp;quot;disconnected&amp;quot; power supply for outside. I did a bunch of reading on this and some people are just connecting a simple 9V battery to the vin/gnd pins but apparently that&#x27;s not very efficient - the amount of power stored in a standard MN1604 9V battery (the common kind that you might use in a smoke alarm) is comparatively low and the vin/gnd pins will be happy with something in the 6V-12V range and there is said to be more loss in regulating 9V to the internal 5V than there would be from a 6V supply.&lt;/p&gt;&#xA;&lt;p&gt;So I settled on a rechargable 6V sealed lead acid battery, which I believe is often used in big torches or in remote control cars. I got one for &#xA3;8 delivered from ebay that is stated to have 4.5Ah (which is a measure, essentially, of how much energy it stores) - for reference, a 9V battery will commonly have about 0.5Ah and so will run out much more quickly. Whatever battery you select, there are ways to eke out more life from them, which I&#x27;ll cover shortly.&lt;/p&gt;&#xA;&lt;p&gt;It&#x27;s completely safe to connect the battery to the vin/gnd ports at the same time as the USB lead is inserted, so you don&#x27;t have to worry about only providing power by the battery &lt;em&gt;or&lt;/em&gt; the USB lead and you can safely connect and disconnect the USB lead while the battery is connected as often as you like.&lt;/p&gt;&#xA;&lt;h3 id=&quot;step-4-capturing-light-levels&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware#step-4-capturing-light-levels&quot;&gt;Step 4: Capturing light levels&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The starter kit that I&#x27;ve got conveniently included an LDR (a &amp;quot;Light Dependent Resistor&amp;quot; aka a &amp;quot;photo-resistor&amp;quot;) and so I just had to work out how to connect that. I knew that the Arduino has a range of digital input/output pins and that it has some analog input pins but I had to remind myself of some basic electronics to put it all together.&lt;/p&gt;&#xA;&lt;p&gt;What you &lt;em&gt;can&#x27;t&lt;/em&gt; do is just put 5V into one pin of the LDR and connect the other end of the LDR straight into an analog pin. I&#x27;m going to try to make a stab at a simple explanation here and then refer you to someone who can explain it better!&lt;/p&gt;&#xA;&lt;p&gt;The analog pin will read a voltage value from between 0 and 5V and allow this to be read in code as a numeric value between 0 and 1023 (inclusive). When we talk about the 5V output pin, this only makes sense in the context of the ground of the board - so the concept of a 5V output with no gnd pin connection makes no sense, there is nothing for that 5V to be measured relative to. So what we need to do is use the varying resistance of the LDR and somehow translate that into a varying voltage to provide to an analog pin (I chose A0 in my build).&lt;/p&gt;&#xA;&lt;p&gt;The way to do this is with a &amp;quot;voltage divider&amp;quot;, which is essentially a circuit that looks a bit like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;gnd &amp;lt;--&amp;gt; resistor &amp;lt;--&amp;gt; connection-to-analog-input &amp;lt;--&amp;gt; LDR &amp;lt;--&amp;gt; 5V&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;If the resistance of the LDR happens to precisely match that resistance of the fixed resistor then precisely 2.5V will be delivered to the analog input. But if the LDR resistance is higher or lower than the fixed resistor&#x27;s value then a higher or lower voltage will be delivered to analog pin.&lt;/p&gt;&#xA;&lt;p&gt;There is a &lt;a href=&quot;https://learn.adafruit.com/photocells/using-a-photocell&quot;&gt;tutorial on learn.adafruit.com&lt;/a&gt; that does a much better job of explaining it! It also suggests what fixed resistor values that you might use for different environments (eg. are you more interested in granular light level readings at low light levels but don&#x27;t mind saturation at high levels or are you more interested in more granular readings at high levels and less granular at lower?) - at the moment, I&#x27;m still experimenting with a few different fixed resistor values to see which ones work for my particular climate.&lt;/p&gt;&#xA;&lt;p&gt;The shield that I&#x27;m using solder pads for mounting components onto but I wasn&#x27;t brave enough for that, so I&#x27;ve been using the pass-through pins and connecting them to the bread board that came with my starter kit.&lt;/p&gt;&#xA;&lt;p&gt;When it&#x27;s not connected to a power supply, it looks a bit like this:&lt;/p&gt;&#xA;&lt;img alt=&quot;The Arduino-plus-shield connected to an LDR on a breadboard&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/ArduinoWithBreadboardAlongside.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;The Arduino-plus-shield connected to an LDR on a breadboard&quot;&gt;&#xA;&lt;p&gt;The code to read the light level value looks like this (while running this code, try slowly moving your hand closer and further from covering the sensor to see the value change when it&#x27;s read each second) -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;void setup() {&#xA;  Serial.begin(9600);&#xA;}&#xA;&#xA;void loop() {&#xA;  Serial.print(&amp;quot;Light level reading: &amp;quot;);&#xA;  Serial.print(analogRead(0));&#xA;  Serial.println();&#xA;&#xA;  delay(1000);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;In an effort to start putting all of this together into a more robust package, I picked up a pack of self-adhesive felt pads from the supermarket and stuck them to appropriate points under the breadboard -&lt;/p&gt;&#xA;&lt;img alt=&quot;Felt pads to more easily align the breadboard on top of the Arduino and shield&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/ArduinoBreadboardFeltPads.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Felt pads to more easily align the breadboard on top of the Arduino and shield&quot;&gt;&#xA;&lt;img alt=&quot;Felt pads attached to the underside of the breadboard&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/ArduinoBreadboardWithFeltPadsAttached.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Felt pads attached to the underside of the breadboard&quot;&gt;&#xA;&lt;p&gt;.. and then I secured it all together with an elastic band:&lt;/p&gt;&#xA;&lt;img alt=&quot;Arduino plus shield plus breadboard secured in a tower&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/ArduinoTower.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Arduino plus shield plus breadboard secured in a tower&quot;&gt;&#xA;&lt;h3 id=&quot;step-5-sleeping-when-not-busy&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware#step-5-sleeping-when-not-busy&quot;&gt;Step 5: Sleeping when not busy&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;In my ideal dream world, I would be able to leave my light level monitoring box outside for a few months. As I explained earlier, due to the direction that my garden faces, the hours at which the sun hits it fully varies by several hours depending upon the time of year. However, NO battery is going to last forever and even with this 4.5Ah battery that is at a 6V output (which is only a small jump down to regulate to 5V), the time that it can keep things running is limited.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;Note: Recharging via a solar panel sounds interesting but it&#x27;s definitely a future iteration possibility at this point!&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;There are, however, some things that can be done to eke out the duration of the battery by reducing the power usage of the board. There are ways to put the board into a &amp;quot;power down&amp;quot; state where it will do less - its timers will stop and its CPU can have a rest. There are tutorials out there about how to put it into this mode and have it only wake up on an &amp;quot;interrupt&amp;quot;, which can be an external circuit setting an input pin (maybe somehow using the RTC on the shield I&#x27;m using) &lt;em&gt;or&lt;/em&gt; using something called the &amp;quot;&lt;a href=&quot;https://create.arduino.cc/projecthub/rafitc/what-is-watchdog-timer-fffe20&quot;&gt;Watchdog Timer&lt;/a&gt;&amp;quot; that stays running on the Arduino even when it&#x27;s in power down mode.&lt;/p&gt;&#xA;&lt;p&gt;I read &lt;em&gt;a lot&lt;/em&gt; of posts and tutorials on this and I really struggled to get it to work. Until, finally, I came across this one: &lt;a href=&quot;https://circuitdigest.com/microcontroller-projects/arduino-sleep-modes-and-how-to-use-them-to-reduce-power-consumption&quot;&gt;Arduino Sleep Modes and How to use them to Save the Power&lt;/a&gt;. It explains in a clear table the difference between the different power-reduced modes (idle, power-save, power-down, etc..) &lt;em&gt;and&lt;/em&gt; it recommends a library called &amp;quot;&lt;a href=&quot;https://github.com/rocketscream/Low-Power&quot;&gt;Low-Power&lt;/a&gt;&amp;quot; that takes all of the hard work out of it.&lt;/p&gt;&#xA;&lt;p&gt;Whereas other tutorials talked about calling &amp;quot;sleep_enable()&amp;quot; and &amp;quot;set_sleep_mode(..)&amp;quot; functions and then using &amp;quot;attachInterrupt(..)&amp;quot; and adding some magic method to then undo all of those things, this library allows you to write a one-liner as follows:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This will cause the board to go into its most power-saving mode for eight seconds (which is the longest that&#x27;s possible when relying upon its internal Watchdog Timer to wake it up).&lt;/p&gt;&#xA;&lt;p&gt;No muss, no fuss.&lt;/p&gt;&#xA;&lt;p&gt;I haven&#x27;t measured yet how long that my complete device can sit outside in its waterproof box on a single charge of a battery but I&#x27;m confident that it&#x27;s definitely measured in days, not hours - and that was &lt;em&gt;before&lt;/em&gt; introducing this &amp;quot;LowPower.powerDown(..)&amp;quot; call.&lt;/p&gt;&#xA;&lt;p&gt;Since I only want a reading every 30-60s, I call &amp;quot;LowPower.powerDown(..)&amp;quot; in a loop so that there are several 8s power down delays. While I haven&#x27;t confirmed this yet, I would be astonished if it didn&#x27;t last &lt;em&gt;at least&lt;/em&gt; a week out there on one charge. And if I have to bring it in some nights (when it&#x27;s dark and I don&#x27;t care about light measurements) to charge it, then that&#x27;s fine by me (though I&#x27;d like to be as infrequently as possible).&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Gotcha Seven:&lt;/strong&gt; When entering power-down mode, if you are connected to the USB port in order to use the Serial Monitor to watch what&#x27;s going on, ensure that you call &amp;quot;Serial.flush()&amp;quot; before entering power-down, otherwise the message might get buffered up and not fully sent through the serial connection before the board takes a nap.&lt;/p&gt;&#xA;&lt;h3 id=&quot;step-6-preparing-for-the-outdoors-in-the-british-weather&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware#step-6-preparing-for-the-outdoors-in-the-british-weather&quot;&gt;Step 6: Preparing for the outdoors (in the British weather!)&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I always associate the brand &amp;quot;&lt;a href=&quot;https://www.independent.co.uk/life-style/food-and-drink/how-tupperware-s-fate-was-sealed-a7899771.html&quot;&gt;Tupperware&lt;/a&gt;&amp;quot; as being a very British thing - it&#x27;s what we get packed lunches put into and what we get takeaway curries in. At least, I &lt;em&gt;think&lt;/em&gt; that it is - maybe it&#x27;s like &amp;quot;hoover&amp;quot;, where everyone uses the phrase &amp;quot;hoover&amp;quot; when they mean their generic vacuum cleaner. Regardless the origin, this seemed like the simplest way to make my device waterproof. The containers are not completely transparent but they shouldn&#x27;t make a significant impact on the light levels being recorded by the photo-resistor because they&#x27;re also far from opaque. And these containers are sealable, waterproof and come in all shapes and sizes!&lt;/p&gt;&#xA;&lt;p&gt;I took my elastic-band-wrapped &amp;quot;stack&amp;quot; of Arduino-plus-shield-plus-breadboard and connected it to the battery -&lt;/p&gt;&#xA;&lt;img alt=&quot;The Arduino &#x27;stack&#x27; connected to a battery&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/ArduinoStackWithBattery.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;The Arduino &#x27;stack&#x27; connected to a battery&quot;&gt;&#xA;&lt;p&gt;.. and put in a plastic box. By turning the battery so that it was length-side-up, it was quite a snug fit and meant that the battery wouldn&#x27;t slide around inside the box. There wasn&#x27;t a lot of space for the stack to move around and so it seemed like quite a secure arrangement:&lt;/p&gt;&#xA;&lt;img alt=&quot;The Arduino &#x27;stack&#x27; and battery in its waterproof container&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/ArduinoInBox.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;The Arduino &#x27;stack&#x27; and battery in its waterproof container&quot;&gt;&#xA;&lt;h3 id=&quot;step-7-the-final-code&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware#step-7-the-final-code&quot;&gt;Step 7: The final code&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;So far, each code sample has demonstrated &lt;em&gt;aspects&lt;/em&gt; of what I want to do but now it&#x27;s time to bring it all fully together.&lt;/p&gt;&#xA;&lt;p&gt;In trying to write the following code, I was reminded how much I&#x27;ve taken for granted in C# (and other higher level languages) with their string handling! I tried a little C and C&#x2B;&#x2B; &lt;em&gt;maaaaany&lt;/em&gt; years ago and so writing Arduino code was a bit of a throwback for me - at first, I was trying to make a char array for a filename and I set the length of the array to be the number of characters that were required for the filename.. silly me, I had forgotten that C strings need to be null-terminated and so you need an extra zero character at the end in order for things to work properly! Failing to do so would not result in a compile or run time error, it would just mean that the files weren&#x27;t written properly. Oh, how I&#x27;ve been spoilt! But, on the other hand, it also feels kinda good being this close to the &amp;quot;bare metal&amp;quot; :)&lt;/p&gt;&#xA;&lt;p&gt;The following sketch will record the light level about twice a minute to a file on the SD card where the filename is based upon the current date (as maintained by the RTC module and its CR1220 battery) -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;#include &amp;lt;Wire.h&amp;gt;&#xA;#include &amp;lt;SdFat.h&amp;gt;    // https://github.com/greiman/SdFat&#xA;#include &amp;lt;RTClib.h&amp;gt;   // https://github.com/adafruit/RTClib&#xA;#include &amp;lt;LowPower.h&amp;gt; // https://github.com/rocketscream/Low-Power&#xA;&#xA;// chipSelect = 10 according to &amp;quot;Item description&amp;quot; section of&#xA;// https://www.play-zone.ch/de/dk-data-logging-shield-v1-0.html&#xA;#define SD_CHIP_SELECT 10&#xA;&#xA;RTC_DS1307 rtc;&#xA;&#xA;void setup() {&#xA;  // The clock won&#x27;t work with this (thanks https://arduino.stackexchange.com/a/44305!)&#xA;  Wire.begin();&#xA;&#xA;  bool rtcWasAlreadyConfigured;&#xA;  if (rtc.isrunning()) {&#xA;    rtcWasAlreadyConfigured = true;&#xA;  }&#xA;  else {&#xA;    rtc.adjust(DateTime(__DATE__, __TIME__));&#xA;    rtcWasAlreadyConfigured = false;&#xA;  }&#xA;&#xA;  Serial.begin(9600);&#xA;&#xA;  if (rtcWasAlreadyConfigured) {&#xA;    Serial.println(&amp;quot;setup: RTC is already running&amp;quot;);&#xA;  }&#xA;  else {&#xA;    Serial.println(&amp;quot;setup: RTC was not running, so it was set to the time of compilation&amp;quot;);&#xA;  }&#xA;}&#xA;&#xA;void loop() {&#xA;  // Character arrays need to be long enough to store the number of &amp;quot;real&amp;quot; characters plus the&#xA;  // null terminator&#xA;  char filename[13]; // yyyyMMdd.txt = 12 chars &#x2B; 1 null&#xA;  char timestamp[9]; // 00:00:00     =  8 chars &#x2B; 1 null&#xA;  DateTime now = rtc.now();&#xA;  snprintf(filename, sizeof(filename), &amp;quot;%04u%02u%02u.txt&amp;quot;, now.year(), now.month(), now.day());&#xA;  snprintf(timestamp, sizeof(timestamp), &amp;quot;%02u:%02u:%02u&amp;quot;, now.hour(), now.minute(), now.second());&#xA;&#xA;  int sensorValue = analogRead(0);&#xA;&#xA;  Serial.print(filename);&#xA;  Serial.print(&amp;quot; &amp;quot;);&#xA;  Serial.print(timestamp);&#xA;  Serial.print(&amp;quot; &amp;quot;);&#xA;  Serial.println(sensorValue);&#xA;&#xA;  SdFat sd;&#xA;  if (!sd.begin(SD_CHIP_SELECT, SPI_HALF_SPEED)) {&#xA;    Serial.println(&amp;quot;ERROR: sd.begin() failed&amp;quot;);&#xA;  }&#xA;  else {&#xA;    SdFile file;&#xA;    if (!file.open(filename, O_WRITE | O_APPEND | O_CREAT)) {&#xA;      Serial.println(&amp;quot;ERROR: file.open() failed - unable to write&amp;quot;);&#xA;    }&#xA;    else {&#xA;      file.print(timestamp);&#xA;      file.print(&amp;quot; Sensor value: &amp;quot;);&#xA;      file.println(sensorValue);&#xA;      file.close();&#xA;    }&#xA;  }&#xA;&#xA;  Serial.flush(); // Ensure we finish sending serial messages before going to sleep&#xA;&#xA;  // 4x 8s is close enough to a reading every 30s, which gives me plenty of data&#xA;  // - Using this instead of &amp;quot;delay&amp;quot; should mean that the battery will power the device for longer&#xA;  for (int i = 0; i &amp;lt; 3; i&#x2B;&#x2B;) {&#xA;    LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);&#xA;  }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;At the moment, I&#x27;m bringing the box inside each night and then disconnecting the battery, pulling out the card and looking at the values recorded in the file to see if it&#x27;s clear when the sun was fully hitting the table that I had placed the box on.&lt;/p&gt;&#xA;&lt;p&gt;I&#x27;ve only started doing this in the last couple of days and each day has been rather grey and so there haven&#x27;t been any sunny periods so that I can confirm that the readings clearly distinguish between &amp;quot;regular daylight&amp;quot; and &amp;quot;sun directly on the table&amp;quot;. Once I get some sun again, I&#x27;ll be able to get a better idea - and if I &lt;em&gt;can&#x27;t&lt;/em&gt; distinguish well enough then I&#x27;ll adjust the pull-down resistor that splits the voltage with the LDR and keep experimenting!&lt;/p&gt;&#xA;&lt;p&gt;When I&#x27;m happy with the configuration, &lt;em&gt;then&lt;/em&gt; I&#x27;ll start experimenting with leaving the box outside for longer to see how long this battery can last in conjunction with the &amp;quot;LowPower.powerDown(..)&amp;quot; calls. One obvious optimisation for my use case would be to continue keeping it in power-down mode between the hours of 10pm and 8am - partly because I know that it will definitely be dark after 10pm and partly because I am &lt;em&gt;not&lt;/em&gt; a morning person and so would not want to be outside before 8am, even if it &lt;em&gt;was&lt;/em&gt; streaming with light (which it wouldn&#x27;t be due to when my yard actually gets direct sunlight).&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Gotcha Eight:&lt;/strong&gt; The RTC has no awareness of daylight savings time and so I&#x27;ll need to take this into account when the clocks change in the UK. I&#x27;ll worry about this another day!&lt;/p&gt;&#xA;&lt;h3 id=&quot;step-8-draw-some-graphs-one-day&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/monitoring-my-gardens-limited-sunlight-time-period-with-an-arduino-and-some-tupperware#step-8-draw-some-graphs-one-day&quot;&gt;Step 8: Draw some graphs (one day)&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;As you can tell from the above, I&#x27;m still very much in the early phases of gathering data. But, at some point, I&#x27;m going to have to &lt;em&gt;use&lt;/em&gt; this data to predict when the yard will get sun for future dates - once I&#x27;ve got a few months of data for different times of year, hopefully I&#x27;ll be able to do so!&lt;/p&gt;&#xA;&lt;p&gt;I foresee a little bit of data-reading and Excel-graph-drawing in my future! There&#x27;s just something about seeing &lt;a href=&quot;https://www.productiverage.com/when-a-disk-cache-performs-better-than-an-inmemory-cache-befriending-the-net-gc&quot;&gt;results on a graph&lt;/a&gt; that make everything feel so much more real. As much as I&#x27;d like to be able to stare at 1000s of numbers and read them like the Matrix, seeing trends and curves plotted out just feels so much more satisfying and definitive. Maybe there will be a follow-up post with the results, though I feel that they would be much more personal and less useful to the general populace than even &lt;em&gt;my&lt;/em&gt; standard level of esoteric and niche blog posts! Maybe there are some graphs in my Twitter stream&#x27;s future!&lt;/p&gt;&#xA;&lt;p&gt;On the other hand.. if I learn any more power-saving techniques or have any follow-up information about how long these rechargeable torch-or-remote-control batteries last then maybe &lt;em&gt;that&lt;/em&gt; will be grounds for a follow-up!&lt;/p&gt;&#xA;&lt;p&gt;In the meantime, I hope you&#x27;ve enjoyed this little journey - and if you&#x27;ve tried to do anything similar with these cheap Deek Robot boards, then maybe the code samples here have been of use to you. I hope so! (Because, goodness knows, feeling like a beginner again and getting onto those new forums has been &lt;em&gt;quite&lt;/em&gt; an experience!)&lt;/p&gt;&#xA;&lt;div class=&quot;Related&quot;&gt;&lt;h3&gt;You may also be interested in (see &lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;here&lt;/a&gt; for information about how these are generated):&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts-part-2&quot;&gt;Automating &amp;quot;suggested / related posts&amp;quot; links for my blog posts - Part 2&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;Automating &amp;quot;suggested / related posts&amp;quot; links for my blog posts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/javascript-compression-putting-my-json-search-indexes-on-a-diet&quot;&gt;JavaScript Compression (Putting my JSON Search Indexes on a diet)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
                <pubDate>Sat, 22 Aug 2020 21:34:00 GMT</pubDate>
            </item>
            <item>
                <title>How are barcodes read?? (Library-less image processing in C#)</title>
                <link>https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp</link>
                <guid>https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp</guid>
                <description>&lt;p&gt;I&#x27;ve been using MyFitnessPal and it has the facility to load nutrition information by scanning the barcode on the product. I can guess how the retrieval works once the barcode number is obtained (a big database somewhere) but it struck me that I had no idea how the reading of the barcode &lt;em&gt;itself&lt;/em&gt; worked and.. well, I&#x27;m curious and enjoy the opportunity to learn something new to me by writing the code to do it. I do enjoy being able to look up (almost) anything on the internet to find out how it works!&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(For anyone who wants to either play along but not copy-paste the code themselves or for anyone who wants to jump to the end result, I&#x27;ve put the code - along with the example image I&#x27;ve used in this post - up on a &lt;a href=&quot;https://github.com/ProductiveRage/BarcodeReader&quot;&gt;GitHub repo&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;the-plan-of-attack&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp#the-plan-of-attack&quot;&gt;The plan of attack&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;There are two steps required here:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Read image and try to identify areas that look like barcodes&lt;/li&gt;&#xA;&lt;li&gt;Try to extract numbers from the looks-like-a-barcode regions&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;As with anything, these steps may be broken down into smaller tasks. The first step can be done like this:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Barcodes are black and white regions that have content that has steep &amp;quot;gradients&amp;quot; in image intensity horizontally (where there is a change from a black bar to a white space) and little change in intensity vertically (as each bar is a vertical line), so first we greyscale the image and then generate horizontal and vertical intensity gradients values for each point in the image and combine the values by subtracting vertical gradient from horizontal gradient&lt;/li&gt;&#xA;&lt;li&gt;These values are normalised so that they are all on the scale zero to one - this data could be portrayed as another greyscale image where the brightest parts are most likely to be within barcodes&lt;/li&gt;&#xA;&lt;li&gt;These values are then &amp;quot;spread out&amp;quot; or &amp;quot;blurred&amp;quot; and then a threshold value is applied where every value about it is changed into a 1 and every value below it a 0&lt;/li&gt;&#xA;&lt;li&gt;This &amp;quot;mask&amp;quot; (where every value is a 0 or 1) should have identified many of the pixels within the barcodes and we want to group these pixels into distinct objects&lt;/li&gt;&#xA;&lt;li&gt;There is a chance, though, that there could be gaps between bars that mean that a single barcode is spread across multiple masked-out objects and we need to try to piece them back together into one area (since the bars are tall and narrow, this may be done by considering a square area over every object and then combining objects whose squared areas overlap into one)&lt;/li&gt;&#xA;&lt;li&gt;This process will result in a list of areas that may be barcodes - any that are taller than they are wide are ignored (because barcode regions are always wider than they are tall)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The second step can be broken down into:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Take the maybe-barcode region of the image, greyscale it and then turn into a mask by setting any pixel with an intensity less than a particular threshold to zero and otherwise to one&lt;/li&gt;&#xA;&lt;li&gt;Take a horizontal slice across the image region - all of the pixels on the first row of the image - and change the zero-or-one raw data into a list of line lengths where a new line starts at any transition from zero-to-one or one-to-zero (so &amp;quot;01001000110&amp;quot; becomes &amp;quot;1,1,2,1,3,2,1&amp;quot; because there is 1x zero and then 1x one and then 2x zero and then 1x one, etc..)&lt;/li&gt;&#xA;&lt;li&gt;These line lengths should correspond to bar sizes (and space-between-bar sizes) if we&#x27;ve found a barcode - so run the values through the magic barcode bar-size-reading algorithm (see section 2.1 in &lt;a href=&quot;https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2859730/&quot;&gt;https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2859730/&lt;/a&gt;) and if we get a number (and the checksum is correct) then we&#x27;re done, hurrah!&lt;/li&gt;&#xA;&lt;li&gt;If we couldn&#x27;t get a number from this horizontal slice then move one pixel down and go back around&lt;/li&gt;&#xA;&lt;li&gt;If it was not possible to extract a number from any of the slices through the image region then it&#x27;s either not a barcode or it&#x27;s somehow so distorted in the image that we can&#x27;t read it&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;This approach is fairly resilient to changes in lighting and orientation because the barcode regions are still likely to have the highest horizontal intensity gradient whether the image is dark or light (and even if &lt;em&gt;part&lt;/em&gt; of the image is light and part of it is dark) and the barcode-reading algorithm works on ratios of bar/space-between-bar widths and these remain constant if the image is rotated.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(Some of the techniques are similar to things that I did in my &lt;a href=&quot;https://www.productiverage.com/face-or-no-face-finding-faces-in-photos-using-c-sharp-and-accordnet&quot;&gt;Face or no face (finding faces in photos using C# and Accord.NET)&lt;/a&gt; and so I&#x27;ll be using some of the same code shortly that I described then)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;identifying-maybe-barcode-images&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp#identifying-maybe-barcode-images&quot;&gt;Identifying maybe-barcode images&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Let&#x27;s this image as an example to work with (peanut butter.. I &lt;em&gt;do&lt;/em&gt; love peanut butter) -&lt;/p&gt;&#xA;&lt;img alt=&quot;Delicious peanut butter&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Delicious peanut butter&quot;&gt;&#xA;&lt;p&gt;Before looking at any code, let&#x27;s visualise the process.&lt;/p&gt;&#xA;&lt;p&gt;We&#x27;re going to consider horizontal and vertical gradient intensity maps - at every point in the image we either look to the pixels to the left and to the right (for the horizontal gradient) or we look at the pixels above and below (for the vertical gradient) and the larger the change, the brighter the pixel in the gradient intensity map&lt;/p&gt;&#xA;&lt;img alt=&quot;Horizontal gradient intensity&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-HorizontalAndVerticalGradients.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Horizontal gradient intensity&quot;&gt;&#xA;&lt;p&gt;And when they&#x27;re combined by subtracting the vertical gradient at each point from the horizontal gradient, it looks lke this:&lt;/p&gt;&#xA;&lt;img alt=&quot;Combined gradient intensity&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-CombinedGradients.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Combined gradient intensity&quot;&gt;&#xA;&lt;p&gt;If this image is blurred then we get this:&lt;/p&gt;&#xA;&lt;img alt=&quot;Blurred combined gradient intensity&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-Blurred.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Blurred combined gradient intensity&quot;&gt;&#xA;&lt;p&gt;.. and if we create a binary mask by saying &amp;quot;normalise the intensity values so that their range goes from zero (for the darkest pixel) to one (for the brightest) and then set any pixels that are in the bottom third in terms of intensity to 0 and set the rest to 1&amp;quot; then we get this:&lt;/p&gt;&#xA;&lt;img alt=&quot;Mask of possibly-part-of-a-barcode areas&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-Mask.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Mask of possibly-part-of-a-barcode areas&quot;&gt;&#xA;&lt;p&gt;If each distinct area (where an &amp;quot;area&amp;quot; means &amp;quot;a group of pixels that are connected&amp;quot;) is identified and squares overlaid and centered around the areas then we see this:&lt;/p&gt;&#xA;&lt;img alt=&quot;Mask of possibly-part-of-a-barcode areas, extended into squared areas&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-MaskSquares.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Mask of possibly-part-of-a-barcode areas, extended into squared areas&quot;&gt;&#xA;&lt;p&gt;.. and if the areas whose bounding squares overlap are combined and then cropped around the white pixels then we end up with this:&lt;/p&gt;&#xA;&lt;img alt=&quot;Combined possibly-a-barcode areas&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-MaskSquaresCombined.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Combined possibly-a-barcode areas&quot;&gt;&#xA;&lt;p&gt;This has identified the area around the barcode and also two tiny other areas - when we come to trying to read barcode numbers out of these, the tiny regions will result in no value while the area around the genuine barcode content &lt;em&gt;should&lt;/em&gt; result in a number successfully being read. But I&#x27;m getting ahead of myself.. let&#x27;s look at the code required to perform the above transformations.&lt;/p&gt;&#xA;&lt;p&gt;I&#x27;m going to start with a &lt;strong&gt;DataRectangle&lt;/strong&gt; for performing transformations -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;public static class DataRectangle&#xA;{&#xA;    public static DataRectangle&amp;lt;T&amp;gt; For&amp;lt;T&amp;gt;(T[,] values) =&amp;gt; new DataRectangle&amp;lt;T&amp;gt;(values);&#xA;}&#xA;&#xA;public sealed class DataRectangle&amp;lt;T&amp;gt;&#xA;{&#xA;    private readonly T[,] _protectedValues;&#xA;    public DataRectangle(T[,] values) : this(values, isolationCopyMayBeBypassed: false) { }&#xA;    private DataRectangle(T[,] values, bool isolationCopyMayBeBypassed)&#xA;    {&#xA;        if ((values.GetLowerBound(0) != 0) || (values.GetLowerBound(1) != 0))&#xA;            throw new ArgumentException(&amp;quot;Both dimensions must have lower bound zero&amp;quot;);&#xA;        var arrayWidth = values.GetUpperBound(0) &#x2B; 1;&#xA;        var arrayHeight = values.GetUpperBound(1) &#x2B; 1;&#xA;        if ((arrayWidth == 0) || (arrayHeight == 0))&#xA;            throw new ArgumentException(&amp;quot;zero element arrays are not supported&amp;quot;);&#xA;&#xA;        Width = arrayWidth;&#xA;        Height = arrayHeight;&#xA;&#xA;        if (isolationCopyMayBeBypassed)&#xA;            _protectedValues = values;&#xA;        else&#xA;        {&#xA;            _protectedValues = new T[Width, Height];&#xA;            Array.Copy(values, _protectedValues, Width * Height);&#xA;        }&#xA;    }&#xA;&#xA;    /// &amp;lt;summary&amp;gt;&#xA;    /// This will always be greater than zero&#xA;    /// &amp;lt;/summary&amp;gt;&#xA;    public int Width { get; }&#xA;&#xA;    /// &amp;lt;summary&amp;gt;&#xA;    /// This will always be greater than zero&#xA;    /// &amp;lt;/summary&amp;gt;&#xA;    public int Height { get; }&#xA;&#xA;    public T this[int x, int y]&#xA;    {&#xA;        get&#xA;        {&#xA;            if ((x &amp;lt; 0) || (x &amp;gt;= Width))&#xA;                throw new ArgumentOutOfRangeException(nameof(x));&#xA;            if ((y &amp;lt; 0) || (y &amp;gt;= Height))&#xA;                throw new ArgumentOutOfRangeException(nameof(y));&#xA;            return _protectedValues[x, y];&#xA;        }&#xA;    }&#xA;&#xA;    public IEnumerable&amp;lt;Tuple&amp;lt;Point, T&amp;gt;&amp;gt; Enumerate(Func&amp;lt;Point, T, bool&amp;gt;? optionalFilter = null)&#xA;    {&#xA;        for (var x = 0; x &amp;lt; Width; x&#x2B;&#x2B;)&#xA;        {&#xA;            for (var y = 0; y &amp;lt; Height; y&#x2B;&#x2B;)&#xA;            {&#xA;                var value = _protectedValues[x, y];&#xA;                var point = new Point(x, y);&#xA;                if (optionalFilter?.Invoke(point, value) ?? true)&#xA;                    yield return Tuple.Create(point, value);&#xA;            }&#xA;        }&#xA;    }&#xA;&#xA;    public DataRectangle&amp;lt;TResult&amp;gt; Transform&amp;lt;TResult&amp;gt;(Func&amp;lt;T, TResult&amp;gt; transformer)&#xA;    {&#xA;        return Transform((value, coordinates) =&amp;gt; transformer(value));&#xA;    }&#xA;&#xA;    public DataRectangle&amp;lt;TResult&amp;gt; Transform&amp;lt;TResult&amp;gt;(Func&amp;lt;T, Point, TResult&amp;gt; transformer)&#xA;    {&#xA;        var transformed = new TResult[Width, Height];&#xA;        for (var x = 0; x &amp;lt; Width; x&#x2B;&#x2B;)&#xA;        {&#xA;            for (var y = 0; y &amp;lt; Height; y&#x2B;&#x2B;)&#xA;                transformed[x, y] = transformer(_protectedValues[x, y], new Point(x, y));&#xA;        }&#xA;        return new DataRectangle&amp;lt;TResult&amp;gt;(transformed, isolationCopyMayBeBypassed: true);&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;And then I&#x27;m going to add a way to load image data into this structure -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;public static class BitmapExtensions&#xA;{&#xA;    /// &amp;lt;summary&amp;gt;&#xA;    /// This will return values in the range 0-255 (inclusive)&#xA;    /// &amp;lt;/summary&amp;gt;&#xA;    // Based on http://stackoverflow.com/a/4748383/3813189&#xA;    public static DataRectangle&amp;lt;double&amp;gt; GetGreyscale(this Bitmap image)&#xA;    {&#xA;        var values = new double[image.Width, image.Height];&#xA;        var data = image.LockBits(&#xA;            new Rectangle(0, 0, image.Width, image.Height),&#xA;            ImageLockMode.ReadOnly,&#xA;            PixelFormat.Format24bppRgb&#xA;        );&#xA;        try&#xA;        {&#xA;            var pixelData = new Byte[data.Stride];&#xA;            for (var lineIndex = 0; lineIndex &amp;lt; data.Height; lineIndex&#x2B;&#x2B;)&#xA;            {&#xA;                Marshal.Copy(&#xA;                    source: data.Scan0 &#x2B; (lineIndex * data.Stride),&#xA;                    destination: pixelData,&#xA;                    startIndex: 0,&#xA;                    length: data.Stride&#xA;                );&#xA;                for (var pixelOffset = 0; pixelOffset &amp;lt; data.Width; pixelOffset&#x2B;&#x2B;)&#xA;                {&#xA;                    // Note: PixelFormat.Format24bppRgb means the data is stored in memory as BGR&#xA;                    const int PixelWidth = 3;&#xA;                    var r = pixelData[pixelOffset * PixelWidth &#x2B; 2];&#xA;                    var g = pixelData[pixelOffset * PixelWidth &#x2B; 1];&#xA;                    var b = pixelData[pixelOffset * PixelWidth];&#xA;                    values[pixelOffset, lineIndex] = (0.2989 * r) &#x2B; (0.5870 * g) &#x2B; (0.1140 * b);&#xA;                }&#xA;            }&#xA;        }&#xA;        finally&#xA;        {&#xA;            image.UnlockBits(data);&#xA;        }&#xA;        return DataRectangle.For(values);&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;With these classes, we can load an image and calculate the combined horizontal-gradient-minus-vertical-gradient value like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static IEnumerable&amp;lt;Rectangle&amp;gt; GetPossibleBarcodeAreasForBitmap(Bitmap image)&#xA;{&#xA;    var greyScaleImageData = image.GetGreyscale();&#xA;    var combinedGradients = greyScaleImageData.Transform((intensity, pos) =&amp;gt;&#xA;    {&#xA;        // Consider gradients to be zero at the edges of the image because there aren&#x27;t pixels&#xA;        // both left/right or above/below and so it&#x27;s not possible to calculate a real value&#xA;        var horizontalChange = (pos.X == 0) || (pos.X == greyScaleImageData.Width - 1)&#xA;            ? 0&#xA;            : greyScaleImageData[pos.X &#x2B; 1, pos.Y] - greyScaleImageData[pos.X - 1, pos.Y];&#xA;        var verticalChange = (pos.Y == 0) || (pos.Y == greyScaleImageData.Height - 1)&#xA;            ? 0&#xA;            : greyScaleImageData[pos.X, pos.Y &#x2B; 1] - greyScaleImageData[pos.X, pos.Y - 1];&#xA;        return Math.Max(0, Math.Abs(horizontalChange) - Math.Abs(verticalChange));&#xA;    });&#xA;&#xA;    // .. more will go here soon&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Before jumping straight into the image analysis, though, it&#x27;s worth resizing the source image if it&#x27;s large. Since this stage of the processing is looking for areas that look approximately like barcodes, we don&#x27;t require a lot of granularity - I&#x27;m envisaging (as with the MyFitnessPal use case) source images where the barcode takes up a significant space in the image and is roughly aligned with the view port* and so resizing the image such that the largest side is 300px should work well. If you wanted to scan an image where there were many barcodes to process (or even where there was only one but it was very small) then you might want to allow larger inputs than this - the more data that there is, though, the more work that must be done and the slower that the processing will be!&lt;/p&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(The barcode has to be roughly aligned with the viewport because the approaching of looking for areas with large horizontal variance in intensity with minor vertical variance would not work - as we&#x27;ll see later, though, there is considerable margin for error in this approach and perfect alignment is not required)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;A naive approach to this would be force the image so that its largest side is 300px, regardless of what it was originally. However, this is unnecessary if the largest side is already less than 300px (scaling it up will actually give us more work to do) and if the largest side is not much more than 300px then it&#x27;s probably not worth doing either - scaling it down may make any barcodes areas fuzzy and risk reducing the effectiveness of the processing while not actually reducing the required work. So I&#x27;m going to say that if the largest side of the image is 450px or larger than resize it so that its largest side is 300px and do nothing otherwise. To achieve that, we need a method like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static DataRectangle&amp;lt;double&amp;gt; GetGreyscaleData(&#xA;    Bitmap image,&#xA;    int resizeIfLargestSideGreaterThan,&#xA;    int resizeTo)&#xA;{&#xA;    var largestSide = Math.Max(image.Width, image.Height);&#xA;    if (largestSide &amp;lt;= resizeIfLargestSideGreaterThan)&#xA;        return image.GetGreyscale();&#xA;&#xA;    int width, height;&#xA;    if (image.Width &amp;gt; image.Height)&#xA;    {&#xA;        width = resizeTo;&#xA;        height = (int)(((double)image.Height / image.Width) * width);&#xA;    }&#xA;    else&#xA;    {&#xA;        height = resizeTo;&#xA;        width = (int)(((double)image.Width / image.Height) * height);&#xA;    }&#xA;    using var resizedImage = new Bitmap(image, width, height);&#xA;    return resizedImage.GetGreyscale();&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The next steps are to &amp;quot;normalise&amp;quot; the combined intensity variance values so that they fit the range zero-to-one, to &amp;quot;blur&amp;quot; this data and to then create a binary mask where the brighter pixels get set to one and the darker pixels get set to zero. In other words, to extend the code earlier (that calculated the intensity variance values) like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static IEnumerable&amp;lt;Rectangle&amp;gt; GetPossibleBarcodeAreasForBitmap(Bitmap image)&#xA;{&#xA;    var greyScaleImageData = GetGreyscaleData(&#xA;        image,&#xA;        resizeIfLargestSideGreaterThan: 450,&#xA;        resizeTo: 300&#xA;    );&#xA;    var combinedGradients = greyScaleImageData.Transform((intensity, pos) =&amp;gt;&#xA;    {&#xA;        // Consider gradients to be zero at the edges of the image because there aren&#x27;t pixels&#xA;        // both left/right or above/below and so it&#x27;s not possible to calculate a real value&#xA;        var horizontalChange = (pos.X == 0) || (pos.X == greyScaleImageData.Width - 1)&#xA;            ? 0&#xA;            : greyScaleImageData[pos.X &#x2B; 1, pos.Y] - greyScaleImageData[pos.X - 1, pos.Y];&#xA;        var verticalChange = (pos.Y == 0) || (pos.Y == greyScaleImageData.Height - 1)&#xA;            ? 0&#xA;            : greyScaleImageData[pos.X, pos.Y &#x2B; 1] - greyScaleImageData[pos.X, pos.Y - 1];&#xA;        return Math.Max(0, Math.Abs(horizontalChange) - Math.Abs(verticalChange));&#xA;    });&#xA;&#xA;    const int maxRadiusForGradientBlurring = 2;&#xA;    const double thresholdForMaskingGradients = 1d / 3;&#xA;&#xA;    var mask = Blur(Normalise(combinedGradients), maxRadiusForGradientBlurring)&#xA;        .Transform(value =&amp;gt; (value &amp;gt;= thresholdForMaskingGradients));&#xA;&#xA;    // .. more will go here soon&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;To do that we, need a &amp;quot;Normalise&amp;quot; method - which is simple:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static DataRectangle&amp;lt;double&amp;gt; Normalise(DataRectangle&amp;lt;double&amp;gt; values)&#xA;{&#xA;    var max = values.Enumerate().Max(pointAndValue =&amp;gt; pointAndValue.Item2);&#xA;    return (max == 0)&#xA;        ? values&#xA;        : values.Transform(value =&amp;gt; (value / max));&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;.. and a &amp;quot;Blur&amp;quot; method - which is a little less simple but hopefully still easy enough to follow &lt;em&gt;(for every point, look at the points around it and take an average of all of them; it just looks for a square area, which is fine for small &amp;quot;maxRadius&amp;quot; values but which might be better implemented as a circular area if large &amp;quot;maxRadius&amp;quot; values might be needed, which they aren&#x27;t in this code):&lt;/em&gt;&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static DataRectangle&amp;lt;double&amp;gt; Blur(DataRectangle&amp;lt;double&amp;gt; values, int maxRadius)&#xA;{&#xA;    return values.Transform((value, point) =&amp;gt;&#xA;    {&#xA;        var valuesInArea = new List&amp;lt;double&amp;gt;();&#xA;        for (var x = -maxRadius; x &amp;lt;= maxRadius; x&#x2B;&#x2B;)&#xA;        {&#xA;            for (var y = -maxRadius; y &amp;lt;= maxRadius; y&#x2B;&#x2B;)&#xA;            {&#xA;                var newPoint = new Point(point.X &#x2B; x, point.Y &#x2B; y);&#xA;                if ((newPoint.X &amp;lt; 0) || (newPoint.Y &amp;lt; 0)&#xA;                || (newPoint.X &amp;gt;= values.Width) || (newPoint.Y &amp;gt;= values.Height))&#xA;                    continue;&#xA;                valuesInArea.Add(values[newPoint.X, newPoint.Y]);&#xA;            }&#xA;        }&#xA;        return valuesInArea.Average();&#xA;    });&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This gets us to this point:&lt;/p&gt;&#xA;&lt;img alt=&quot;Mask of possibly-part-of-a-barcode areas&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-Mask.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Mask of possibly-part-of-a-barcode areas&quot;&gt;&#xA;&lt;p&gt;.. which feels like good progress!&lt;/p&gt;&#xA;&lt;p&gt;Now we need to try to identify distinct &amp;quot;islands&amp;quot; of pixels where each &amp;quot;island&amp;quot; or &amp;quot;object&amp;quot; is a set of points that are within a single connected area. A straightforward way to do that is to look at every point in the mask that is set to 1 and either:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Perform a pixel-style &amp;quot;flood fill&amp;quot; starting at this point in order to find other points in an object&lt;/li&gt;&#xA;&lt;li&gt;If this pixel has already been included in such a fill operation, do nothing (because it&#x27;s already been accounted for)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;This was made easier for me by reading the article &lt;a href=&quot;https://simpledevcode.wordpress.com/2015/12/29/flood-fill-algorithm-using-c-net/&quot;&gt;Flood Fill algorithm (using C#.Net)&lt;/a&gt;..&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static IEnumerable&amp;lt;IEnumerable&amp;lt;Point&amp;gt;&amp;gt; GetDistinctObjects(DataRectangle&amp;lt;bool&amp;gt; mask)&#xA;{&#xA;    // Flood fill areas in the looks-like-bar-code mask to create distinct areas&#xA;    var allPoints = new HashSet&amp;lt;Point&amp;gt;(&#xA;        mask.Enumerate(optionalFilter: (point, isMasked) =&amp;gt; isMasked).Select(point =&amp;gt; point.Item1)&#xA;    );&#xA;    while (allPoints.Any())&#xA;    {&#xA;        var currentPoint = allPoints.First();&#xA;        var pointsInObject = GetPointsInObject(currentPoint).ToArray();&#xA;        foreach (var point in pointsInObject)&#xA;            allPoints.Remove(point);&#xA;        yield return pointsInObject;&#xA;    }&#xA;&#xA;    // Inspired by code at&#xA;    // https://simpledevcode.wordpress.com/2015/12/29/flood-fill-algorithm-using-c-net/&#xA;    IEnumerable&amp;lt;Point&amp;gt; GetPointsInObject(Point startAt)&#xA;    {&#xA;        var pixels = new Stack&amp;lt;Point&amp;gt;();&#xA;        pixels.Push(startAt);&#xA;&#xA;        var valueAtOriginPoint = mask[startAt.X, startAt.Y];&#xA;        var filledPixels = new HashSet&amp;lt;Point&amp;gt;();&#xA;        while (pixels.Count &amp;gt; 0)&#xA;        {&#xA;            var currentPoint = pixels.Pop();&#xA;            if ((currentPoint.X &amp;lt; 0) || (currentPoint.X &amp;gt;= mask.Width)&#xA;            || (currentPoint.Y &amp;lt; 0) || (currentPoint.Y &amp;gt;= mask.Height))&#xA;                continue;&#xA;&#xA;            if ((mask[currentPoint.X, currentPoint.Y] == valueAtOriginPoint)&#xA;            &amp;amp;&amp;amp; !filledPixels.Contains(currentPoint))&#xA;            {&#xA;                filledPixels.Add(new Point(currentPoint.X, currentPoint.Y));&#xA;                pixels.Push(new Point(currentPoint.X - 1, currentPoint.Y));&#xA;                pixels.Push(new Point(currentPoint.X &#x2B; 1, currentPoint.Y));&#xA;                pixels.Push(new Point(currentPoint.X, currentPoint.Y - 1));&#xA;                pixels.Push(new Point(currentPoint.X, currentPoint.Y &#x2B; 1));&#xA;            }&#xA;        }&#xA;        return filledPixels;&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The problem is that, even with the blurring we performed, there will likely be some groups of distinct objects that are actually part of a single barcode. These areas need to be joined together. It&#x27;s quite possible for there to be relatively large gaps in the middle of barcodes (there is in the example that we&#x27;ve been looking at) and so we might not easily be able to just take the distinct objects that we&#x27;ve got and join together areas that seem &amp;quot;close enough&amp;quot;.&lt;/p&gt;&#xA;&lt;img alt=&quot;Areas that are possibly part of a barcode&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-MaskObjects.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Areas that are possibly part of a barcode&quot;&gt;&#xA;&lt;p&gt;On the basis that individual bars in a barcode are tall compared to the largest possible width that any of them can be (which I&#x27;ll go into more detail about later on), it seems like a reasonable idea to take any areas that are taller than they are wide and expand their width until they become square. That would give us this:&lt;/p&gt;&#xA;&lt;img alt=&quot;Mask of possibly-part-of-a-barcode areas, extended into squared areas&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-MaskSquares.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Mask of possibly-part-of-a-barcode areas, extended into squared areas&quot;&gt;&#xA;&lt;p&gt;We&#x27;d then work out which of these &amp;quot;squared off&amp;quot; rectangles overlap (if any) and replace overlapping rectangles with rectangles that cover their combined areas, which would look like this:&lt;/p&gt;&#xA;&lt;img alt=&quot;Overlapping squared-off areas that have been combined&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-MaskSquaresCombinedPreTrimmed.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Overlapping squared-off areas that have been combined&quot;&gt;&#xA;&lt;p&gt;The only problem with this is that the combined rectangles extend too far to the left and right of the areas, so we need to trim them down. The will be fairly straightforward because we have the information about what distinct objects there are and each object is just a list of points - so we work out which objects have points within each of the combined bounding areas and then we work out which out of all of the objects for each combined area has the smallest &amp;quot;x&amp;quot; value and smallest &amp;quot;y&amp;quot; value and which have the largest values. That way, we can change the combined bounding areas to only cover actual barcode pixels. Which would leave us with this:&lt;/p&gt;&#xA;&lt;img alt=&quot;Combined possibly-a-barcode areas&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-MaskSquaresCombined.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Combined possibly-a-barcode areas&quot;&gt;&#xA;&lt;p&gt;That might sound like a lot of complicated work but if we take a bit of a brute force* approach to it then it can be expressed like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static IEnumerable&amp;lt;Rectangle&amp;gt; GetOverlappingObjectBounds(&#xA;    IEnumerable&amp;lt;IEnumerable&amp;lt;Point&amp;gt;&amp;gt; objects)&#xA;{&#xA;    // Translate each &amp;quot;object&amp;quot; (a list of connected points) into a bounding box (squared off if&#xA;    // it was taller than it was wide)&#xA;    var squaredOffBoundedObjects = new HashSet&amp;lt;Rectangle&amp;gt;(&#xA;        objects.Select((points, index) =&amp;gt;&#xA;        {&#xA;            var bounds = Rectangle.FromLTRB(&#xA;                points.Min(p =&amp;gt; p.X),&#xA;                points.Min(p =&amp;gt; p.Y),&#xA;                points.Max(p =&amp;gt; p.X) &#x2B; 1,&#xA;                points.Max(p =&amp;gt; p.Y) &#x2B; 1&#xA;            );&#xA;            if (bounds.Height &amp;gt; bounds.Width)&#xA;                bounds.Inflate((bounds.Height - bounds.Width) / 2, 0);&#xA;            return bounds;&#xA;        })&#xA;    );&#xA;&#xA;    // Loop over the boundedObjects and reduce the collection by merging any two rectangles&#xA;    // that overlap and then starting again until there are no more bounds merges to perform&#xA;    while (true)&#xA;    {&#xA;        var combinedOverlappingAreas = false;&#xA;        foreach (var bounds in squaredOffBoundedObjects)&#xA;        {&#xA;            foreach (var otherBounds in squaredOffBoundedObjects)&#xA;            {&#xA;                if (otherBounds == bounds)&#xA;                    continue;&#xA;&#xA;                if (bounds.IntersectsWith(otherBounds))&#xA;                {&#xA;                    squaredOffBoundedObjects.Remove(bounds);&#xA;                    squaredOffBoundedObjects.Remove(otherBounds);&#xA;                    squaredOffBoundedObjects.Add(Rectangle.FromLTRB(&#xA;                        Math.Min(bounds.Left, otherBounds.Left),&#xA;                        Math.Min(bounds.Top, otherBounds.Top),&#xA;                        Math.Max(bounds.Right, otherBounds.Right),&#xA;                        Math.Max(bounds.Bottom, otherBounds.Bottom)&#xA;                    ));&#xA;                    combinedOverlappingAreas = true;&#xA;                    break;&#xA;                }&#xA;            }&#xA;            if (combinedOverlappingAreas)&#xA;                break;&#xA;        }&#xA;        if (!combinedOverlappingAreas)&#xA;            break;&#xA;    }&#xA;&#xA;    return squaredOffBoundedObjects.Select(bounds =&amp;gt;&#xA;    {&#xA;        var allPointsWithinBounds = objects&#xA;            .Where(points =&amp;gt; points.Any(point =&amp;gt; bounds.Contains(point)))&#xA;            .SelectMany(points =&amp;gt; points)&#xA;            .ToArray(); // Don&#x27;t re-evaluate in the four accesses below&#xA;        return Rectangle.FromLTRB(&#xA;            left: allPointsWithinBounds.Min(p =&amp;gt; p.X),&#xA;            right: allPointsWithinBounds.Max(p =&amp;gt; p.X) &#x2B; 1,&#xA;            top: allPointsWithinBounds.Min(p =&amp;gt; p.Y),&#xA;            bottom: allPointsWithinBounds.Max(p =&amp;gt; p.Y) &#x2B; 1&#xA;        );&#xA;    });&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p class=&quot;footnote&quot;&gt;* &lt;em&gt;(There are definitely more efficient ways that this could be done but since we&#x27;re only looking at 300px images then we&#x27;re not likely to end up with huge amounts of data to deal with)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;To complete the process, we need to do three more things:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Since barcodes are wider than they are tall, we can discard any regions that don&#x27;t fit this shape (of which there are two in the example image)&lt;/li&gt;&#xA;&lt;li&gt;The remaining regions are expanded a little across so that they more clearly surround the barcode region, rather than being butted right up to it (this will make the barcode reading process a little easier)&lt;/li&gt;&#xA;&lt;li&gt;As the regions that have been identified may well be on a resized version of the source image, they may need to scaled up so that they correctly apply to the source&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;To do that, we&#x27;ll start from this code that we saw earlier:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;var mask = Blur(Normalise(combinedGradients), maxRadiusForGradientBlurring)&#xA;    .Transform(value =&amp;gt; (value &amp;gt;= thresholdForMaskingGradients));&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;.. and expand it like so (removing the &amp;quot;// .. more will go here soon&amp;quot; comment), using the methods above:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;// Determine how much the image was scaled down (if it had to be scaled down at all)&#xA;// by comparing the width of the potentially-scaled-down data to the source image&#xA;var reducedImageSideBy = (double)image.Width / greyScaleImageData.Width;&#xA;&#xA;var mask = Blur(Normalise(combinedGradients), maxRadiusForGradientBlurring)&#xA;    .Transform(value =&amp;gt; (value &amp;gt;= thresholdForMaskingGradients));&#xA;&#xA;return GetOverlappingObjectBounds(GetDistinctObjects(mask))&#xA;    .Where(boundedObject =&amp;gt; boundedObject.Width &amp;gt; boundedObject.Height)&#xA;    .Select(boundedObject =&amp;gt;&#xA;    {&#xA;        var expandedBounds = boundedObject;&#xA;        expandedBounds.Inflate(width: expandedBounds.Width / 10, height: 0);&#xA;        expandedBounds.Intersect(&#xA;            Rectangle.FromLTRB(0, 0, greyScaleImageData.Width, greyScaleImageData.Height)&#xA;        );&#xA;        return new Rectangle(&#xA;            x: (int)(expandedBounds.X * reducedImageSideBy),&#xA;            y: (int)(expandedBounds.Y * reducedImageSideBy),&#xA;            width: (int)(expandedBounds.Width * reducedImageSideBy),&#xA;            height: (int)(expandedBounds.Height * reducedImageSideBy)&#xA;        );&#xA;    });&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The final result is that the barcode has been successfully located on the image - hurrah!&lt;/p&gt;&#xA;&lt;img alt=&quot;Barcode located!&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-Identified.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Barcode located!&quot;&gt;&#xA;&lt;p&gt;With this information, we should be able to extract regions or &amp;quot;sub images&amp;quot; from the source image and attempt to decipher the barcode value in it (presuming that there IS a bar code in it and we haven&#x27;t got a false positive match).&lt;/p&gt;&#xA;&lt;p&gt;As we&#x27;ll see in a moment, the barcode doesn&#x27;t have to be perfectly lined up - some rotation is acceptable (depending upon the image, up to around 20 or 30 degrees should be fine). The MyFitnessPal app has a couple of fallbacks that I&#x27;ve noticed, such as being able to read barcodes that are upside down or even back to front (which can happen if a barcode is scanned from the wrong side of a transparent wrapper). While I won&#x27;t be writing code here for either of those approaches, I&#x27;m sure that you could envisage how it could be done - the source image data could be processed as described here and then, if no barcode is read, rotated 180 degrees and re-processed and reversed and re-processed, etc..&lt;/p&gt;&#xA;&lt;h3 id=&quot;how-to-read-a-bar-code&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp#how-to-read-a-bar-code&quot;&gt;How to read a bar code&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;A barcode is comprised of both black and white bars - so it&#x27;s not just the black parts that are significant, it is the spaces between them as well.&lt;/p&gt;&#xA;&lt;p&gt;The format of a barcode is as follows:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Three single-width bars (a black one, a white one and another black one) that are used to gauge what is considered to be a &amp;quot;single width&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;Information for six numbers then appears, where each number is encoded by a sequence of four bars (white, black, white, black) - particular combinations of bar widths relate to particular digits (see below)&lt;/li&gt;&#xA;&lt;li&gt;Another guard section appears with five single width bars (white, black, white, black, white)&lt;/li&gt;&#xA;&lt;li&gt;Six more numbers appear (using the same bar-width-combinations encoding as before but the groups of four bars are now black, white, black, white)&lt;/li&gt;&#xA;&lt;li&gt;A final guard section of three single width bards (black, white, black)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The numbers are encoded using the following system:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt; Digit      Bar widths&#xA;&#xA;   0        3, 2, 1, 1&#xA;   1        2, 2, 2, 1&#xA;   2        2, 1, 2, 2&#xA;   3        1, 4, 1, 1&#xA;   4        1, 1, 3, 2&#xA;   5        1, 2, 3, 1&#xA;   6        1, 1, 1, 4&#xA;   7        1, 3, 1, 2&#xA;   8        1, 2, 1, 3&#xA;   9        3, 1, 1, 2&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;em&gt;(Note that every combination of values totals 7 when they added up - this is very helpful later!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;To see what that looks like in the real world, here&#x27;s a slice of that barcode from the jar of peanut butter with each section and each numeric value identified:&lt;/p&gt;&#xA;&lt;img alt=&quot;Barcode numbers interpreted&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-AnnotatedValues.png&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;Barcode numbers interpreted&quot;&gt;&#xA;&lt;p&gt;&lt;em&gt;(I should point out that the article &lt;a href=&quot;https://habr.com/en/post/439768/&quot;&gt;How does a barcode work?&lt;/a&gt; was extremely helpful in the research I did for this post and I&#x27;m very grateful to the author for having written it in such an approachable manner!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Any combination of bar widths that is not found in the table is considered to be invalid. On the one hand, you might think that this a potential loss; the format could support more combinations of bar widths to encode more values and then more data could be packed into the same space. There is an advantage, however, to having relatively few valid combinations of bar widths - it makes easier to tell whether the information being read appears to be correct. If a combination is encountered that seems incorrect then the read attempt should be aborted and retried. The format has existed for decades and it would make sense, bearing that in mind, to prioritise making it easier for the hardware to read rather prioritising trying to cram as much data in there as possible. There is &lt;em&gt;also&lt;/em&gt; a checksum included in the numerical data to try to catch any &amp;quot;misreads&amp;quot; but when working with low resolutions or hardware with little computing power, the easier that it is to bail out of a scan and to retry the better.&lt;/p&gt;&#xA;&lt;p&gt;The way to tackle the reading is to:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Convert the sub image to greyscale&lt;/li&gt;&#xA;&lt;li&gt;Create a binary mask so that the darker pixels become 0 and the lighter ones become 1&lt;/li&gt;&#xA;&lt;li&gt;Take a single line across the area&lt;/li&gt;&#xA;&lt;li&gt;Change the individual 1s and 0s into lengths of continuous &amp;quot;runs&amp;quot; of values&#xA;&lt;ul&gt;&#xA;&lt;li&gt;eg. 0001100 would become 3, 2, 2 because there are three 0s then two 1s and then two 0s&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;These runs of values will represent the different sized (black and white) bars that were encountered&#xA;&lt;ul&gt;&#xA;&lt;li&gt;For a larger image, each run length will be longer than for a small image but that won&#x27;t matter because when we encounter runs of four bar length values that we think should be interpreted as a single digit, we&#x27;ll do some dividing to try to guess the average size of a single width bar&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;Take these runs of values, skip through the expected guard regions and try to interpret each set of four bars that is thought to represent a digit of the bar code as that digit&lt;/li&gt;&#xA;&lt;li&gt;If successful then perform a checksum calculation on the output and return the value ass a success if it meets expectations&lt;/li&gt;&#xA;&lt;li&gt;If the line couldn&#x27;t be interpreted as a barcode or the checksum calculation fails then take the next line down and go back to step 4&lt;/li&gt;&#xA;&lt;li&gt;If there are no more lines to attempt then a barcode could not be identified in the image&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;This processing is fairly light computationally and so there is no need to resize the &amp;quot;may be a barcode&amp;quot; image region before attempting the work. In fact, it&#x27;s beneficial to &lt;em&gt;not&lt;/em&gt; shrink it as shrinking it will likely make the barcode section fuzzier and that makes the above steps less likely to work - the ideal case for creating a binary mask is where there is no significant &amp;quot;seepage&amp;quot; of pixel intensity between the black bar areas and the white bar areas. That&#x27;s not to say that the images have to be crystal clear or perfectly aligned with the camera because the redundancy built into the format works in our favour here - if one line across the image can&#x27;t be read because it&#x27;s fuzzy then there&#x27;s a good chance that one of the other lines will be legible.&lt;/p&gt;&#xA;&lt;p&gt;60 length values is the precise number that we expect to find - there is expected to be some blank space before the barcode starts (1) and then a guard section of three single-width lines that we use to gauge bar width (3) and then six numbers that are encoded in four bars each (6x4=24) and then a guard section of five single-width lines (5) and then six numbers (6x4=24) and then a final guard region of three single-width bars, giving 1&#x2B;3&#x2B;24&#x2B;5&#x2B;24&#x2B;3=60.&lt;/p&gt;&#xA;&lt;p&gt;There will likely be another section of blank content after the barcode that we ignore&lt;/p&gt;&#xA;&lt;p&gt;If we don&#x27;t want to validate the final guard region then we can work with a barcode image where some of the end of cut off, so long as the data for the 12 digits is there; in this case, 57 lengths if the minimum number that we can accept&lt;/p&gt;&#xA;&lt;h3 id=&quot;reading-the-numeric-value-with-code&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp#reading-the-numeric-value-with-code&quot;&gt;Reading the numeric value with code&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I&#x27;m going to try to present the code in approximately the same order as the steps presented above. So, firstly we need to convert the sub image to greyscale and create a binary mark from it. Then we&#x27;ll go line by line down the image data and try to read a value. So we&#x27;ll take this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;public static string? TryToReadBarcodeValue(Bitmap subImage)&#xA;{&#xA;    const double threshold = 0.5;&#xA;&#xA;     // Black lines are considered 1 and so we set to true if it&#x27;s a dark pixel (and 0 if light)&#xA;    var mask = subImage.GetGreyscale().Transform(intensity =&amp;gt; intensity &amp;lt; (256 * threshold));&#xA;    for (var y = 0; y &amp;lt; mask.Height; y&#x2B;&#x2B;)&#xA;    {&#xA;        var value = TryToReadBarcodeValueFromSingleLine(mask, y);&#xA;        if (value is object)&#xA;            return value;&#xA;    }&#xA;    return null;&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;.. and the read-each-slice-of-the-image code looks like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static string? TryToReadBarcodeValueFromSingleLine(&#xA;    DataRectangle&amp;lt;bool&amp;gt; barcodeDetails,&#xA;    int sliceY)&#xA;{&#xA;    if ((sliceY &amp;lt; 0) || (sliceY &amp;gt;= barcodeDetails.Height))&#xA;        throw new ArgumentOutOfRangeException(nameof(sliceY));&#xA;&#xA;    var lengths = GetBarLengthsFromBarcodeSlice(barcodeDetails, sliceY).ToArray();&#xA;    if (lengths.Length &amp;lt; 57)&#xA;    {&#xA;        // As explained, we&#x27;d like 60 bars (which would include the final guard region) but we&#xA;        // can still make an attempt with 57 (but no fewer)&#xA;        // - There will often be another section of blank content after the barcode that we ignore&#xA;        // - If we don&#x27;t want to validate the final guard region then we can work with a barcode&#xA;        //   image where some of the end is cut off, so long as the data for the 12 digits is&#xA;        //   there (this will be the case where there are only 57 lengths)&#xA;        return null;&#xA;    }&#xA;&#xA;    var offset = 0;&#xA;    var extractedNumericValues = new List&amp;lt;int&amp;gt;();&#xA;    for (var i = 0; i &amp;lt; 14; i&#x2B;&#x2B;)&#xA;    {&#xA;        if (i == 0)&#xA;        {&#xA;            // This should be the first guard region and it should be a pattern of three single-&#xA;            // width bars&#xA;            offset &#x2B;= 3;&#xA;        }&#xA;        else if (i == 7)&#xA;        {&#xA;            // This should be the guard region in the middle of the barcode and it should be a&#xA;            // pattern of five single-width bars&#xA;            offset &#x2B;= 5;&#xA;        }&#xA;        else&#xA;        {&#xA;            var value = TryToGetValueForLengths(&#xA;                lengths[offset],&#xA;                lengths[offset &#x2B; 1],&#xA;                lengths[offset &#x2B; 2],&#xA;                lengths[offset &#x2B; 3]&#xA;            );&#xA;            if (value is null)&#xA;                return null;&#xA;            extractedNumericValues.Add(value.Value);&#xA;            offset &#x2B;= 4;&#xA;        }&#xA;    }&#xA;&#xA;    // Calculate what the checksum should be based upon the first 11 numbers and ensure that&#xA;    // the 12th matches it&#xA;    if (extractedNumericValues.Last() != CalculateChecksum(extractedNumericValues.Take(11)))&#xA;        return null;&#xA;&#xA;    return string.Join(&amp;quot;&amp;quot;, extractedNumericValues);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;With the code below, we find the runs of continuous 0 or 1 lengths that will represent bars are return that list (again, for larger images each run will be longer and for smaller images each run will be shorter but this will be taken care of later) -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static IEnumerable&amp;lt;int&amp;gt; GetBarLengthsFromBarcodeSlice(&#xA;    DataRectangle&amp;lt;bool&amp;gt; barcodeDetails,&#xA;    int sliceY)&#xA;{&#xA;    if ((sliceY &amp;lt; 0) || (sliceY &amp;gt;= barcodeDetails.Height))&#xA;        throw new ArgumentOutOfRangeException(nameof(sliceY));&#xA;&#xA;    // Take the horizontal slice of the data&#xA;    var values = new List&amp;lt;bool&amp;gt;();&#xA;    for (var x = 0; x &amp;lt; barcodeDetails.Width; x&#x2B;&#x2B;)&#xA;        values.Add(barcodeDetails[x, sliceY]);&#xA;&#xA;    // Split the slice into bars - we only care about how long each segment is when they&#xA;    // alternate, not whether they&#x27;re dark bars or light bars&#xA;    var segments = new List&amp;lt;Tuple&amp;lt;bool, int&amp;gt;&amp;gt;();&#xA;    foreach (var value in values)&#xA;    {&#xA;        if ((segments.Count == 0) || (segments[^1].Item1 != value))&#xA;            segments.Add(Tuple.Create(value, 1));&#xA;        else&#xA;            segments[^1] = Tuple.Create(value, segments[^1].Item2 &#x2B; 1);&#xA;    }&#xA;    if ((segments.Count &amp;gt; 0) &amp;amp;&amp;amp; !segments[0].Item1)&#xA;    {&#xA;        // Remove the white space before the first bar&#xA;        segments.RemoveAt(0);&#xA;    }&#xA;    return segments.Select(segment =&amp;gt; segment.Item2);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Now we need to implement the &amp;quot;TryToGetValueForLengths&amp;quot; method that &amp;quot;TryToReadBarcodeValueFromSingleLine&amp;quot; calls. This takes four bar lengths that are thought to represent a single digit in the bar code value (they are not part of a guard region or anything like that). It take those four bar lengths and guesses how many pixels across a single bar would be - which is made my simpler by the fact that all of the possible combinations of bar lengths in the lookup chart that we saw earlier add up to 7.&lt;/p&gt;&#xA;&lt;p&gt;There&#x27;s a little flexibility introduced here to try to account for a low quality image or if the threshold was a bit strong in the creation of the binary mask; we&#x27;ll take that calculated expected width of a single bar and tweak it up or down a little if apply that division to the bar lengths means that we made some of the bars too small that they disappeared or too large and it seemed like the total width would be more than seven single estimated-width bars. There&#x27;s only a &lt;em&gt;little&lt;/em&gt; flexibility here because if we fail then we can always try another line of the image! (Or maybe it will turn out that this sub image was a false positive match and there isn&#x27;t a bar code in it at all).&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static int? TryToGetValueForLengths(int l0, int l1, int l2, int l3)&#xA;{&#xA;    if (l0 &amp;lt;= 0)&#xA;        throw new ArgumentOutOfRangeException(nameof(l0));&#xA;    if (l1 &amp;lt;= 0)&#xA;        throw new ArgumentOutOfRangeException(nameof(l1));&#xA;    if (l2 &amp;lt;= 0)&#xA;        throw new ArgumentOutOfRangeException(nameof(l2));&#xA;    if (l3 &amp;lt;= 0)&#xA;        throw new ArgumentOutOfRangeException(nameof(l3));&#xA;&#xA;    // Take a guess at what the width of a single bar is based upon these four values&#xA;    // (the four bars that encode a number should add up to a width of seven)&#xA;    var raw = new[] { l0, l1, l2, l3 };&#xA;    var singleWidth = raw.Sum() / 7d;&#xA;    var adjustment = singleWidth / 10;&#xA;    var attemptedSingleWidths = new HashSet&amp;lt;double&amp;gt;();&#xA;    while (true)&#xA;    {&#xA;        var normalised = raw.Select(x =&amp;gt; Math.Max(1, (int)Math.Round(x / singleWidth))).ToArray();&#xA;        var sum = normalised.Sum();&#xA;        if (sum == 7)&#xA;            return TryToGetNumericValue(normalised[0], normalised[1], normalised[2], normalised[3]);&#xA;&#xA;        attemptedSingleWidths.Add(singleWidth);&#xA;        if (sum &amp;gt; 7)&#xA;            singleWidth &#x2B;= adjustment;&#xA;        else&#xA;            singleWidth -= adjustment;&#xA;        if (attemptedSingleWidths.Contains(singleWidth))&#xA;        {&#xA;            // If we&#x27;ve already tried this width-of-a-single-bar value then give up -&#xA;            // it doesn&#x27;t seem like we can make the input values make sense&#xA;            return null;&#xA;        }&#xA;    }&#xA;&#xA;    static int? TryToGetNumericValue(int i0, int i1, int i2, int i3)&#xA;    {&#xA;        var lookFor = string.Join(&amp;quot;&amp;quot;, new[] { i0, i1, i2, i3 });&#xA;        var lookup = new[]&#xA;        {&#xA;            // These values correspond to the lookup chart shown earlier&#xA;            &amp;quot;3211&amp;quot;, &amp;quot;2221&amp;quot;, &amp;quot;2122&amp;quot;, &amp;quot;1411&amp;quot;, &amp;quot;1132&amp;quot;, &amp;quot;1231&amp;quot;, &amp;quot;1114&amp;quot;, &amp;quot;1312&amp;quot;, &amp;quot;1213&amp;quot;, &amp;quot;3112&amp;quot;&#xA;        };&#xA;        for (var i = 0; i &amp;lt; lookup.Length; i&#x2B;&#x2B;)&#xA;        {&#xA;            if (lookFor == lookup[i])&#xA;                return i;&#xA;        }&#xA;        return null;&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Finally we need the CalculateChecksum method (as noted in the code, there&#x27;s a great explanation of how to do this in &lt;a href=&quot;https://en.wikipedia.org/wiki/Check_digit#UPC&quot;&gt;wikipedia&lt;/a&gt;) -&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;private static int CalculateChecksum(IEnumerable&amp;lt;int&amp;gt; values)&#xA;{&#xA;    if (values == null)&#xA;        throw new ArgumentNullException(nameof(values));&#xA;    if (values.Count() != 11)&#xA;        throw new ArgumentException(&amp;quot;Should be provided with precisely 11 values&amp;quot;);&#xA;&#xA;    // See https://en.wikipedia.org/wiki/Check_digit#UPC&#xA;    var checksumTotal = values&#xA;        .Select((value, index) =&amp;gt; (index % 2 == 0) ? (value * 3) : value)&#xA;        .Sum();&#xA;    var checksumModulo = checksumTotal % 10;&#xA;    if (checksumModulo != 0)&#xA;        checksumModulo = 10 - checksumModulo;&#xA;    return checksumModulo;&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;With this code, we have executed all of the planned steps outlined before.&lt;/p&gt;&#xA;&lt;p&gt;It should be noted that, even with the small amount of flexibility in the &amp;quot;TryToGetValueForLengths&amp;quot; method, in the peanut butter bar code example it requires 15 calls to &amp;quot;GetBarLengthsFromBarcodeSlice&amp;quot; until a bar code is successfully matched! Presumably, this is because there is a little more distortion further up the bar code due to the curve of the jar.&lt;/p&gt;&#xA;&lt;p&gt;That&#x27;s not to say, however, that this approach to bar reading is particularly fussy. The redundancy and simplicity, not to mention the &lt;em&gt;size&lt;/em&gt; of the average bar code, means that there is plenty of opportunity to try reading a sub image in multiple slices until one of them does match. In fact, I mentioned earlier that the barcode doesn&#x27;t have to be perfectly at 90 degrees in order to be interpretable and that some rotation is acceptable. This hopefully makes some intuitive sense based upon the logic above and how it doesn&#x27;t matter how long each individual bar code line is because they are averaged out - if a bar code was rotated a little and then a read was attempted of it line by line then the ratios between each line should remain consistent and the same data should be readable.&lt;/p&gt;&#xA;&lt;p&gt;To illustrate, here&#x27;s a zoomed-in section of the middle of the peanut butter bar code in the orientation shown so far:&lt;/p&gt;&#xA;&lt;img alt=&quot;A strip of the peanut butter jar&#x27;s bar code&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-SingleStrip.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;A strip of the peanut butter jar&#x27;s bar code&quot;&gt;&#xA;&lt;p&gt;If we then rotate it like this:&lt;/p&gt;&#xA;&lt;img alt=&quot;The peanut butter jar rotated slightly&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-Rotated.png&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;The peanut butter jar rotated slightly&quot;&gt;&#xA;&lt;p&gt;.. then the code above will still read the value correctly because a strip across the rotated bar code looks like this:&lt;/p&gt;&#xA;&lt;img alt=&quot;A strip of the peanut butter jar&#x27;s bar code from the rotated image&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-SingleStripFromRotated.png&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;A strip of the peanut butter jar&#x27;s bar code from the rotated image&quot;&gt;&#xA;&lt;p&gt;Hopefully it&#x27;s clear enough that, for each given line, the ratios are essentially the same as for the non-rotated strip:&lt;/p&gt;&#xA;&lt;img alt=&quot;A strip of the peanut butter jar&#x27;s bar code&quot; src=&quot;https://www.productiverage.com/Content/Images/Posts/PeanutBarcode-SingleStrip.jpg&quot; class=&quot;NoBorder AlwaysFullWidth&quot; title=&quot;A strip of the peanut butter jar&#x27;s bar code&quot;&gt;&#xA;&lt;p&gt;To get a reading from an image that is rotated more than this requires a very clear source image and will still be limited by the first stage of processing - that tried to find sections where the horizontal image intensity changed with steep gradients but the vertical intensity did not. If the image is rotated too much then there will be more vertical image intensity differences encountered and it is less likely to identify it as a &amp;quot;maybe a bar code&amp;quot; region.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;(Note: I experimented with rotated images that were produced by an online barcode generator and had more success - meaning that I could rotate them more than I could with real photographs - but that&#x27;s because those images are generated with stark black and white and the horizontal / vertical intensity gradients are maintained for longer when the image is rotated if they start with such a high level of clarity.. I&#x27;m more interested in reading values from real photographs and so I would suggest that only fairly moderate rotation will work - though it would still be plenty for an MyFitnessPal-type app that expects the User to hold the bar code in roughly the right orientation!)&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&quot;tying-it-all-together&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp#tying-it-all-together&quot;&gt;Tying it all together&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;We&#x27;ve looked at the separate steps involved in the whole reading process, all that is left is to combine them. The &amp;quot;GetPossibleBarcodeAreasForBitmap&amp;quot; and &amp;quot;TryToReadBarcodeValue&amp;quot; methods can be put together into a fully functioning program like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;static void Main()&#xA;{&#xA;    using var image = new Bitmap(&amp;quot;Source.jpg&amp;quot;);&#xA;&#xA;    var barcodeValues = new List&amp;lt;string&amp;gt;();&#xA;    foreach (var area in GetPossibleBarcodeAreasForBitmap(image))&#xA;    {&#xA;        using var areaBitmap = new Bitmap(area.Width, area.Height);&#xA;        using (var g = Graphics.FromImage(areaBitmap))&#xA;        {&#xA;            g.DrawImage(&#xA;                image,&#xA;                destRect: new Rectangle(0, 0, areaBitmap.Width, areaBitmap.Height),&#xA;                srcRect: area,&#xA;                srcUnit: GraphicsUnit.Pixel&#xA;            );&#xA;        }&#xA;        var valueFromBarcode = TryToReadBarcodeValue(areaBitmap);&#xA;        if (valueFromBarcode is object)&#xA;            barcodeValues.Add(valueFromBarcode);&#xA;    }&#xA;&#xA;    if (!barcodeValues.Any())&#xA;        Console.WriteLine(&amp;quot;Couldn&#x27;t read any bar codes from the source image :(&amp;quot;);&#xA;    else&#xA;    {&#xA;        Console.WriteLine(&amp;quot;Read the following bar code(s) from the image:&amp;quot;);&#xA;        foreach (var barcodeValue in barcodeValues)&#xA;            Console.WriteLine(&amp;quot;- &amp;quot; &#x2B; barcodeValue);&#xA;    }&#xA;&#xA;    Console.WriteLine();&#xA;    Console.WriteLine(&amp;quot;Press [Enter] to terminate..&amp;quot;);&#xA;    Console.ReadLine();&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h3 id=&quot;finito&quot;&gt;&lt;a href=&quot;https://www.productiverage.com/how-are-barcodes-read-libraryless-image-processing-in-c-sharp#finito&quot;&gt;Finito!&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;And with that, we&#x27;re finally done! I must admit that I started writing this post about three years ago and it&#x27;s been in my TODO list for a loooooong time now. But I&#x27;ve taken a week off work and been able to catch up with a few things and have finally been able to cross it off the list. And I&#x27;m quite relieved that I didn&#x27;t give up on it entirely because it was a fun little project and coming back to it now allowed me to tidy it up a bit with the newer C# 8 syntax and even enable the nullable reference types option on the project (I sure do hate unintentional nulls being allowed to sneak in!)&lt;/p&gt;&#xA;&lt;p&gt;A quick reminder if you want to see it in action or play about it yourself, the &lt;a href=&quot;https://github.com/ProductiveRage/BarcodeReader&quot;&gt;GitHub repo is here&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Thanks to anyone that read this far!&lt;/p&gt;&#xA;&lt;div class=&quot;Related&quot;&gt;&lt;h3&gt;You may also be interested in (see &lt;a href=&quot;https://www.productiverage.com/automating-suggested-related-posts-links-for-my-blog-posts&quot;&gt;here&lt;/a&gt; for information about how these are generated):&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/finding-the-brightest-area-in-an-image-with-c-sharp-fixing-a-blurry-presentation-video-part-one&quot;&gt;Finding the brightest area in an image with C# (fixing a blurry presentation video - part one)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/parallelising-linq-work-in-c-sharp&quot;&gt;Parallelising (LINQ) work in C#&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.productiverage.com/face-or-no-face-finding-faces-in-photos-using-c-sharp-and-accordnet&quot;&gt;Face or no face (finding faces in photos using C# and Accord.NET)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
                <pubDate>Fri, 07 Aug 2020 23:24:00 GMT</pubDate>
            </item>

    </channel>

</rss>