What This Thing Does — and What Happens When You Click
First the product, then we follow one real click all the way into the code.
What is this app?
It is a clone of a city's public traffic-camera portal, modeled on Semarang, Indonesia. You get a live map of the city, and each colored dot is a real street camera.
Click a dot and live video plays with green boxes drawn around every vehicle, plus a running count of how busy that road is right now.
Every dot is a real traffic camera, placed where it actually sits in the city.
Tap a dot and the camera's live stream opens right there.
The AI finds each car and draws a box around it.
It tallies vehicles and tells you how crowded that road is, second by second.
Green free, yellow moderate, orange congested, red severe. No reading required.
You can read the traffic state of a hundred roads in one glance, because the answer is encoded as a color, not a sentence. Green means go, red means stuck.
Think of it as a tireless traffic control room
Picture a traffic control room: a wall of monitors showing far more cameras than any human could ever watch at once. A real operator can glance at maybe one or two feeds at a time, and only when they happen to be looking.
This app is the operator that never blinks. It watches every monitor simultaneously, recognizes the vehicles on each one, and turns what it sees into numbers and colors on the map.
A human operator
Watches 1-2 feeds at a time, gets tired, blinks, takes breaks, misses things.
This software
Watches all 50+ feeds at once, every few seconds, forever, and never looks away.
The hard problem was never the cameras — cities already have those. The hard problem is attention. There is too much video for people to watch. Software that watches everything, all the time, is the actual product.
Trace one click, end to end
You already know what happens on screen — a dot, a click, a video. Now let's follow the journey behind it. Three characters do the work: your browser, the server, and the AI detector.
Here is the same journey as cards you can scan:
Your browser visits the app's home address.
The browser requests the map page; the server builds it and sends it back.
The page asks "what cameras exist?" and draws a colored dot for each one it gets back.
The browser opens that camera's live video stream.
At the same time, the browser starts receiving live traffic counts from the AI detector.
The dot's color and the popup stats keep changing on their own, with no page refresh.
That very first step — your browser visiting the home address — runs one short function on the server. Here it is, in plain English:
@app.route('/')
def index():
"""Main page - Map with CCTV locations"""
return render_template('map_dashboard.html')
"When someone visits the front door (the home address /) ..."
"... run this little routine called index."
A note for humans reminding us this is the main map page.
"Assemble the map dashboard web page and hand it back to the browser."
The route @app.route('/') is the front door. render_template is the act of assembling an HTML page and handing it back so the browser can render it.
When you click a dot, two separate things start: the video arrives one way, and the live numbers arrive a different way. Keep that in mind — it's the key to the puzzle in the quiz.
What's behind the green box?
Behind each green box, the AI is examining the video one picture at a time — every still picture is called a frame — finding vehicles and tallying them as they cross an invisible line on the road.
That tally is what becomes the count, and the count is what becomes the color. We'll open up that machinery in Module 3.
Colors aren't decoration — each congestion state maps to one exact color, defined once and reused everywhere:
const colors = {
'FREE_FLOW': '#4CAF50',
'MODERATE': '#FFC107',
'CONGESTED': '#FF9800',
'SEVERE': '#f44336',
'UNKNOWN': '#9E9E9E'
};
Make a little lookup table from a traffic state to a color.
Free-flowing traffic becomes green.
Moderate traffic becomes yellow.
Congested traffic becomes orange.
Severe gridlock becomes red.
If we don't know yet, use a neutral gray.
End of the lookup table.
You've met the product and traced one click. Next, in "Meet the Cast," we introduce the characters that make it run: the server, an in-memory brain, per-camera workers, the AI eyes, vehicle memory, the database, and the map. Each gets a face and a name.
Check your understanding
These aren't memory questions — each one asks you to use the journey you just traced. Take a moment to think before you check.
You click a camera. The video appears, but the congestion number never changes. Based on the journey you traced, which link is most likely broken?
Why does the browser ask the server "what cameras exist?" instead of having the list baked permanently into the page?
The dot colors update on their own with no page refresh. In practice, what does that let a user do?
A road that should be busy shows video, but no green boxes appear on any vehicle. Which part of the system would you suspect first?
Meet the Cast
The seven characters that make traffic monitoring happen — and what each one is responsible for.
A Live TV News Crew
Picture a live news broadcast: camera crews in the field, an editor in the booth, an archive of old tapes, and the screen you watch at home. This app works exactly the same way — seven roles, each with one job.
Every part of the codebase maps to a role on a TV crew. When you want to change something later, you will ask: "which crew member owns this?"
One dedicated crew per camera, watching the road nonstop.
Glances at the frame and instantly names what is on screen: car, motorcycle, bus.
Remembers each vehicle across frames so the same car is never counted twice.
Serves the web pages, answers requests, and pushes live updates to viewers.
Holds the whole show in memory: every camera, the latest numbers, and the shared AI brain.
Files every reading away on disk so nothing is lost when the show ends.
What the viewer actually watches: a live map of Semarang lighting up with traffic.
The Brain That Holds It Together
The producer's clipboard, TrafficSystem, is one Python class. The app creates a single instance of it and keeps that one object alive the entire time the app runs.
Inside it are the cameras, the live numbers, the worker crews, the archive connection, and the AI model — all in one place. Here is the moment it is born:
class TrafficSystem:
def __init__(self):
self.cctvs = {} # CCTV configurations
self.streams = {} # Active stream handlers
self.detectors = {} # Detection workers
self.db = DatabaseManager()
self.model = None
self.running = False
Define the blueprint for our central "brain."
This runs once, the moment the brain is first created.
Start an empty notebook listing every camera (none yet).
An empty list of live video feeds it is watching.
The roster of per-camera worker crews — empty at first.
Open a connection to the filing cabinet (the archive).
A placeholder for the AI model; it gets loaded a bit later.
A flag saying "the show is not on the air yet."
The real brain also keeps a traffic_data notebook — the live readings (vehicle counts, congestion level) for every camera. That is the page workers scribble fresh numbers onto.
Notice every notebook starts empty ({}). The brain is born knowing nothing — cameras, workers, and readings get added as the show goes live. Engineers call gathering everything in one owner like this "centralized state."
One Crew Per Camera
Every camera gets its very own background worker — a daemon crew that watches that one road and nothing else. This is why self.detectors is a roster: one entry per camera.
If one camera freezes, only its own crew is stuck. Every other camera keeps filming. Independent crews mean a single bad feed never blacks out the whole broadcast. (Module 4 goes deeper on how these crews run side by side.)
Here is the studio floor plan — where each character lives in the project:
Match the Character to the Job
Drag each crew member onto the job they own. One symptom, one responsible character.
"It needs to look at one frame and shout out 'that's a bus, that's a motorcycle.'"
"The same car keeps getting counted again and again — we need it tracked as one vehicle."
"We want to look up yesterday's congestion levels long after the app restarted."
"Where do all the cameras, live numbers, and the AI model get held together?"
"The viewer needs to see roads light up red and green on a map of Semarang."
Think It Through
No definitions here — just situations. Pick the character you would actually talk to.
You want to add a feature that emails an alert whenever a road becomes "SEVERE." Which character is the natural place to trigger it?
Each camera gets its own worker thread. Why not use one worker that loops over all cameras, one at a time?
A tester reports that the vehicle count is way too high — the same car seems to be tallied many times as it drives past. Which character would you investigate first?
You have met the cast. Two of them — the eyes (YOLO) and the continuity assistant (DeepSORT) — do the hardest work: turning raw video into trustworthy numbers. In Module 3, "Turning Video Into Numbers," we open up that detection pipeline: the counting line and the congestion math behind every glowing road.
Turning Video Into Numbers
How a stream of pixels becomes "42 vehicles per minute — CONGESTED."
The person at the stadium door
Picture someone standing in a stadium doorway holding a hand clicker. Every time a person crosses the threshold, they click. Simple.
The hard part is not double-counting. If someone shuffles back and forth in the doorway, you must recognize them as the same person so you only click once. That "recognize the same one again" job is the secret sauce of this whole module.
The software is a tireless doorperson watching a traffic camera: it spots vehicles, gives each one a sticky identity, clicks once as each crosses a line, then turns the clicks into a congestion grade.
Grab a frame from the camera
YOLO finds the vehicles
DeepSORT gives each a sticky ID
Click as it crosses the line
Turn clicks into a congestion grade
Step 1 — Read the video, but not every frame
A camera streams roughly 30 frames per second. Examining all 30 with an AI every second is too slow to keep up live, so the worker analyzes only every 3rd frame — fast enough to stay in real time, accurate enough to count cars.
It counts frames and only stops to look closely when the tally is divisible by 3 — that is 1 in every 3 frames, about 10 looks per second.
# Process every 3rd frame for performance (10fps processing)
if self.frame_count % 3 == 0:
self.process_frame(frame)
A note to ourselves: looking at every frame is too slow, so we will look at 1 in 3.
"If the frame number divides evenly by 3..." — that picks out every 3rd frame.
...then send that frame off for the real AI inspection.
Engineers constantly trade a little accuracy for speed. Skipping frames means slightly coarser counts, but the numbers arrive now instead of falling further behind the live stream every second.
Step 2 — YOLO finds the vehicles (the eyes)
For each frame it does inspect, the system hands the picture to YOLO, an AI model that returns a bounding box around every vehicle and what type it is.
# Run YOLO detection
results = self.model(frame, conf=0.3)
det = results[0].boxes
Time to actually look at the image.
Hand the current frame to the AI model. conf=0.3 means "only keep guesses you are at least 30% sure about" — ignore the wild ones.
Pull out the list of boxes it found — one rectangle per vehicle.
Confidence
is the AI's own certainty score. The 0.3 is a
threshold
— a cutoff. Raise it and you get fewer, surer detections; lower it and you
catch more, including false alarms.
Steps 3 & 4 — Remember each vehicle, then click
YOLO has no memory — to it, every frame is a brand-new scene, so the same car looks "new" 10 times a second. DeepSORT fixes that: it tracks each vehicle and gives it a sticky ID.
Sees boxes in one frame, but forgets instantly. No memory.
"The red car is ID 7 — same one as last frame." One vehicle stays one vehicle.
A line drawn across the middle of the frame. The moment a tracked vehicle crosses it, click — plus one.
Here is the doorway click in code. It fires only once per vehicle, the instant its tracked position moves from one side of the line to the other:
if len(positions) >= 2 and not self.counted_vehicles[track_id]['counted']:
if (positions[0] < line_y and positions[-1] >= line_y) or \
(positions[0] > line_y and positions[-1] <= line_y):
self.counted_vehicles[track_id]['counted'] = True
self.vehicle_count['in'] += 1
Only consider this vehicle if we've seen it move (2+ positions) and haven't already clicked it.
Check if it started above the line and is now at or below it...
...or started below and is now at or above. Either way, it crossed.
Mark it "counted" so we never click this same vehicle twice.
Click! Add one to the running tally.
If DeepSORT isn't available, the code quietly switches to a simpler method that just tallies whatever boxes it sees — no sticky IDs, no real line crossing. It keeps running, but the counts get rougher. We'll dig into this safety net later.
Step 5 — From count to congestion (the math)
Every 10 seconds, the system takes its tally and turns it into a verdict. First it scales the 10-second count up to a full minute (multiply by 6), then sorts that rate into one of four buckets with a Level-of-Service letter grade. This is the hero of the whole module.
vehicles_per_minute = self.vehicle_count['in'] * 6 # Scale 10s to 1min
# Determine congestion level based on vehicle count
if vehicles_per_minute < 10:
congestion = 'FREE_FLOW'
level = 'A'
elif vehicles_per_minute < 30:
congestion = 'MODERATE'
level = 'C'
elif vehicles_per_minute < 60:
congestion = 'CONGESTED'
level = 'D'
else:
congestion = 'SEVERE'
level = 'F'
It counted vehicles over 10 seconds, so multiply by 6 to estimate a whole minute.
Now sort that per-minute rate into buckets...
Under 10/min? The road is basically empty — FREE_FLOW, grade A.
Under 30/min? Busy but moving — MODERATE, grade C.
Under 60/min? Getting clogged — CONGESTED, grade D.
60 or more? Bumper to bumper — SEVERE, grade F (gridlock).
FREE_FLOW
Under 10 vehicles/min. Open road — cruise.
MODERATE
10–29/min. Traffic present but flowing.
CONGESTED
30–59/min. Slow and packed.
SEVERE
60+/min. Gridlock.
Watch one frame travel through the pipeline
The four components are like coworkers passing a clipboard down the line. Step through their conversation as a single frame gets processed end to end.
Think it through
You've seen the full pipeline. Now use it to reason about what could go wrong.
A clearly jammed road reports only FREE_FLOW, yet the boxes appear correctly on the video. Where do you look first?
Why analyze only every 3rd frame instead of all of them?
You move the counting line to the very top edge of the frame. What likely happens to the counts?
Now that we have numbers, how do they get to your screen the instant they change? Module 4 follows the verdict and the live video as they race from the server to your browser.
Talking in Real Time
How the numbers and the live video reach your screen the instant they change — without you ever hitting refresh.
No more hitting refresh
Old websites were like a printed newspaper: to see anything new, you had to fetch a fresh copy yourself. This dashboard is different — the moment traffic changes at a camera, your screen updates on its own.
Think of it like a live sports broadcast. You sit back and watch; the picture and the scoreboard come to you. That magic rests on two ideas.
Doing many things at once
Every camera gets its own background worker — a thread — so one slow camera never freezes the rest.
The server pushes to you
Instead of your browser constantly asking "anything new?", the server speaks up the instant a reading changes, using a WebSocket.
Two feeds reach you at once: the moving video of the game and the scoreboard ticker that flashes the second a goal is scored. They travel on different pipes but arrive together. Hold that picture — it explains the whole module.
One worker per camera
Imagine 50 cameras handled by a single worker who must visit each in turn. If camera #12 hangs, everyone behind it waits. So each camera gets its own dedicated worker running in the background.
A separate background worker is launched per camera, all running at the same time. This is called concurrency.
If one feed stalls, its worker waits alone. The other 49 keep counting cars, because no worker can block the others.
Whenever a worker finishes counting, it hands its fresh reading to the central system — the "brain" — which decides what to do with it next.
Because each camera is independent, adding a 50th camera just adds a 50th worker — the rest never notice. Isolating work into separate threads so one failure cannot drag down the others is a cornerstone of robust system design.
One reading, three destinations
Here is the hero moment. When a worker computes a fresh reading, the brain does three things at once: it updates its live memory, saves a permanent copy, and pushes the news to every open browser.
def update_traffic_data(self, cctv_id, data):
"""Update traffic data and emit to clients"""
self.traffic_data[cctv_id].update(data)
self.traffic_data[cctv_id]['last_updated'] = datetime.now().isoformat()
# Save to database
self.db.add_traffic_data(cctv_id, data)
# Emit to connected clients (with error handling)
try:
socketio.emit('traffic_update', {
'cctv_id': cctv_id,
'data': data
})
except Exception as e:
print(f'[Socket] Emit error: {e}')
A method that takes a camera ID and its newest reading.
Overwrite the live in-memory numbers for that camera.
Stamp it with the exact time it was updated.
Write a permanent copy into the database for keeping.
Now broadcast the live news: socketio.emit(...) pushes the reading to every open browser at once.
The try/except means: if the push fails, do not crash the whole system — just quietly log the error and move on.
One input, three outputs: memory (so the app knows the current state), database (long-term records), and a live push (so you see it now). One function quietly keeps all three in sync.
Two channels to your browser
The numbers and the video do not travel together — they ride two separate pipes that happen to arrive at the same screen, just like the broadcast and its scoreboard ticker.
WebSocket — the newsflash
Carries the numbers. The server pushes each new reading the instant it changes. Tiny, instant, event-driven — like the scoreboard flashing the second a goal is scored.
MJPEG — the flipbook
Carries the video. The browser re-requests the latest processed photo about every 100 ms. MJPEG is really a fast flipbook of still photos.
Here is the video pipe. It loops forever, grabbing the latest processed image (boxes already drawn) and sending it down the wire as one more JPEG.
def generate_stream(cctv_id):
"""Generate MJPEG stream for a CCTV"""
while True:
if cctv_id in traffic_system.detectors:
frame = traffic_system.detectors[cctv_id].get_frame()
if frame:
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
time.sleep(0.033) # ~30fps
A function that streams video for one camera.
Loop forever — a broadcast never stops.
If this camera is up and running...
...grab its newest processed frame (the green detection boxes already drawn on it).
If we actually got an image, send it down the pipe as one more JPEG photo in the flipbook.
Pause ~1/30th of a second, then repeat — about 30 photos per second.
To stay fast, the system only analyzes every 3rd frame (counting cars 30 times a second would be wasteful). And it keeps only the newest frame in a size-1 buffer, dropping old ones. Per the architecture notes, this keeps latency constant at ~3–6 seconds instead of letting the delay grow forever.
Trace a single reading
Watch one brand-new reading travel from a camera worker, through the brain, splitting to both the database and a live push, until a dot on your map quietly changes color. Step through it.
On the browser side, the receiving end is just three short lines: store the new data, refresh the sidebar, and recolor the map markers.
socket.on('traffic_update', (data) => {
trafficData[data.cctv_id] = data.data;
updateSidebar();
updateMarkers();
});
Listen for the "traffic_update" newsflash the server pushes.
When one arrives, save the new numbers for that camera.
Redraw the sidebar list with the fresh figures.
Recolor the map dots so heavy traffic turns red — instantly.
Check your understanding
No definitions to recite here — just real situations. Pick what you would conclude, then check.
The live video plays smoothly, but the congestion numbers are frozen. Both come from the same server. Which channel is broken?
Why keep only the newest frame and drop old ones, instead of queueing them all up?
You add a 50th camera and the other 49 keep working perfectly. What design choice made that possible?
A teammate says "the map dot must recolor the moment a reading changes." Which line in update_traffic_data is doing that live work?
You just watched a reading get saved to the database. But where do those saved readings live, and how does the system know which real street each camera actually watches? Module 5 — Remembering and Mapping the Real World — opens up the SQLite memory and the road matcher that snaps every camera onto an actual road.
Remembering and Mapping the Real World
Where every reading gets filed away forever — and how the system learns which real street each camera is actually watching.
Why bother remembering?
The live counts you saw in the last module live in the app's memory — and memory is forgetful. The moment the app restarts, those numbers vanish to zero.
So the system writes every reading into a database. That permanent record is what gives us history, trends over time, and a full road-network view.
Every traffic reading becomes an index card, filed in the right drawer. Nothing is lost — you can pull yesterday's cards anytime to see how rush hour unfolded.
Three drawers in the filing cabinet
The database is split into three tables, each holding one kind of thing.
cctvs
Each camera's identity: its name, its map location (latitude/longitude), and the link to its live video stream.
road_segments
The real shape of each road (its geometry), plus its speed limit and how many vehicles it can carry.
traffic_data
Every reading ever taken, over time. One row per measurement: counts, congestion level, and a timestamp.
What one reading looks like on disk
Before you can file cards, you write a blueprint describing what each card holds. That blueprint is the schema. Here is the exact blueprint for the readings drawer.
CREATE TABLE IF NOT EXISTS traffic_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cctv_id TEXT NOT NULL,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
vehicle_count INTEGER DEFAULT 0,
vehicles_per_minute REAL DEFAULT 0,
cars INTEGER DEFAULT 0,
motorcycles INTEGER DEFAULT 0,
buses INTEGER DEFAULT 0,
trucks INTEGER DEFAULT 0,
congestion_level TEXT DEFAULT 'UNKNOWN',
los TEXT DEFAULT '-'
)
Make the readings drawer (if it doesn't already exist).
Give every card a unique number, automatically — that's what PRIMARY KEY AUTOINCREMENT does.
Which camera took this reading (it must always be filled in).
When it happened — stamped with the current time by default.
How many vehicles total, and how many per minute.
A breakdown by type: cars, motorcycles, buses, trucks.
The congestion label, and the Level-of-Service grade — both start as "unknown".
Joining the dots
To color a whole road on the map (not just a camera dot), the system links each road to its camera and pulls that camera's latest reading. Stitching two drawers together like this is called a JOIN.
Knows the road's shape — and which camera watches it.
Match the road to its camera, then to that camera's newest reading.
Supplies the latest congestion level — so the whole road lights up.
Pinning cameras to real streets
A camera knows its coordinates, but not which street it overlooks. So the system acts like a surveyor: it walks out to each camera and pins it to the exact road on the map.
It asks OpenStreetMap (through the Overpass API) "which roads are near this point?", then measures the straight-line distance to each with the haversine formula and snaps the camera to the nearest one.
def haversine_distance(lat1, lon1, lat2, lon2):
"""Calculate distance in meters between two lat/lon points."""
R = 6371000 # Earth radius in meters
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlam = math.radians(lon2 - lon1)
a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlam / 2) ** 2
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
A helper that takes two map points and returns the distance between them.
Its one job: measure meters between two latitude/longitude points.
Start with the Earth's radius, in meters.
Convert the coordinates into the angle units the math needs.
The two big lines are the haversine formula doing trigonometry on the globe's curve.
Hand back the real-world distance in meters — used to find the closest road.
The matching process, step by step
Read every camera from the database
Ask OpenStreetMap for roads within ~150 m
Measure distance to each with haversine
Pick the nearest road
Save its shape and link the camera to it
The map server is a free, shared service — think of a nightclub with a strict capacity limit and a bouncer. Barge in too fast and you get turned away (a rate limit). So the code waits its turn in line: it sleeps about 1.5 seconds between cameras, pauses and retries when the server is busy, and rotates between backup servers so it never overwhelms the free public API.
Check your understanding
No memorizing — each question asks you to reason about how memory and mapping really work here.
The app crashes and restarts. The live numbers reset to zero, but yesterday's congestion trend is still available. Why?
Why store each reading as its own row instead of overwriting one "current" value per camera?
The road matcher sleeps between requests and rotates between backup servers. What problem does that prevent?
Why does the matcher run the haversine formula on every nearby road for a camera?
Real cameras, real networks, real map servers — they fail constantly. In the final module we'll see how this system copes, and how you'd debug it when something goes wrong.
When Things Break — and the Big Picture
Real cameras and networks fail constantly. Here is how the system copes, how to debug it from the symptoms, and how every piece fits together.
Software Meets the Messy Real World
Camera feeds drop. Files go missing. A library quietly changes its rules in a new version. Robust software does not hope this won't happen — it expects it.
A frozen video, a number stuck at zero, a crash on startup — each is a symptom. A good engineer (and a good AI-steerer) reasons from symptom to likely cause, exactly like a doctor diagnosing a patient. Keep this in mind for the whole module.
A traffic camera's feed cuts out mid-broadcast — the network hiccuped or the camera went offline.
An AI helper expects a downloaded data file that simply isn't there.
An updated dependency breaks something that worked perfectly yesterday.
Symptom: The Camera Stream Won't Open
The app tries to open a feed with its primary video engine (FFmpeg). If that fails, it tries a different engine before giving up. A backup plan beats surrender.
self.cap = cv2.VideoCapture(self.stream_url, cv2.CAP_FFMPEG)
# Set buffer size to reduce latency
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
if not self.cap.isOpened():
print(f"[Detector {self.cctv_id}] Failed to open stream, retrying with HTTP...")
# Try with GStreamer as fallback
gst_pipeline = f'souphttpsrc location={self.stream_url} ! hlsdemux ! decodebin ! videoconvert ! appsink'
self.cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER)
if not self.cap.isOpened():
print(f"[Detector {self.cctv_id}] Failed to open stream completely")
self.system.cctvs[self.cctv_id]['status'] = 'error'
return
Try to open the camera feed using the primary video engine (FFmpeg).
Keep almost no video backlog, so what we see is as fresh as possible.
Did it fail to open? Then we need a plan B.
Leave a note in the log: "couldn't open it, retrying a different way."
Build a recipe to open the same feed using a second engine (GStreamer).
Try again with that backup engine.
Still no luck? Only NOW do we admit defeat for this camera.
Mark this one camera as 'error' and stop — without touching the others.
The lesson: try a backup before giving up. And notice — only this camera is flagged. Its neighbors keep streaming.
Symptom: The AI Tracker File Is Missing
DeepSORT — the continuity assistant from Module 2 — needs a downloaded checkpoint file to do its smart tracking. If that file is missing, the app does not crash. It quietly falls back to simpler counting and keeps running.
if os.path.exists(cfg_deep.DEEPSORT.REID_CKPT):
self.deepsort = DeepSort(
cfg_deep.DEEPSORT.REID_CKPT,
...
use_cuda=torch.cuda.is_available()
)
self.use_deepsort = True
print(f"[Detector {self.cctv_id}] DeepSORT initialized")
else:
print(f"[Detector {self.cctv_id}] DeepSORT checkpoint not found, using simple detection")
First, check: does the tracker's data file actually exist on disk?
If yes, switch on the smart tracker...
...feeding it that checkpoint file...
...(plus several settings the learner doesn't need)...
...and let it use the graphics card if one is available.
Close out that setup.
Flip a switch saying "smart tracking is ON."
Log that DeepSORT started successfully.
But if the file is missing instead...
...just print a note and quietly carry on with simpler counting.
Losing one feature instead of the whole app is called graceful degradation. The app drops down to simple counting (less accurate, as we saw in Module 3) but stays alive. When you steer an AI tool, ask for exactly this: "if X is missing, fall back to Y instead of crashing."
Two Gotchas Worth Knowing
Two failure stories from this real project — the kind that teach you how to read symptoms.
The Library-Version Trap
A security change in PyTorch 2.6+ changed how AI model files load — and broke the app on startup. The fix wraps the loader to restore the old behavior.
Thread Safety
Each camera worker loads its own copy of the YOLO model — not one shared model. Costs extra memory, but two threads never step on each other.
The Latency Budget
"Real-time" still has a delay of about 3–6 seconds end-to-end: latency from network buffer + capture + AI + streaming. Know your budget.
The version-trap fix — a small wrapper that quietly restores the old loading rule:
def safe_torch_load(*args, **kwargs):
"""Wrapper for torch.load that sets weights_only=False"""
if 'weights_only' not in kwargs:
kwargs['weights_only'] = False
return original_torch_load(*args, **kwargs)
Define our own gentle wrapper around the model-loading function.
A note to ourselves: this restores the old, more permissive loading rule.
If the caller didn't say how strict to be...
...set it to the old, lenient setting so our model loads like it used to.
Then hand off to the real loader with those adjusted settings.
The thread-safety move is a single, deliberate line inside each worker:
# Load YOLO model (each thread needs its own)
self.model = YOLO('yolov8n.pt')
A comment spells out the intent: every worker gets its very own model.
Load a fresh, private copy of the YOLO eyes for this one camera's crew.
Sharing a single AI model across many workers seems cheaper — but two threads reaching into the same object at once can corrupt each other's work. Giving each worker its own copy trades a little memory for thread safety. "Buy safety with memory" is a classic engineering trade.
The Big Picture
Here is the whole system, internal and external. Click any box to see what it does — this is everything you have learned, on one map.
What You See
The Server
The AI Pipeline
The Messy Outside World
Read it left to right: the messy outside world feeds in, the workers run the AI pipeline, the brain holds the live numbers and saves history, and the server pushes it all to your browser.
Diagnose From the Symptoms
The finale: four real debugging situations. Read each symptom and reason your way to the cause — just like a doctor.
One camera shows 'error' on the dashboard while every other camera streams fine. Where do you look, and what is the likely cause?
After you update the project's Python libraries, the app crashes on startup while trying to load the AI model. What is the likely cause?
The counts look roughly correct but feel oddly imprecise, and the logs say "DeepSORT checkpoint not found." What happened, and is the app broken?
The dashboard consistently lags about 4 seconds behind what's actually happening on the road. Bug or expected?
You have seen every character and how they cope when the real world misbehaves. Carry three habits into your work with AI coding tools: ask for graceful fallbacks ("if X is missing, fall back to Y, don't crash"), watch for version traps ("it worked yesterday" often means a dependency moved), and reason from symptoms — diagnose like a doctor, from what you see to the likely cause. That is the whole course. Go build.