This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine. In 2001 I was finishing electronics engineering at the Universidad Iberoamericana in Mexico City. The final project was ambitious: a mobile robot that steered by what it saw. There were three of us, so we split it in three, and the vision part was mine: a DSP and one of the first USB cameras, 640×480 in grayscale, one frame per second, slow to respond and with every frame to process. It worked within the limits of its paradigm: no inference at all. The background had to be uniform, we even had to know its color in advance, and the object had to be less than 50 cm away. Window filters, a threshold and a centroid did the rest.
The images of that era came with ghosts, though not from that camera. Analog TV had them literally: the signal bounces off a building, arrives a few microseconds late, and a faint copy of everything sits to the right of the picture. At the time I also worked on an echo canceler that measured that ghost on a reference signal and took it out.
This week's theme is the ghost in the machine, in the days when Mexico remembers its dead. So I brought that eye back, wrote it again, and haunted the robot's scene with the ghosts of analog TV. Then I put it in front of two vision models from today, running locally on a mini PC. Same haunted frames for all of them. Which one does the ghost fool?
Three eyes, and a scene to haunt them.
Every frame is synthetic and comes from a fixed seed, so the true position of the disk is known exactly. Nobody has to judge by eye who got it right.
The whole run, frame by frame, is at crtdrops.mx/fantasmas (English and Spanish on the same page). Pick a ghost, a strength and a frame, and the page draws where each eye said the disk was, next to where it really was, with what each model answered.
github.com/israel0negrete-crtdrops/ghosts-in-the-frame, MIT. It is the code exactly as it ran, with the logs and every answer each eye gave (the models' answers saved up to their first 300 characters; four of Qwen's are cut there). Names and comments are in Spanish, as they ran; the prompts are in English. comparar.py rebuilds the comparison from those answers, byte for byte.
The scene (escena.py, standard library only) writes each frame as a PGM and the true positions as JSON. The top 8 rows carry a reference signal: black with a short white pulse, the way analog TV carried test and ghost-cancel reference signals in its vertical blanking interval. The echo distorts that pulse like everything else, and that is the whole point.
The echo canceler looks for the copy of the pulse in the reference rows. Where the copy starts gives the delay of the ghost; how bright it is gives its strength. Then it removes the ghost line by line with a recursive filter, y[x] = g[x] − a·(y[x−d] − black), the exact inverse of the ideal single bounce this scene simulates. A real ghost has more bounces and is never this clean. The canceler does not guess from the picture: it measures the ghost on a signal whose shape it already knows. That is an advantage the models don't have. They see the same rows, and their prompt only tells them to ignore them.
The filters and the centroid. A 3×3 window slides over the frame. The median throws away isolated outliers, which is what impulse noise is. Then every pixel above a fixed threshold counts, and the centroid of those pixels is the answer. Four filters plus none, with and without the canceler: ten variants.
The models get each frame as a PNG through Ollama's API, with temperature 0, seed 42 and a 4,096-token context. Gemma is asked for the center of the disk as {"x", "y"}. Qwen is asked for its bounding box, which is how it was trained to locate objects. The run script only works from 7:00 to 21:30, so it never competes with the lab's night shift, and it can be stopped and resumed.
I built it with Claude Code as a pair: the scene and the techniques come from my proposal, Claude wrote most of the code, and we ran it together on the lab machines. The 2001 eye also ran on a Raspberry Pi 5 and gave exactly the same points as on the N100.
Same 72 frames (18 cases, four frames each), on the N100:
| Eye | Median error | Time per frame |
|---|---|---|
| 2001 · median filter + echo canceler | 0.1 px (on the hardest ghost, Gaussian σ 50: 7.8 px) | 37.5 ms |
| Qwen2.5-VL 3B | 1.3 px on the 37 frames where it gave a full box | 146 s |
| Gemma 3 4B | 177.8 px | 104 s |
Qwen did not always answer the same way. On 37 frames it gave a full box, and the center is measured. On 32 it gave only the top-left corner, 2.8 px off, with a width and height it made up (from 10 to 500 px for a disk about 71 px wide in its scale), so there is no center to measure. On 3 it kept nesting the same structure until the 4,096-token context ran out, with no coordinates at all.
The 2001 eye is closer, and about 3,900 times faster than Qwen. Both on the same CPU, with one difference worth saying: the C timing covers the processing of a frame already in memory, while the models' time includes the HTTP call, decoding and resizing the image, and writing the answer.
Qwen sees the disk, but answers in another scale. The first numbers said its median error was 366 px, which looked like blindness. It wasn't. Ollama reported 1,122 input tokens on every frame, and that number gave it away. Take out the roughly 86 tokens of the prompt and 1,036 are left: 37×28 tokens, one per 28×28 px block, which means Ollama had enlarged the 640×480 frame to 1,036×784 before Qwen saw it. Qwen answered in that image, even though the prompt said 640×480. Rescaled, the four clean frames land within 1.4 px. I didn't tune the scale until it fit; it comes from the token count.
The echo fools Qwen first. On 10 of the 11 echo frames where Qwen gave a full box, the box takes in the disk and its ghost together: about 86 px wide, which is the 44 px disk plus the 40 px delay, so its center moves about 20 px to the right. That happens from the weakest echo, 20 %, even though its prompt says to ignore any fainter ghost copy of the disk. The 2001 eye without its canceler withstands the 20 and 35 % echoes, but only because a ghost that faint stays under its fixed threshold; at 50 and 70 % the ghost passes it and drags the centroid the same 20 px. With the canceler it stays within a pixel on all four.
Gemma answered (320, 240) on all 72 frames, the clean ones too: the exact center of the image. With this prompt it does not locate the disk. I tried one prompt only; another might do better.
Not every 2001 trick works. With the Laplacian or the minimum-distance edge test and a fixed threshold, the eye loses the disk as soon as there is any noise at all: Gaussian σ 10, or 1 % impulse noise. I picked the median + canceler variant after seeing the results, as the one with the lowest worst case. All ten variants are in the repo, the bad ones included.
gemma3:4b (id 68ceab7e468a) and qwen2.5vl:3b (id fb90415cde1e), both Q4_K_M, on Ollama 0.40.6. Anyone can pull them, check that the ids match and run the same frames again. A hosted model can change between one run and the next; these only change when I change them.
To be precise about "open": Gemma's weights are under the Gemma Terms of Use, and Qwen2.5-VL 3B's under the Qwen Research License, which is non-commercial, even though Ollama's listing shows Apache 2.0. Neither is an OSI license. What matters here is that the weights are on my disk and I can run them, inspect them and pin them. Ollama is MIT, and so is my code.
None of this says the models are bad. A 3B model that has never seen my scene, with a short prompt as its only instruction, finds a 44 px disk to within a pixel or two when it gives a full box and there is no echo. The 2001 eye knows exactly one thing, and it was made for that.
But that is the lesson I keep from this: when you know the shape of your ghost, you can measure it and take it out, and a few lines of arithmetic beat a model that has to guess. I felt proud seeing it. Mathematics runs the universe, and even with AI it will still be the base of everything. Nostalgia, yes. But technology moves forward by improving on what it already learned, and nothing is impossible.
Best Use of Gemma: Gemma 3 4B runs locally in Ollama as one of the two vision models in the comparison. This post reports what it did, including that with this prompt it answered the center of the image every time.
What technique from twenty-five years ago do you still trust more than a model?
Israel Negrete Lepe is an electronics engineer. Twenty-five years making different systems work together under real conditions: from the sensor, the vehicle or the transaction to the backend, the data, security and operations. Today, increasingly, edge AI. I take on projects by the hour or by deliverable: israel0negrete@gmail.com.