theCensor; What an ML Prize Actually Measured in 2019

hmm..

pop up spam

In November 2019, I spent 22 hours at HackSussex building a thing that watched my screen for NSFW content and closed the tab when it found some. It won first prize in the ML category of Hackathon. I was barely a third year CS student. The repo has sat untouched since. I opened it again recently. 2019 was a long time ago is all that I can say.

The idea

From the README, written at 10am on the second morning:

The objective is to remove "this type of content" when the user did not specifically search for it, for example, when watching a serie or a movie with friends and family.

It isn't about policing or being prude-ish. It's about the unrequested stuff you're deep into a film in a safe for work environment and the film decides it's that kind of film now. A tool that quietly takes it off the screen before you have to lunge for the keyboard is something a person might genuinely want. That was the "serious" pitch. The real pitch was that it was funny to watch a friend get spammed by memes, a horn and a tab closing itself. It was just what a 20 year old thought was funny at the time. Silly billy.

The plan had three parts: detect NSFW content on screen with a CNN, use YOLO to locate where on screen it was, and blur that region.

We shipped one of the three. Piping the live screen capture into a locally run CNN in a loop was hard enough on a meh-GPU (official intel name) laptop.

What 2019 felt like

It's worth saying what the ground looked like then, because it's easy to forget how much scaffolding didn't exist yet. I did some googling.

Waves from AlexNet were still propagating, CNNs were still the hype then, pre diffusion. We were 2 years into Transformers/Attention, BERT-like architectures were just getting off the ground. Before the LLM "revolution", CV models truly had an extended and impressive moment. You could not escape the billions of character recognition/MNIST Kaggle side projects. I remember my CV coursework being something like classifying images into sunny and not sunny. Which felt truly like magic.

No one's jobs was being taken away by models, no existential economical crises looming, no talks of an AI bubble, no AI girlfriends, no mainstream demos of "kill chains", one could even hire a junior dev and the word slop was not remotely in my purview. simpler times.

On the flip side, Keras was standalone on TensorFlow 1.x. tf.keras hadn't finished swallowing everything. There was no pipeline() you could import that would hand you a working vision classifier in four lines. There was no HuggingFace hub for this. If you wanted a model that could sort the safe-for-work from the "not" (wink wink), you went looking on GitHub, you found somebody's .h5, you downloaded it from their S3 bucket.

So the actual work of this project was: get a fine-tuned InceptionV3 loaded and running. Wire mss into a live screen-capture loop. Run inference continuously on a laptop, fast enough to matter. Take the prediction and drive the operating system back with it. Every one of those is a thing you had to "assemble by hand", remember that? artisan coding without agents.

That's what the ML prize measured. Being funny to a lecture hall of 18-20 year olds.

The model isn't mine, and that's the point

The classifier is GantMan/nsfw_model by Gant Laborde. Off the shelf, fine-tuned InceptionV3, 299x299 input, five-way softmax over a label set that runs from neutral and drawings at one end to the categories you'd expect at the other. I didn't train it. I didn't fine-tune it. Our first commit dropped his entire repository into ours and the weights came down from his bucket. Open setup.py today and it still says AUTHOR = 'Gant Laborde'. There's a whole training/ directory in there his, with a two-stage transfer learning setup and a hand-tuned LR schedule that we never ran a line of. It still points at D:\nswf_model_training_data, a path that never existed on any machine we owned. We didn't even "sample" the training data in the computer labs. badum tss.

We did not train a model in 22 hours. What you do in 22 hours is know the model exists, know it's good enough, know how to get it running, and the part that's actually hard know what to do with a prediction once you have one.

That last question is the whole project, and it's where everything interesting happened.

The pipeline

The loop is about sixty lines of main.py: **

mss.grab({'top': 400, 'left': 200, 'width': 800, 'height': 800})
  → cv2.imwrite('data/original/data.jpg')
  → resizeimage.resize_cover(image, [299, 299])
  → detector.predict('data/original/data.jpg')
  → argmax over the five class labels
  → if the winner is one of the NSFW ones: punishment()

** Bad implementation all around because when I reread this code my first instinct was "ew": every frame gets written to a JPEG and read back, even though mss.grab already handed us the pixels in a numpy array. Writing to disk, every frame, on data we already had in memory.

Then I actually read the library again. predict() takes file paths. That's its entire public surface it calls keras.preprocessing.image.load_img() on them internally and keys the results dict by path.

05:33

At half five in the morning, one commit landed: YOLO (yolo.py, yolo_utils.py), a PowerShell script called blurrer.ps1, ten reaction JPEGs, an MP3 named nope.mp3, and an empty file called blur.py.

That's the project in one snapshot. The ambitious plan, the fallback, the joke, and a placeholder where the headline feature was supposed to go.

blur.py is zero bytes. It has been zero bytes since 05:33:37 on November 10th, 2019.

11:42

Localization was never going to land. Not because the idea was wrong, but because it was a second project bolted onto the side of the first one, and we picked it around hour twelve of twenty-two. The same commit widened the trigger from the single strictest class to three of the five detect harder to make up for not knowing where and replaced the entire blur plan with this:

def punishment():
    hotkey('ctrl', 'w')
    os.system("sound.py")

Can't blur the bad part of the page? Close the page. Play a noise.

This is the best decision we made all night. The alternative was burning the last six hours debugging localization to feed a blur that didn't exist yet, and finishing with a demo that did nothing. Instead we finished with something crude that worked every single time.

11:56

The last commit before judging isn't code. It's nope.mp3, re-encoded louder. 66KB to 111KB. Nothing else in the diff.

It won anyway. Turns out judges really like when a lecture hall full of 18-20 year olds are laughing uncontrollably at a demo.


theCensor HackSussex 2019, with Basile B, Vivien N, and Elodie M. I haven't spoken to them since sadly, hope to run into them again one day. blur.py is still empty. It's staying that way.