Scanned airphotos carry a black collar — film holder edges, fiducial marks, chamfered
corners, scanner overrun — outside the exposed frame. Left in place it covers adjacent
photos in a mosaic. fly_mask() writes a copy of each image with an alpha band
marking that collar transparent, leaving every pixel inside the frame untouched.
Arguments
- src
Character vector of image paths.
- dest_dir
Directory for the masked copies. Created if it does not exist.
- threshold
How close to black a pixel must be to count as collar, as a per-band distance from 0. Defaults to 16, which is measured rather than chosen — see Threshold below. The default is written as a call so that the shipped value and the calibrated constant cannot drift apart; there is only one of them.
- overwrite
If
FALSE(default), skip images whose masked copy already exists.
Value
A tibble with one row per input:
sourcethe input path
destthe masked copy, or
NAwhere none was writtenmask_fractionshare of the frame the mask covers.
NAwherever no mask was computed — a resumed row, or any refusal reached before the measurementmask_fraction_interiorshare of the central box the mask covers — the quantity the runaway guard tests.
NAon the same rows asmask_fractionthresholdthe threshold requested for this image.
NAon a resumed row, whose file was written on an earlier run at a threshold this call did not measure and must not claimmaskedwhether a masked copy was written
reasonwhy the row is what it is.
NAonly for an ordinary freshly masked image — a resumed row carries a reason withmasked = TRUE, so partition onmasked, never onis.na(reason)successwhether the call reached a definite conclusion about this image. A reasoned refusal — a non-Byte source, a colliding destination, a mask that exceeds the interior cap or could not be measured — is
TRUE: the function did its job and declined.FALSEmeans something stopped it from concluding at all: the file is missing or unreadable, GDAL errored, or the copy could not be written
Details
The collar is edge-connected, not a shape. The mask is a flood fill seeded from the
image border (GDAL nearblack -alg floodfill), so only darkness reachable from the
edge is masked. A dark lake or deep shadow inside the frame is kept, which is the
whole point: a plain threshold masks it too. Measured on 1990 roll bcb90128 frame 213,
a plain threshold masks 7.7% of the frame and this masks 2.5% — the missing 5% is
water.
It is not a circle. fly#23 proposed detecting a circular lens boundary; measured
over 264 thumbnails spanning 1967-2018, the dark region's median area is 2.7% where an
inscribed circle implies 21.5%, and the middles of the image edges are as dark as the
corners, which a circle cannot produce. See inst/notes/border-masking.md.
Threshold: scanned black is not 0. On these JPEGs it runs 3-12, so the exact-zero
matching fly_georef() used before v0.11.0 masked a median of 0.16% of the frame
against the 3.11% a threshold of 16 reaches — a 19.8x gap. (Both are mask
fractions from mask_border_sweep.csv. The 2.7% quoted above is the raw dark
fraction, a different measurement, and the two must not be compared.)
The default is set from the per-frame threshold at which a frame's own collar is fully
consumed, taken across the measured population; inst/notes/border-masking.md carries
the distribution. It assumes Byte bands, and a source that is not Byte is refused
rather than reported as having no collar — -near is an absolute per-band distance, so
the number does not transfer to a 16-bit scan.
The runaway guard. A threshold high enough to reach scene content floods inward and the mask stops being a collar. Because a genuine collar hugs the border, it contributes almost nothing to a central box — so the guard tests the interior fraction, not the total, which cannot tell a thick collar from a lake running off the edge. When it trips, the image is left unmasked with a warning rather than skipped: an unmasked frame shows a black border, which is visible and is what callers already live with, while an over-eager mask deletes real imagery transparently and nothing downstream reports it.
A mask fraction of zero is not a warning. 27 of the 264 measured frames carry no collar at all, digital frames among them, and for those an empty mask is the right answer.
Examples
centroids <- sf::st_read(system.file("testdata/photo_centroids.gpkg", package = "fly"))
#> Reading layer `photo_centroids' from data source
#> `/home/runner/work/_temp/Library/fly/testdata/photo_centroids.gpkg'
#> using driver `GPKG'
#> Simple feature collection with 20 features and 16 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: -126.7631 ymin: 54.34512 xmax: -126.449 ymax: 54.47635
#> Geodetic CRS: WGS 84
fetched <- fly_fetch(centroids[1, ], type = "thumbnail", dest_dir = tempdir())
#> Downloaded 1 of 1 files
masked <- fly_mask(fetched$dest[fetched$success], dest_dir = tempdir())
#> Masked 1 of 1 images
masked[, c("mask_fraction", "mask_fraction_interior", "masked")]
#> # A tibble: 1 × 3
#> mask_fraction mask_fraction_interior masked
#> <dbl> <dbl> <lgl>
#> 1 0.0526 0 TRUE
