<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dimitri Rudenko</title>
    <description>The latest articles on DEV Community by Dimitri Rudenko (@dimitrirudenko).</description>
    <link>https://dev.to/dimitrirudenko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4021216%2Ff71e78f1-206b-464e-93bf-8637a096ef7f.jpeg</url>
      <title>DEV Community: Dimitri Rudenko</title>
      <link>https://dev.to/dimitrirudenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dimitrirudenko"/>
    <language>en</language>
    <item>
      <title>Counting people at a bus door: why 98% accuracy is an engineering problem, not a sensor spec</title>
      <dc:creator>Dimitri Rudenko</dc:creator>
      <pubDate>Wed, 04 Nov 2026 16:28:08 +0000</pubDate>
      <link>https://dev.to/dimitrirudenko/counting-people-at-a-bus-door-why-99-accuracy-is-an-engineering-problem-not-a-sensor-spec-23hf</link>
      <guid>https://dev.to/dimitrirudenko/counting-people-at-a-bus-door-why-99-accuracy-is-an-engineering-problem-not-a-sensor-spec-23hf</guid>
      <description>&lt;p&gt;Disclosure: I work at &lt;a href="https://acorel.com/" rel="noopener noreferrer"&gt;ACOREL&lt;/a&gt;, a French company that builds automatic passenger counting (APC) systems for public transport. This post is about the engineering problem, not about our products.&lt;br&gt;
A bus door is about a metre wide. People go in, people go out. Counting them sounds like a weekend project with a camera and an object detector.&lt;br&gt;
Then you look at what the numbers are used for. Transit agencies plan timetables with them. In integrated fare systems, operators split ticket revenue based on how many passengers each one carried. Public service contracts pay per passenger-kilometre. At that point, 95% accuracy isn't "pretty good". It's a 5% error in somebody's revenue.&lt;br&gt;
Getting from 95% to 98% is not about buying a better sensor. It's about the whole chain: geometry, tracking, edge processing, validation and data integration. Here's how that chain works.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Why the doorway is a hard place to count
&lt;/h2&gt;

&lt;p&gt;A doorway is a narrow, crowded, constantly changing scene. A few things that break naive counting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;People stop. Someone stands in the door to let others out, or to check the route number. A track that lingers can be counted twice, or not at all.&lt;/li&gt;
&lt;li&gt;People overlap. In rush hour, two or three people move through at once, shoulder to shoulder. From above they can merge into one blob.&lt;/li&gt;
&lt;li&gt;Not everything that moves is a passenger. Prams, suitcases, wheelchairs, bikes, dogs, a backpack held at chest height.&lt;/li&gt;
&lt;li&gt;Children walk close to adults. Small, close together, sometimes carried.&lt;/li&gt;
&lt;li&gt;People change their mind. One step in, one step back out. Net zero, but two line crossings.&lt;/li&gt;
&lt;li&gt;The environment changes. Sunlight through the door, darkness at night, rain, reflective floors, vibration while the vehicle moves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these produces noise in the raw signal. Most of the engineering is about removing that noise without removing real passengers.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Why depth beats a normal camera
&lt;/h2&gt;

&lt;p&gt;A regular 2D camera pointed at a doorway sees colours and shapes. It struggles with shadows, sudden light changes and people who overlap. It also captures faces, which creates a privacy problem before you've written a single line of code.&lt;br&gt;
Most modern APC systems use 3D sensors mounted above the door, looking straight down. From above, people rarely hide each other, and every point in the image has a height. The two main approaches:&lt;/p&gt;

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

&lt;p&gt;The key advantage of depth is simple: a head is a local maximum in a height map. That one property makes it much easier to separate two people walking side by side, ignore a suitcase on the floor, or tell a pram from the adult pushing it.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. The counting logic: detect, track, cross a line
&lt;/h2&gt;

&lt;p&gt;Once you have a depth map, the pipeline usually looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect candidate heads: local height maxima above a minimum height, with a plausible size.&lt;/li&gt;
&lt;li&gt;Track each candidate across frames, so the same person keeps the same ID.&lt;/li&gt;
&lt;li&gt;Count when a track crosses a virtual line in the doorway, in a given direction.&lt;/li&gt;
&lt;li&gt;Gate counting with the door state: count only while the door is open.&lt;/li&gt;
&lt;li&gt;
A simplified version of the line-crossing step:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified illustration, not production code.
# Each track is a sequence of (x, y) positions of one detected head.
# The counting line is y = LINE_Y; inside the vehicle is y &amp;gt; LINE_Y.
&lt;/span&gt;
&lt;span class="n"&gt;LINE_Y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="n"&gt;MIN_TRAVEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;  &lt;span class="c1"&gt;# metres travelled past the line before we trust a crossing
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count_track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;start_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;end_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Ignore tracks that never really crossed: someone who stepped
&lt;/span&gt;    &lt;span class="c1"&gt;# in and back out, or stood in the doorway.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LINE_Y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MIN_TRAVEL&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LINE_Y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MIN_TRAVEL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start_y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LINE_Y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;end_y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;   &lt;span class="c1"&gt;# boarding
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start_y&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LINE_Y&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;end_y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;   &lt;span class="c1"&gt;# alighting
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The hard part isn't this function. It's everything that feeds it: keeping IDs stable when two people merge and split, deciding whether a small height maximum is a child or a bag, and handling a track that starts or ends outside the field of view. Counting on the net movement of a track, not on every crossing, is what stops one indecisive passenger from becoming two.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Privacy by design: process at the edge, export only counts
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5xvhxkc0o7tcnabldo2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5xvhxkc0o7tcnabldo2.png" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;br&gt;
This is the part I find most interesting from an architecture point of view.&lt;br&gt;
In Europe, anything that can identify a person falls under GDPR. You could collect images, send them to a server and anonymise them there. But then you've built a system that handles personal data at every step: storage, transfer, access control, retention, breach risk.&lt;br&gt;
The alternative is to make sure personal data never exists outside the sensor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[3D sensor] -&amp;gt; depth frames -&amp;gt; [on-device processing] -&amp;gt; events -&amp;gt; [vehicle gateway] -&amp;gt; [back office]
                                  detect / track / count       boardings, alightings,
                                  frames discarded             door ID, timestamp

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a system where privacy isn't a policy added later. It's a property of the architecture. That's much easier to defend to a regulator, a city council or a passenger who asks, "Is that a camera?"&lt;br&gt;
The trade-off is real, though. If you don't keep frames, you can't go back and re-run a better algorithm on last month's data. You have to get the edge processing right, and you need a separate, controlled way to collect reference data for validation.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Validation: how do you prove 98%?
&lt;/h2&gt;

&lt;p&gt;"Our sensor is 98% accurate" means nothing without saying how it was measured.&lt;br&gt;
The usual approach is to compare the system's counts with ground truth: manual counting, or counting from a reference video recorded only for the test, under controlled conditions. Then you look at two different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Per-event error: how far off each door opening is.&lt;/li&gt;
&lt;li&gt;Systematic error (bias): whether the system over- or under-counts on average across many events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction matters because errors cancel out. A system that counts +1 at one stop and -1 at the next looks perfect in aggregate. For planning, that may be fine. For revenue, what matters most is that there's no systematic bias: an operator must not be consistently over- or under-paid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified bias check over a validation sample
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference_counts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total_system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total_reference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference_counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_system&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;total_reference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_reference&lt;/span&gt;

&lt;span class="c1"&gt;# +0.008 means the system over-counts by 0.8% on this sample
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Germany, the industry standard VDV 457 formalises this. It defines how to build the sample, how to compare with reference counts, and how to show statistically that the bias stays within a tight limit. Operators across Europe use it as a reference in tenders.&lt;br&gt;
This is also why a lab figure is worth very little. Accuracy has to be shown in your conditions: your vehicles, your doors, your rush hour, your winter coats and prams. A sample that only covers quiet midday trips will flatter any system.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. From counts to decisions: the data pipeline
&lt;/h2&gt;

&lt;p&gt;A count from one door is not useful yet. Before anyone can plan a timetable or split revenue, the data has to go through several steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aggregate doors into vehicle-level boardings and alightings per stop.&lt;/li&gt;
&lt;li&gt;Match to location and schedule: combine with the vehicle's GPS or AVL data and the timetable (often GTFS) to know which stop, trip and route each event belongs to.&lt;/li&gt;
&lt;li&gt;Check plausibility: over a full trip, boardings should roughly equal alightings, and the load on board can never be negative.&lt;/li&gt;
&lt;li&gt;Balance and correct: distribute small imbalances in a documented, consistent way, so corrections don't introduce their own bias.&lt;/li&gt;
&lt;li&gt;Integrate: send the clean data to the systems that use it: scheduling, ticketing and revenue sharing, network planning, real-time crowding information for passengers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plausibility check is a nice example of how a domain constraint catches errors that the sensor alone can't see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_trip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stops&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# stops: list of (boardings, alightings) in stop order
&lt;/span&gt;    &lt;span class="n"&gt;load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stops&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;load&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Negative load at stop &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: check counts or stop matching&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Trip ends with &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; passengers on board: imbalance to review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my experience, this is where many projects lose value. A good sensor with poor integration produces a nice dashboard that nobody uses. A good sensor with clean, validated data in the scheduling and revenue systems changes real decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Accuracy is a property of the whole system, not of the sensor alone.&lt;/li&gt;
&lt;li&gt;Depth sensing from above turns a hard vision problem into a much easier geometry problem.&lt;/li&gt;
&lt;li&gt;Processing at the edge and exporting only counts makes privacy a property of the architecture.&lt;/li&gt;
&lt;li&gt;Validate against ground truth, in real conditions, and look at bias, not only average error.&lt;/li&gt;
&lt;li&gt;The value appears only when counts are matched to the schedule, checked for plausibility and integrated into the systems that make decisions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you've worked on people counting, tracking in crowded scenes or edge processing under privacy constraints, I'd like to hear which edge case gave you the most trouble.&lt;/p&gt;

</description>
      <category>computervision</category>
      <category>iot</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Inside People Flow Management: How Acorel Turns Sensor Data Into Building Intelligence</title>
      <dc:creator>Dimitri Rudenko</dc:creator>
      <pubDate>Wed, 08 Jul 2026 11:25:50 +0000</pubDate>
      <link>https://dev.to/dimitrirudenko/inside-people-flow-management-how-acorel-turns-sensor-data-into-building-intelligence-jk5</link>
      <guid>https://dev.to/dimitrirudenko/inside-people-flow-management-how-acorel-turns-sensor-data-into-building-intelligence-jk5</guid>
      <description>&lt;p&gt;If you've ever wondered how a shopping mall knows exactly how crowded its west wing is right now, or how an airport terminal predicts a security queue backing up 20 minutes before it happens, you've bumped into the world of people flow management — a category of systems that sits at the intersection of edge sensing, computer vision, and IoT integration.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://acorel.com" rel="noopener noreferrer"&gt;Acorel&lt;/a&gt; is one of the more interesting players in this space, with roots going back to 1989 and deployments ranging from the Musée d'Orsay to SNCF trains to EuroAirport. As a dev, I found the architecture underneath their products worth unpacking — not because it's exotic, but because it's a genuinely solid reference pattern for anyone building sensor-to-decision pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: counting people is a harder distributed-systems problem than it sounds
&lt;/h2&gt;

&lt;p&gt;Naively, "count how many people are in a room" sounds like a single sensor and a counter variable. In practice, at any real scale, you're dealing with:&lt;/p&gt;

&lt;p&gt;Occlusion — people walking close together, or standing behind an object&lt;br&gt;
False positives — luggage, strollers, security robots&lt;br&gt;
Multi-zone deduplication — a person crossing from zone A to zone B shouldn't be counted twice, or lost entirely&lt;br&gt;
Latency vs. accuracy tradeoffs — a security team needs alerts in seconds, not after a nightly batch job&lt;br&gt;
Privacy constraints baked into the pipeline itself, not bolted on after the fact&lt;/p&gt;

&lt;p&gt;Acorel's platform (branded VISION, with vertical variants like VISION Pop for public venues, VISION Rail for transit, and VISION Air for airports) is built around solving these problems with a fairly classic edge + cloud split.&lt;/p&gt;
&lt;h2&gt;
  
  
  The sensor layer: multiple modalities, not one
&lt;/h2&gt;

&lt;p&gt;Rather than betting on a single sensing technology, the stack mixes:&lt;/p&gt;

&lt;p&gt;Infrared — cheap, reliable for simple entrance/exit counting&lt;br&gt;
3D stereoscopic and LiDAR — for accurate headcounts in crowded or high-density areas, where 2D approaches lose accuracy fast&lt;br&gt;
Computer vision on existing CCTV — this one's the clever bit: rather than requiring new hardware everywhere, the platform can run inference on camera feeds a venue already has, which massively cuts deployment time and cost for large sites like train stations&lt;/p&gt;

&lt;p&gt;This mirrors a pattern worth stealing for any IoT project: don't assume homogeneous sensor hardware. Design your ingestion and normalization layer to accept multiple sensor types and reconcile them into one canonical occupancy signal, rather than hard-coding assumptions about a single sensor's data format.&lt;/p&gt;
&lt;h2&gt;
  
  
  Privacy by design, at the pipeline level
&lt;/h2&gt;

&lt;p&gt;This is the part I think is most relevant to developers outside the physical-sensing world too. Acorel's stated approach is "Privacy by Design": no images are stored, and no faces are identified — the anonymization happens at the point of capture, not as a downstream filtering step.&lt;/p&gt;

&lt;p&gt;If you're architecting any system that ingests video or biometric-adjacent data (this applies well beyond people counting — think fleet dashcams, retail analytics, workplace tools), the lesson generalizes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BAD:  camera → store raw frames → run face detection → discard PII downstream
GOOD: camera → on-device inference (headcount only) → emit anonymized event → transmit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doing the reduction at the edge, before data ever leaves the device, means there's no raw PII sitting in a bucket somewhere waiting for a misconfigured ACL to leak it. It also happens to make GDPR/CNIL compliance a much easier conversation, since there's no personal data to protect in the first place — you're not protecting it well, you're simply not collecting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The integration layer: an open API as the actual product
&lt;/h2&gt;

&lt;p&gt;A counting sensor on its own is just a number. The interesting engineering — and the actual value for a customer — is what that number triggers. Acorel exposes flow and occupancy data through an open API that connects into:&lt;/p&gt;

&lt;p&gt;Building management systems (BMS) — adjusting HVAC and lighting based on real occupancy instead of a fixed schedule&lt;br&gt;
Ticketing and access-control systems — for venues like museums and stadiums&lt;br&gt;
CRM and passenger information systems — for transit and airport contexts&lt;/p&gt;

&lt;p&gt;A simplified version of what a downstream integration might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# Illustrative pattern — check Acorel's own docs for actual endpoint specs
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_zone_occupancy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.acorel.example/v1/zones/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;zone_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/occupancy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;maybe_trigger_hvac_boost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;occupancy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;occupancy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# push a signal into the BMS integration
&lt;/span&gt;        &lt;span class="nf"&gt;notify_bms_system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;occupancy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zone_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;increase_ventilation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Note: the endpoint above is illustrative — I don't have Acorel's actual API schema, so treat this as "the shape of the pattern," not a working snippet.)&lt;/p&gt;

&lt;p&gt;What makes this pattern powerful is that the counting system isn't the end product — it's the trigger layer for a whole set of downstream automations: staffing decisions, energy savings, safety alerts, passenger information displays. The actual ROI for a customer comes from that last mile, not the raw count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this shows up in production
&lt;/h2&gt;

&lt;p&gt;A few deployments worth knowing about if you're benchmarking this category:&lt;/p&gt;

&lt;p&gt;SNCF uses 3D stereoscopic sensors to monitor passenger occupancy on regional (TER) trains and at stations.&lt;br&gt;
Musée d'Orsay has run Acorel's people-counting tech for 15+ years, with security staff monitoring zone-by-zone occupancy on tablets during high-traffic exhibitions.&lt;br&gt;
Airport deployments (branded VISION Air) cover the full passenger journey — check-in, security, passport control, boarding — rather than a single chokepoint, with predictive congestion modeling layered on top of raw counts.&lt;/p&gt;

&lt;p&gt;That range — trains, museums, airports, retail — on one underlying platform is really a story about a well-abstracted data model: an "occupancy event" is an occupancy event whether it happens on a train platform or in a gallery, and the differentiation happens in the vertical-specific dashboards and integrations built on top, not in the core sensing pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways for building your own flow/IoT system
&lt;/h2&gt;

&lt;p&gt;If you're building anything in this space — smart building, retail analytics, transit ops — a few patterns worth borrowing:&lt;/p&gt;

&lt;p&gt;Normalize multi-sensor input early. Don't let downstream logic know or care whether a count came from infrared, LiDAR, or a repurposed CCTV feed.&lt;br&gt;
Push privacy-preserving reduction to the edge. Anonymize before transmission, not after storage.&lt;br&gt;
Treat the sensor as a trigger, not a report. The real product is what the data automates — HVAC, staffing, alerts — not the dashboard showing the number.&lt;br&gt;
Design one canonical data model across verticals. An occupancy/flow event schema that works for a train platform should also work for a retail floor, with vertical differences living in the application layer, not the core pipeline.&lt;/p&gt;

&lt;p&gt;People counting sounds like a solved problem until you actually have to build it at scale, across sensor types, with hard privacy constraints and real-time SLAs. Worth studying if you're in IoT, smart buildings, or anything adjacent.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>architecture</category>
      <category>iot</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
