FAIRUAV 0.1.0
dotnet add package FAIRUAV --version 0.1.0
NuGet\Install-Package FAIRUAV -Version 0.1.0
<PackageReference Include="FAIRUAV" Version="0.1.0" />
<PackageVersion Include="FAIRUAV" Version="0.1.0" />
<PackageReference Include="FAIRUAV" />
paket add FAIRUAV --version 0.1.0
#r "nuget: FAIRUAV, 0.1.0"
#:package FAIRUAV@0.1.0
#addin nuget:?package=FAIRUAV&version=0.1.0
#tool nuget:?package=FAIRUAV&version=0.1.0
FAIRUAV
A small F# library for reading UAV image metadata, referring to images inside ZIP archives, and mapping documented fields to curated ARC annotations.
The initial DJI model and mappings are refactored from metadata-tag_investigation.ipynb and arc_creation.ipynb in the earlier AgDaFAIR flyover work. The domain model covers all 59 fields in the notebook's DJI Metadata XMP tag explanations section. Cross-vocabulary assignments use the curated mappings. Supported native EXIF fields retain their published property identities; fields without a binding or applicable mapping remain available for curation.
Install
Requires .NET 10. Add the package to a project:
dotnet add package FAIRUAV --version 0.1.0
Or reference it from an F# script:
#r "nuget: FAIRUAV, 0.1.0"
Build and test
Requires the .NET 10 SDK/runtime.
dotnet build FAIRUAV.slnx
dotnet run --project tests/FAIRUAV.Tests -- --sequenced
The library targets .NET 10 to match the current script environment. This also avoids the .NET 7/8 ZIP filename-decoding regression when combining UTF-8 names with a CP437 fallback.
Runtime dependencies are MetadataExtractor 2.9.3, PolyglotSSSOM 1.0.0, and ARCtrl.Core 3.2.3. The exploratory report has no plotting package or external browser assets. The notebook's Plotly.NET 5.1.0 dependency conflicts with current ARCtrl's DynamicObj dependency; the report's data records remain usable by other visualization tools.
Package for NuGet
Pack the library from the repository root:
dotnet pack src/FAIRUAV/FAIRUAV.fsproj -c Release
The package is written to artifacts/packages/FAIRUAV.0.1.0.nupkg. It includes the library, XML API documentation, README, and MIT license; the default SSSOM document is embedded in the library. Image files and playground outputs are not packaged.
To publish it yourself, with your NuGet API key supplied through NUGET_API_KEY:
dotnet nuget push artifacts/packages/FAIRUAV.0.1.0.nupkg --source https://api.nuget.org/v3/index.json --api-key $env:NUGET_API_KEY
Read an image or archived image
open FAIRUAV
let raw = Metadata.readImageMetadata "images/photo.jpeg"
let selectors = Archive.getZipSelectors "images.zip"
let selector = selectors |> Array.find (fun s -> s.StartsWith("#file="))
let archivedRaw = Metadata.readZipImageMetadata "images.zip" selector
let filename = FragmentSelectors.fileName selector // string option
Both read functions return IReadOnlyList<MetadataExtractor.Directory> with all directories and tags decoded by MetadataExtractor. They do not filter to DJI fields or mapped terms. Tags, descriptions, and directory-level parsing errors remain accessible; coverage depends on MetadataExtractor's support for the image format and metadata. The filesystem overload also includes filesystem metadata; the stream/archive overloads cannot provide that directory.
open MetadataExtractor
for directory in raw do
for tag in directory.Tags do
printfn "%s / %s = %s" directory.Name tag.Name (directory.GetString(tag.Type))
for error in directory.Errors do
printfn "%s: %s" directory.Name error
let xmpProperties = Xmp.getProperties raw // Scalar properties, including unmapped ones.
For the complete parsed XMP tree, use MetadataExtractor.Formats.Xmp.XmpDirectory.XmpMeta on the returned XMP directories. Xmp.getProperties is a scalar convenience view that excludes containers and qualifiers. Parsing DJI fields, applying mappings, and exporting ARC cells leave these raw directories intact.
The archive reader opens only the selected member. Non-seekable entry streams are decompressed into a memory buffer because MetadataExtractor requires seeking; no files are extracted to disk. Memory usage therefore depends on the uncompressed size of the selected image. Metadata.readStream fileName stream also accepts caller-owned streams positioned at the start of an image and leaves them open.
Missing files, ambiguous ZIP names, invalid selectors, and native read failures throw. MetadataExtractor may return partial directories with errors, and supports some non-image formats too; FAIRUAV preserves its behavior rather than enforcing a separate image-type whitelist.
Parse DJI XMP
let parsed = Dji.parse archivedRaw
let latitude = parsed.Metadata.GpsLatitude
let frameRate = parsed.Metadata.SensorFPS |> Option.map Dji.numberValue
let problems = parsed.Issues
let unknown = parsed.UnknownProperties
- All model fields are optional. Missing fields do not become zero, empty placeholders, or
N/A. - Lookup uses namespace URI and property identity, so prefixes can vary. Standard XMP creation/modification dates, TIFF Make/Model, and
dc:formatare handled separately from DJI properties. - Numeric parsing is invariant-culture and rejects nonfinite values. Rational frame rates such as
5819/100retain their rational representation. - Timestamps retain their source string and distinguish an explicit offset from an unspecified timezone. A machine-local timezone is never inferred.
- RTK and binary status codes preserve unknown values. Camera calibration records retain the raw representation and the documented nine/twelve coefficient order.
- Differing duplicate values produce an issue rather than silently choosing a winner. Raw scalar properties, unknown fields, and parsing issues remain available in the result.
Dji.knownFields provides the documented field definitions. Dji.parseProperties accepts existing XmpProperty observations, and Xmp.getProperties exposes scalar XMP properties from all metadata directories. Schema, container, and qualifier nodes are excluded from scalar counts, while array items and unknown properties remain inspectable. The original native directories retain the complete XMP tree.
Map metadata to ontology annotations
The curated source of truth is mappings/uav-metadata.sssom.tsv, a standard SSSOM document with embedded metadata. PolyglotSSSOM reads it directly into its native SssomDocument and Mapping types. The default document is bundled with the library; callers can supply another document.
let mapping = OntologyMapping.mapRaw archivedRaw
// Or, for just the DJI model and its source observations:
let djiMapping = OntologyMapping.mapDji parsed
// Use a document maintained independently of the library:
let document = Mappings.read "mappings/uav-metadata.sssom.tsv"
let customMapping = OntologyMapping.mapRawWith document archivedRaw
let annotations = mapping.Annotations
let needsCuration = mapping.Unmapped
A supplied document replaces the default mapping set; it is not implicitly merged. To extend the defaults in consumer code, load a fresh copy and add a standard SSSOM row:
let customDocument = Mappings.loadDefault ()
customDocument.Metadata.EnsurePrefix("example", "https://example.org/uav/")
let customRow = SSSOM.Mapping.CreateEntityMapping(
"dji:FlightXSpeed", "skos:relatedMatch", "example:flightXSpeed",
"semapv:UnspecifiedMatching")
customRow.SubjectLabel <- Some "Flight X speed"
customRow.ObjectLabel <- Some "Example flight X speed"
customDocument.AddMapping customRow
let customMapping = OntologyMapping.mapRawWith customDocument raw
example:flightXSpeed is illustrative; replace it with a term you have curated and record the appropriate justification. For an already mapped source, replace its term row rather than adding a competing term row. Any XMP property with an existing namespace, and native tags with a published source binding, can be mapped this way. Unbound native tags remain available in raw metadata, but require a source binding before SSSOM rows can match them.
The bundled document contains fourteen rows: seven term associations and seven separate qudt:hasUnit assignments. It covers longitude, latitude, altitude units, date/time, zoom factor, DJI capture time, camera model and serial number, and gimbal-angle units. Longitude, latitude, zoom factor, and date/time retain their NCIT targets; capture time and camera identity use EXIF/CIPA properties. Altitude and gimbal fields have units and plain labels, with their field terms still uncurated. No custom SSSOM fields or ontology-term wrapper are used.
Native EXIF directory/tag pairs are bound to published W3C EXIF properties, with CIPA's property used for camera-body serial number. Bound fields can be annotated directly under their source property when no alternative SSSOM term mapping is supplied; no self-mapping rows are needed. OntologyMapping.sourceId returns an optional IRI, and unbound native tags stay in Unmapped. XMP keeps its existing namespaces and still requires applicable mapping rows for annotation.
Annotations retain any native SSSOM term and unit rows; Term is None for a direct native annotation. Source values and observations, unmapped metadata, DJI parsing issues, mapping conflicts, and native metadata errors remain available. Accepted numeric values retain their source strings after validation; the typed DJI model is available separately for calculations. ARCtrl ontology annotations are created only when projecting ARC tables.
See the mapping inventory and curation notes for source identifiers and the editing workflow. Changes to target IDs and labels belong in the SSSOM document; extraction and value conversion belong in code.
Explore an image
let report = Exploration.inspect archivedRaw
let tagRows = report.Tags
Exploration.saveHtml "image-metadata.html" filename.Value report
The self-contained report contains tag-count charts by directory and XMP namespace, parser errors, and a searchable table of stored values and descriptions. It counts actual scalar XMP properties rather than the synthetic XMP value-count tag. Repeated directory names remain separate instances. Saving a report is explicit; it makes no network requests or automatic browser launches. Reports contain the metadata values, including any location or serial-number data present in the image.
An executable example demonstrates both input forms:
dotnet run --project examples/InspectImage -- "photo.jpeg" "report.html"
dotnet run --project examples/InspectImage -- "images.zip" "#file=data/photo.jpeg" "report.html"
ARC table prototype
let table = ArcAnnotations.createImageTable mapping.Document "images" [
{ ImageReference = "images.zip" + selector
Annotations = mapping.Annotations }
]
This is an experimental projection: one image per row, an Input [Data] reference, and Parameter columns for mapped fields and directly bound native EXIF fields. Missing values remain blank. It does not write an ARC, choose a study/run layout, infer process relationships, or silently apply review candidates. Table semantics are intentionally left for the next refinement.
ARC cell values pass through ArcExport.formatValue. Known timestamp properties are exported in ISO syntax: for example, 2026:07:23 11:32:26 becomes 2026-07-23T11:32:26. Formatting uses the source property IRI, independent of the mapped header. Raw annotation values and observations stay unchanged. Explicit offsets and fractional precision are preserved; missing timezones are not inferred. An invalid recognized timestamp raises FormatException, with the image reference included when exporting a table. See ARC export formatting for the supported properties and formats.
ZIP selectors and current limits
Selectors follow the ZIP fragment identifier draft:
#file=data/photo.jpg
#directory=data/
#directory=/
The index includes inferred folders and explicit empty folders. Names are case-sensitive; duplicate entry names are rejected. Slashes and allowed punctuation remain readable. Spaces, &, #, and literal % are encoded. FragmentSelectors.parse decodes each path segment exactly once and rejects malformed UTF-8, encoded separators, and unsupported paths. display produces a decoded label for display only; use the encoded selector in references.
Archive entry names use .NET's UTF-8 flag handling with CP437 fallback. As in the original script, the BCL reader does not implement Unicode Path extra-field overrides (0x7075), strict rejection of malformed UTF-8 archive filenames, or validation of contradictory entry-type attributes. It is therefore not a complete implementation of every ZIP-draft requirement. Payload parsing is delegated to MetadataExtractor; no additional archive integrity/authenticity verification is claimed.
No images, notebook outputs, or study-specific data are bundled. Automated tests use small synthetic images, ZIPs, and XMP packets.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- ARCtrl.Core (>= 3.2.3)
- FSharp.Core (>= 10.1.403)
- MetadataExtractor (>= 2.9.3)
- PolyglotSSSOM (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 48 | 10/29/2026 |