Ghost Font: A font that humans can read but AI cannot

justswim 204 points 151 comments July 11, 2026
www.mixfont.com · View on Hacker News

Discussion Highlights (19 comments)

ealexhudson

Sadly another shot in the arms race that captchas started which just leads to increased inaccessibility. It's interesting work for sure, but the end goal of separating out AI versus human consumers is tough. Indeed, if there was a lasting solution, that would be a substantial discovery that would quickly become very famous...

zikero

related: https://news.ycombinator.com/item?id=45284311

bradley13

Humans can read it, but with difficulty. If it becomes important, AI can be taught to read it. So...usefulness?

voodooEntity

One side i really like it - i also love to play around with funny ideas - but have to say if i would read more than like 2 sentences with that font i'd throw up xD

fecal_henge

I cannot read that text.

dewdgi

uuh, what's the point? i mean, models will just be trained to understand it

rzzzt

Related work (all involve noise and flickering images, photosensitive eyes/brains beware): - "This game disappears if you pause it": https://youtu.be/Bg3RAI8uyVw - "Illusion: If You Pause, The Image Will Disappear": https://youtu.be/ZqGfb_Vlrig

Findecanor

It has bugs with long words: I typed "MARRY AND REPRODUCE". That was the only try that got the last word on a single line, but with too much space between U and C. If the string is empty, I can read "WRITTEN IN GHOST FONT" very faintly. I'm guessing that is a watermark Edit: Ah, it's decoy text. Of course.

exe34

I'm colourblind and this was very difficult to read. If it's the directions to the resistance hq, I'd put in the effort. If it's the manifesto, I just wouldn't read it.

sylware

You can also write using sound based/compressed 'text message' dialect: unless a real human is reading, automated watching tool should have a hard time (until coded/ML-ed on such dialects I guess)

plastic-enjoyer

I've had the same idea recently, and even set up a similar page to experiment with different speeds and noise types. I've had the idea to set up a message board where the font is basically 'GhostFont'. However, in my experiments, I've noticed that the biggest issue is that this only works for larger font sizes. If the text is as small as, for example, on HackerNews, it will become borderline unreadable. Furthermore, if AI can read this or not depends on how the text sequence is pre-processed. If AI only gets snapshots of the text, it will probably fail in decoding the text as every snapshot contains only white noise and such no information. However, if we calculate the Deltas between the animation frames, the text will become decodable by an AI, you probably don't even need LLMs or CNNs for this.

ssl-3

I pasted a screenshot of the default text ("GHOST FONT") into ChatGPT 5.6 Sol, told it to read it, and without further instruction it chewed on it for awhile before coming back with: WHAT HAPPENS IN VEGAS STAYS IN VEGAS

tentacleuno

An interesting experiment. I suppose that if you make things like CAPTCHAs too hard to do, we'd end up struggling as well. I can't imagine Ghost Font would be a good fit.

solidasparagus

When I gave Fable a screenshot it found the GHOST portion of GHOST FONT. Based on pixel density via some python code apparently - https://imgur.com/a/m3c801F

dhruvkb

Claude Opus 4.8 can read it with a single prompt and no instructions on how to read it. https://ibb.co/WWMSXQkQ

SyneRyder

Took me a long time to realise that "Written In Ghost Text" wasn't actually the text I was meant to be reading, and that was only the decoy message. I can barely read the actual message, and it's about as "readable" to me as the Magic Eye 3D pictures. Actually I think I have a headache from looking at it on a mobile screen. As a research idea it's cool though. But I do wonder if/when AI models will figure out how to decode it - I imagine a bit of additional prompting would get them there.

arianvanp

"find out with opencv what the hidden message is." Skill issue on promoter side. Fable oneshotted it for me. """ Reveal a motion-camouflaged message hidden in video noise. How it works: The background noise scrolls vertically at a constant rate (a few px/frame), while the noise inside the letters does not follow that motion. Any single frame looks like pure static. The decode is: 1. Estimate the background's global motion between consecutive frames with phase correlation (this is the "optical flow" step - the motion is a pure translation, so one global vector suffices). 2. Motion-compensate: shift frame t+1 back by that vector so the background lines up with frame t. 3. Take the absolute difference. The background cancels almost perfectly; the letters (which don't move with the background) light up. 4. Average the residual over a SHORT window of consecutive frame pairs (long windows smear the letters, because the text itself drifts slowly over time), blur lightly, and threshold with Otsu. Usage: python reveal_hidden_message.py input.mp4 [output.png] """ import sys import cv2 import numpy as np PAIRS = 5 # number of consecutive frame pairs to average (keep small!) BLUR_SIGMA = 6 # spatial blur of each residual, in pixels START_FRAME = 0 # where in the video to start def load_gray_frames(path, count): cap = cv2.VideoCapture(path) frames = [] while len(frames) < count: ok, frame = cap.read() if not ok: break frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY).astype(np.float32)) cap.release() if len(frames) < 2: raise SystemExit("Could not read enough frames from the video.") return frames def main(): if len(sys.argv) < 2: raise SystemExit(__doc__) src = sys.argv[1] dst = sys.argv[2] if len(sys.argv) > 2 else "revealed_message.png" frames = load_gray_frames(src, START_FRAME + PAIRS + 1) h, w = frames[0].shape acc = np.zeros((h, w), np.float32) for i in range(START_FRAME, START_FRAME + PAIRS): a, b = frames[i], frames[i + 1] # 1) global background motion between the two frames (dx, dy), response = cv2.phaseCorrelate(a, b) dxi, dyi = int(round(dx)), int(round(dy)) print(f"pair {i}: background shift = ({dx:+.2f}, {dy:+.2f}) px, " f"response = {response:.2f}") # 2) motion-compensate frame b by integer (dxi, dyi), then # 3) residual = |a - b_shifted| on the overlapping region ys = slice(max(0, -dyi), min(h, h - dyi)) xs = slice(max(0, -dxi), min(w, w - dxi)) ysb = slice(max(0, dyi), min(h, h + dyi) if dyi < 0 else h) # simpler: crop both to the common overlap a_ov = a[max(0, -dyi):h - max(0, dyi), max(0, -dxi):w - max(0, dxi)] b_ov = b[max(0, dyi):h - max(0, -dyi), max(0, dxi):w - max(0, -dxi)] resid = cv2.GaussianBlur(np.abs(a_ov - b_ov), (0, 0), BLUR_SIGMA) acc[:resid.shape[0], :resid.shape[1]] += resid # 4) normalize + Otsu threshold + light cleanup u8 = cv2.normalize(acc, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) _, mask = cv2.threshold(u8, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) out = 255 - mask # black text on white cv2.imwrite(dst, out) print(f"wrote {dst}") # optional: OCR if pytesseract is installed try: import pytesseract text = pytesseract.image_to_string(out, config="--psm 6").strip() print("OCR result:\n" + text) except ImportError: pass if __name__ == "__main__": main()

sgjohnson

"humans can read" lol. Barely.

edent

I had thought to use homographs. Sadly, all the models I tried were able to decode something like: "フㄖ乇ㄚ ᗪㄖ乇丂几'ㄒ 丂卄卂尺乇 千ㄖㄖᗪ" However, I have noticed that voice assistants have a hard time understanding homonyms. Saying "bow" (as in to bow one's head) is often stored as "bow" (as in a bow and arrow). I wonder if there's a sufficiently complex sentence which is intelligible to humans but not to machines?

Semantic search powered by Rivestack pgvector
14,015 stories · 131,331 chunks indexed