Pangram verdict · v3.3
We believe this text is mainly human-written, with some AI content.
AI likelihood · overall
HumanArticle text · 1,692 words · 1 segments analyzed
2026-07-29 — 3020 Words — 16 min I'm very excited to announce the 1.0 release of Perspec! Perspec is a desktop app for correcting the perspective of images. This is primarily useful for photos of documents and receipts, but it can be used for any kind of image. This has finally become the app I envisioned when I started working on the project 9 years ago. I didn't think it would take me this long to get here, but I'm very happy with the result and I hope you'll like it too! Initial Motivation You're probably familiar with the scanner apps available for mobile phones, like Adobe Scan, vFlat, SwiftScan, … and numerous others. Scanning functionality is also integrated into Dropbox, and these days even natively into iOS itself. However, I don't like working on my phone and I'd rather just take photos of the documents and receipts and deal with cleaning them up and organizing them on my computer another day. There, I have a big screen, a keyboard, and a precise mouse, which makes editing faster and more accurate. Also, the mobile apps make some annoying technical decisions in the name of giving users something they're familiar with. For example: If you store a document as a grayscale PNG, you can get small file sizes without introducing any compression artifacts. However, all the popular apps will give you a grayscale JPEG image with a much bigger file size and worse image quality, just because JPEG is what people are familiar with. Or maybe I'm giving them too much credit and they actually don't know that PNGs can be smaller than JPEGs if the image contains large areas of uniform color, whereas for normal photos, JPEGs are smaller than PNGs. And no, converting it to PNG afterwards is not an option, as by then the image already contains all the JPEG compression artifacts. For example, let's compare the results of scanning the following document: The other apps produce bigger files, and you can clearly see the compression artifacts that degrade the result. App Result Preview Notes Perspec ~110 kB, PNG View result Scanner Pro ~190 kB, JPEG View result Extracted JPEG from exported PDF iOS ~300 kB, JPEG View result Extracted JPEG from exported PDF Another thing that annoys me more than it should is the ridiculous detection previews that seemingly every app includes these days: While you're taking a photo, the app shows you a live overlay of where it detects the document. This, however, doesn't help you at all. Just because it can detect the document correctly in the preview video feed doesn't mean it will detect it correctly in the final photo. Due to the higher resolution, different lighting (exposure times, flash, …), and different contrast, the detection will often be quite different in the final photo. So all the preview is telling you is that there is indeed a document in front of your camera, which you already know since you placed it there. 🤦♂ Lastly, and most importantly, I knew I could build a better document detection algorithm for the kind of photos I was taking. The detection in existing apps would often be slightly off, even if you had a good picture with good contrast between the document and the background. Most apps use some kind of edge detection step in their pipeline, as Dropbox explains here. But I knew that documents and receipts often don't have straight edges but rather wrinkled or curved ones. When you try to match even just a slight curve with a straight line, the endpoints will be quite far off. So instead, the app should try to detect the corners and build up the document from there. There is a detailed explanation of the computer vision techniques later in the post. The Long Road to 1.0 I was still a student when I started working on Perspec and had to scan a lot of stuff for my studies, so I had plenty of motivation to build something like this. Sure, you could also fix the perspective with Photoshop, Affinity Photo, or GIMP. But the overhead is substantial: Open each photo, find the perspective tool, drag the corners, pick the right export settings, repeat for the next photo, and so on. These tools are built to do everything with any image and not to churn through 50 receipts as quickly as possible. I wanted an app that's focused on this one task, with a workflow that's as streamlined as possible. My first iteration was a fully automatic CLI app called Perspectra, implemented with Python and scikit-image. You'd pass your image and it would try to detect and extract the document for you. Simple as that. Although I actually liked scikit-image — feature-rich, yet more straightforward than OpenCV — I quickly realized that I absolutely do not like Python. But more importantly, I realized that I also needed a GUI to fix incorrectly detected document boundaries, as the fully automatic CV pipeline would never get all documents 100% right. And how do you build a desktop app with a GUI? Obviously with Haskell. 😝 Joking aside, I had recently started learning Haskell and was absolutely in love with it. So naturally, I wanted to see if it could be used for building the desktop app. As I didn't want to use Python any longer, my next instinct was to use ImageMagick for the computer vision and image manipulation tasks, as I had some experience with its features and capabilities. The existing Haskell bindings were rather lacking, so I opted to simply call magick as an external process. While this mostly worked, it was always a pain to get it installed and linked correctly across platforms, and the performance was surprisingly bad for larger images. Another obvious choice would have been OpenCV, but I had some bad memories of using it at university (maybe it was just the C++ context …), and the Haskell bindings looked rather painful. So, my next experiment was using the native Haskell image processing library Hip. With the help of its author @lehins himself and @HanStolpo, we were able to make it work at ZuriHac! (Thanks again!) However, it was still missing some features that I wanted, like binarization with Otsu's Method. While it was certainly possible to implement this in Hip, I (for once) felt that Haskell's abstractions didn't really help with the task at hand and only complicated things unnecessarily. A for loop in C, by comparison, is conceptually very simple and just as fast as the Haskell code. Luckily, C is a first-class citizen in Haskell and it's very easy to bundle some C code and call it via Haskell's FFI. Unfortunately, there didn't seem to be a straightforward C library that I could hook up to Perspec without too many FFI headaches, and so I started working on FlatCV — a pure C library for computer vision and image manipulation. I might have overdone it with the yak shaving here, but since the whole project is a labor of love anyway, why not go all the way? 😅 I'm quite happy with the experience of using C for the image manipulation algorithms, and I was able to quickly build a fully functioning version with the necessary Haskell bindings. Just recently, I released version 0.3.0, and by now it has most of the basic operations you would expect from an image manipulation library. I also ported some of the higher-level CV operations, like adaptive binarization and corner detection, that I first implemented in Perspectra. There are still plenty of opportunities to improve the performance of FlatCV: SIMD, GPU usage, streamed processing, etc. However, as FlatCV isn't used in a real-time context (i.e., 60 fps), the performance is already more than sufficient. With FlatCV in place, I could finally implement the last missing piece for 1.0: Automatic corner detection directly in Perspec. Edge Detection vs. Corner Detection Most scanner apps detect documents with a pipeline along the lines of the one described by Dropbox: Downscale the image Run an edge detection algorithm (e.g. Canny) Find the most prominent straight lines with a Hough transform Build quadrilaterals from the intersections of those lines and score them to pick the best one This works great for a perfectly flat sheet of paper on a high-contrast background. But real documents are rarely perfectly flat: Receipts are wrinkled, book pages are curved, and paper that has been folded never lies completely flat either. When you fit a straight line to a curved edge, the intersections of the lines (i.e. the reconstructed corners) can be quite off, even if the edge detection itself was perfect. Perspec therefore approaches it from the other side: Instead of looking for straight edges, it segments the photo into document and background and then derives the corners from the document's outline. This is FlatCV's corner detection pipeline in detail: Convert the image to grayscale and downscale it to 256×256 px. (The detection doesn't need the full resolution, and this makes it fast.) Blur the image to get rid of noise and paper texture. Create an elevation map with a Sobel filter. (Strong edges become mountain ridges.) Flood the elevation map with watershed segmentation: The center of the image seeds the document basin and the image border seeds the background basin. The result is a binary mask of the document. Smooth the mask with a binary closing. Run a Förstner corner detector on the mask. (Unlike the more popular Harris detector, whose corners are shifted inwards, the Förstner detector yields sub-pixel-accurate corner positions.) Sort the corner candidates and keep the 4 corners with the largest angles. Scale the corner coordinates back up to the original resolution. Input Detected Corners The nice thing about this approach is that it never assumes straight edges. The watershed happily follows a wrinkled document boundary, and even on a crumpled receipt the corners are still locally well defined. And if the detection does get it wrong, you can simply drag the selection polygon into the right size and position. The best of both worlds: automatic detection and manual correction. Binarization Algorithms