perspectiveTransform causing large memory spike / app being killed

I have a PDF which contains geocoordinates. I'm extracting out that image with the following code (this is for an iOS application):

guard let cgDocument = CGPDFDocument(overlay.pdfUrl as CFURL) else { return }
guard let cgPage = cgDocument.page(at: 1) else { return }
        
var boundingRect = self.rect(for: overlay.boundingMapRect)
let pdfPageRect = cgPage.getBoxRect(.mediaBox)
let renderer = UIGraphicsImageRenderer(size: pdfPageRect.size)
        
var img = renderer.image { ctx in
         UIColor.white.set()
         ctx.fill(pdfPageRect)
         ctx.cgContext.translateBy(x: 0.0, y: pdfPageRect.size.height)
         ctx.cgContext.scaleBy(x: 1.0, y: -1.0)
         ctx.cgContext.drawPDFPage(cgPage)
}

Once I have that image, I then need to adjust it to fit the specific coordinate corners. For that, I'm doing the following using a perspectiveTransform:

let ciImg = CIImage(image: img)!
let perspectiveTransformFilter = CIFilter.perspectiveTransform()
perspectiveTransformFilter.inputImage = ciImg
perspectiveTransformFilter.topRight = cartesianForPoint(point: ur, extent: boundingRect)
perspectiveTransformFilter.topLeft = cartesianForPoint(point: ul, extent: boundingRect)
perspectiveTransformFilter.bottomRight = cartesianForPoint(point: lr, extent: boundingRect)
perspectiveTransformFilter.bottomLeft = cartesianForPoint(point: ll, extent: boundingRect)

let txImg = perspectiveTransformFilter.outputImage!
img = UIImage(ciImage: txImg)

The original image is 792 x 612 (a landscape PDF) but the boundingRect covering the coordinates is 25625 x 20030. Obviously when I try to draw the image into that bounding box the app runs out of memory (25625 x 20030 x 4 is around 2GB of memory).

What I'm struggling with is - how do I correctly scale this image to fit into the bounding box even though the bounding box is, oh, 10x the resolution of the actual device? There's some key CoreGraphics thing I'm sure I'm missing here.

What are you trying to do? You said this PDF has geocoordinates. Is this a geospatial PDF?

Apple doesn't have any API for georeferencing. You'll have to use a 3rd party framework like PROJ or GDAL. It's not easy.

Geospatial data is often very large. Apple APIs are not suitable. Apple APIs are all designed for images that will fit into a single GPU texture. But if you're doing some kind of transform, you'll likely need two of them. So you'll need to keep your images to 4K x 4K at most.

For geospatial data, that means you'll be tiling. Again, Apple has little support for tiling. There is some support, but it's horribly complicated and slow.

Thanks. It is geospatial PDF, and I've already incorporated GDAL to extract the coordinates, so that's not the issue.

Basically the issue is - when I use the drawPDFPage it properly fills the bounding rectangle. However, I need to also apply a perspectiveTransform and when I go to draw that it runs out of memory. So somehow the scaling logic is just fine if I don't transform it - I'm trying to figure out how to transform it but still have it be able to draw appropriately without running out of memory

Just to make it a little clearer, here's a (cleaned up) version of my renderer. If I skip lines 26 to 36 (the transform filter) this renders in the bounding box size wise. But if I keep those lines in, I get the out of memory exception.

This unfortunately isn't just a simple rotation, because, well, 2D box on a 3D Earth. But hopefully that helps explain a little more what's going on.

class PDFOverlayRenderer: MKOverlayRenderer {    
    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
        var boundingRect = self.rect(for: overlay.boundingMapRect)
        guard let cgDocument = CGPDFDocument(overlay.pdfUrl as CFURL),
            let cgPage = cgDocument.page(at: 1) else {  return }
        let pdfPageRect = cgPage.getBoxRect(.mediaBox)
        let renderer = UIGraphicsImageRenderer(size: pdfPageRect.size)
        
        img = renderer.image { ctx in
            UIColor.white.set()
            ctx.fill(pdfPageRect)
            ctx.cgContext.translateBy(x: 0.0, y: pdfPageRect.size.height)
            ctx.cgContext.scaleBy(x: 1.0, y: -1.0)
            ctx.cgContext.drawPDFPage(cgPage)
        }
        var img: UIImage!
        
        UIGraphicsPushContext(context)
        
        if !overlay.actualCoords.isEmpty {
            var ll = self.point(for: overlay.actualCoords[3])
            var lr = self.point(for: overlay.actualCoords[2])
            var ur = self.point(for: overlay.actualCoords[1])
            var ul = self.point(for: overlay.actualCoords[0])

            let ciImg = CIImage(image: img)!
            let perspectiveTransformFilter = CIFilter.perspectiveTransform()
            perspectiveTransformFilter.inputImage = ciImg
            perspectiveTransformFilter.topRight = cartesianForPoint(point: ur, extent: boundingRect)
            perspectiveTransformFilter.topLeft = cartesianForPoint(point: ul, extent: boundingRect)
            perspectiveTransformFilter.bottomRight = cartesianForPoint(point: lr, extent: boundingRect)
            perspectiveTransformFilter.bottomLeft = cartesianForPoint(point: ll, extent: boundingRect)
            
            let txImg = perspectiveTransformFilter.outputImage!
            
            img = UIImage(ciImage: txImg)
            boundingRect.origin = CGPoint(x: minX, y: minY)
        }
        
        img.draw(in: boundingRect, blendMode: .normal, alpha: 1.0)
        UIGraphicsPopContext()
    }
    
    func cartesianForPoint(point:CGPoint, extent:CGRect) -> CGPoint {
        return CGPoint(x: point.x,y: extent.height - point.y)
    }
}

GDAL is for much more than just extracting coordinates. You would use GDAL to actually warp the image into a new 2D representation, based on your selected projection. And GDAL will do that out of the box (not true), much more accurately than any perspective transform.

Any geospatial data is likely going to be much larger than anything Apple supports, especially on iOS. Apple APIs are designed to handle an image generated by the on-board camera. Even the latest iPhones released today are still touting 48 MP images. Your moderate-sized geospatial PDF is over 10 times larger than that.

And you're using iOS, which has very limited RAM. Even on macOS, where you can open these large images, it will lock up the UI for any operation.

And I'm curious about something. One thing that GDAL doesn't support out of the box is geospatial PDF. It requires one of a few different PDF libraries, each of which is problematic in its own way. Which one are you using?

I understand where you are coming from, but also the notion of georeferencing maps and images is pretty standard in these specific categories of apps. I'm already doing it with KML Image Overlays, and other than the need to rotate / skew the image the data itself isn't the problem - it's the bitmap representation from mapping the geocoordinates to the map and then needing to map that to the scale of the image. In other words, if the PDF is north-up already, it overlays perfectly. It's when we need to skew it / manipulate the image we have the issue.

The GDAL/PDF libraries are outside of the scope of this really - I've already processed the PDF / TIFF and extracted the geocoordinates at this stage. But I've custom compiled GDAL with Proj and PDFium, though we've done the coordinate testing with Poppler as well and gotten the same results.

I can't tell you any more than I already have.

perspectiveTransform causing large memory spike / app being killed
 
 
Q