Most of the plumbing for this already exists in v3, so here's what I found reading the code, plus the two things that would need new work.
What's there: each post carries at most one Image-v1 line, and that line already records the width, height and a description. So a grid can reserve the right aspect ratio for every tile before anything loads, and the description can be the alt text. (The current list thumbnails use alt="", which a gallery shouldn't copy.) ListThumbnails::forPosts() already turns a page of post IDs into image entries in one query, and it works the same for replies as for thread roots. Including reply images costs nothing extra; each tile would just link to /posts/<id> instead of the thread.
What's missing:
Real previews. Today the board and Recent thumbnails point straight at /images/<sha256>.<ext>, which is the original upload (up to 2048 px a side, 5 MiB). Fine for a few rows, but a grid of 40 would pull 40 originals. I'd generate a small preview per hash with GD (already loaded for upload validation) and cache it outside the git repo. It's derived from the hash, so it can always be rebuilt and never needs signing.
A way to find image posts without scanning bodies. forPosts() starts from IDs the page already has. A gallery has to ask "newest posts with an image," and right now that means instr(body, 'Image-v1:') across the whole posts table. That's fine at today's size. Longer term, a has_image column (or a small images table) filled in at import time would keep it cheap.
For a first version I'd skip filters: newest first, roots and replies mixed, title or first line plus reply count under each tile, and keep it behind a feature flag the way list thumbnails are. Ordering by likes could come later.