An interactive tool for diagnosing UAV survey plans.
A drone photographs a heritage structure from 185 planned viewpoints. Whether that plan is any good cannot be judged from a list of coordinates. You are building the instrument that lets an operator see the weak parts of a plan — and test whether one extra camera fixes them — before anything flies.
- Scene
- Kiri Vehera
- Viewpoints
- 185
- Surface targets
- 60 000
- Mastery share
- 20 %
The brief in one page
Read the glossary once before anything else. Every technical term below — pose, picking, sector, weight, visibility — has a plain-language definition, reproduced in section 09.
What the tool is for
UAVs capture the photographs used to build 3D models of buildings, including sites of cultural significance. Before the drone flies, a planning algorithm produces a set of camera viewpoints — a position plus where it is pointing — intended to cover the whole surface from multiple angles.
The operator needs to know whether every part of the surface is observed often enough, from enough different directions, at a sensible distance and viewing angle. Your application answers that question visually, and lets the user intervene.
Provided · not marked
- The
viewscope_corelibrary — every formula, constant and geometric computation - Pose construction and projection
- Offscreen photographic and depth rendering
- Visibility test with occlusion
- Sector assignment, weights, scores
- Safe-placement test; validating loaders
- The Kiri Vehera data package
Yours · all of it marked
- Architecture and the whole user interface
- Picking, selection, drawing the plan
- Surface colouring, heatmaps, sector and weight visualisations
- First-person view, photo export, manual camera placement
- Deciding when to call the library and caching what it returns
- Incremental update, threading, process orchestration
- Packaging, tests, documentation
The library computes. Your application decides when to compute, remembers what was computed, and shows it.
Standing up the GUI, 3D and packaging toolchain is itself part of the project. VTK is advised for the interactive viewport — it is fast and avoids performance problems later.
The data you are given
Five files, one world frame. +Z up, right-handed, metres, degrees. Nothing is ever recentred or rescaled, so geometry from any two files can be drawn together directly.
| File | What it holds | Used for |
|---|---|---|
scene.obj | Coloured triangle mesh — vertices carry real scanned colour | Display and photography |
targets.ply | 60 000 surface points, each with an outward normal | Coverage is judged here — never at mesh vertices |
plan.json | 185 viewpoints: position, yaw, pitch, plus the camera model | The plan under inspection |
flyable.npz | Collision-free flight volume as a voxel grid | Where a UAV may safely be |
visibility.npz | Which targets each shipped viewpoint can see, occlusion included | Lookup for shipped cameras |
scores.npz | Per-observer weight, f_rho, f_theta, sector (8 and 4), best-per-sector, diversity, target score | Everything the panels display |
Surface targets are the measurement grid
They are 3D points spread evenly over the scanned surface, each carrying a normal — the direction sticking straight out of the surface. A camera can only usefully observe a target from the side its normal points towards. For every target, the package lists the viewpoints that can see it, with occlusion by the building already accounted for.
Each observing camera arrives with values you display, not compute
One of a fixed number of equal directional wedges measured around the target normal — like compass directions around the point. Eight is the default, four is the coarse alternative. Cameras in different sectors see the target from genuinely different directions, which is exactly what photogrammetry needs.
A number between 0 and 1 saying how useful that camera is for that target. It is supplied together with its two factors — one for distance, one for viewing angle — so your interface can show which of the two is holding a camera back.
A voxel grid marking where a UAV may safely be: outside the building, clear of it by the safe distance, above the minimum altitude, and reachable from outside airspace. Every planned viewpoint already lies inside it.
Shipped viewpoints are looked up. Edited viewpoints are recomputed through the library. A camera the user adds or moves has no precomputed values — your application must obtain them at interaction time by calling the library.
The library you call
viewscope_core is a dependency, not a starting point. You never reimplement, modify
or tune any of its mathematics. If you believe something in it is wrong, report it — do
not fork it. Install with pip install -e as described in the Resources README.
| Call | Returns | When you use it |
|---|---|---|
load_scene / load_targets | Mesh; positions + normals | Startup, with validation |
load_plan | Plan: camera, ids, positions, yaws, pitches | Startup |
load_scores | Scores: weights, factors, sectors, best, diversity, score | Startup — then look up |
camera_pose(pos, yaw, pitch) | 4 × 4 pose matrix | Every time you need a pose. Never build it yourself |
frustum_corners(...) / camera_glyph(...) | Frustum geometry for drawing | Drawing viewpoints as oriented pyramids |
SceneRenderer.render_color / render_depth | RGB image · depth map | First-person view, batch export, occlusion |
camera_visibility(...) | Boolean mask over all targets | Every camera the user adds or moves |
observer_geometry / weight_factors | ρ, θ · weight, fρ, fθ | New-camera scoring |
sector_indices / best_per_sector | Sector per observer · leaders + maxima | New-camera scoring |
angular_diversity / target_score | Diversity figure · target quality score | Sector plot, heatmap |
check_placement(mesh, targets, pos) | (ok, reason) | Before accepting a manual placement |
What a full recompute for one new camera looks like
# 1 · refuse unsafe placements before doing any work
ok, reason = vc.check_placement(mesh, targets.positions, position)
if not ok:
return Rejected(reason) # show `reason` to the user verbatim
# 2 · pose, then visibility against a freshly rendered depth map
pose = vc.camera_pose(position, yaw_deg, pitch_deg)
depth = renderer.render_depth(pose) # separate process — pyrender needs its own GL
vis = vc.camera_visibility(depth, targets.positions, targets.normals,
position, pose, plan.camera)
# 3 · score this camera against every target it can see
seen = np.flatnonzero(vis)
for t in seen:
rho, theta = vc.observer_geometry(targets.positions[t], targets.normals[t],
position[None, :])
w, f_rho, f_theta = vc.weight_factors(rho, theta)
s = vc.sector_indices(targets.positions[t], targets.normals[t],
position[None, :], sector_count)
# 4 · cache the result, then update every dependent view incrementally
cache.put(camera_id, vis, weights, sectors)
bus.emit("camera_changed", camera_id)
Recompute the visibility of one shipped viewpoint through the library and match
the packaged visibility.npz row for it exactly. Keep the input geometry
and camera model precisely as shipped. This is the test that proves you are calling the library
correctly — make it an automated test, not a one-off.
The scoring model
All constants live inside the library. You never type a numeric model parameter yourself — the values below are shown so you can read the panels, not so you can enter them.
| Constant | Value | Meaning |
|---|---|---|
MU_RHO_M | 10.0 m | Preferred stand-off distance — the reference ring on the sector plot |
SIGMA_RHO_M | 5.0 m | How quickly usefulness falls off with distance |
MU_THETA_DEG | 45° | Preferred incidence angle to the target normal |
SIGMA_THETA_DEG | 15° | Angular tolerance around that preference |
SAFE_DISTANCE_M | 5.0 m | Closest a camera may be placed to the surface |
DEFAULT_SECTOR_COUNT | 8 | Default directional wedges |
ALTERNATE_SECTOR_COUNT | 4 | Coarse alternative — must be switchable in the UI |
ZNEAR_M | 0.1 m | Near plane; targets closer than this are not visible |
How a target ends up with a score
Two Gaussian factors, multiplied. f_rho peaks when the camera
is 10 m away; f_theta peaks at 45° off the normal. Both are supplied separately so a
panel can say “this camera is fine on angle but far too close.”
Each observer's direction is projected into the tangent plane at the target and binned by azimuth into one of 8 (or 4) equal wedges. Exactly one wedge per observer.
Within each wedge, the highest-weight camera wins. That is the camera your 3D view marks, and the bar the sector chart draws.
The mean over all sectors of the best weight in that sector, with empty sectors counting zero. This is what the heatmap colours. A target seen brilliantly from one direction only still scores badly — which is the entire point.
A separate figure: how evenly observers are spread across the wedges, capped at the ideal share per wedge. Shown on the sector plot alongside the score.
Core requirements
Sixteen items, 80 % of the marks. The approach below is the suggested one — you may modify it, but you must be able to explain your approach clearly and credit any external sources used. Tick items off as you complete them; your progress is saved in this browser.
Mastery work · 20 %
Work beyond the core. These are engineering extensions, not new mathematics — the marks awarded depend on the complexity of what you add. The items are independent of one another, and the animation and photography items may follow the raw viewpoint order.
Code structure
Four rules the brief states outright. They are cheap to satisfy early and expensive to retrofit.
Modularise as a pipeline
Loading and validation → scene construction → interaction and selection → scoring lookup and aggregation → presentation. Each stage should be replaceable without disturbing its neighbours.
viewscope_app/
├─ io/ # loaders, validation, fail-closed errors
├─ scene/ # VTK actors: mesh, targets, frustums, axes, ground
├─ interact/ # picking, selection state, the selection bus
├─ analysis/ # scoring lookup, aggregation, cache, incremental update
├─ services/ # RenderService — one caller-agnostic rendering path
├─ ui/ # dockable panels, parameter panel, metrics
└─ workers/ # thread pool + renderer subprocess, clean shutdown
Keep components replaceable
The rendering back end, the scoring source and the persistence format each sit behind an interface. Most importantly: the first-person view and the batch exporter must be two callers of one rendering service — not two rendering implementations.
Use appropriate data structures
NumPy arrays for numerical data, dictionaries for metadata. A per-target Python object for 60 000 targets will not stay interactive. Consider a plugin architecture for extensibility.
Treat the library as a dependency
Do not modify it. Report anything you believe is wrong instead of forking it.
Navigation must not drop frames. Every click gives visible feedback within 0.2 s. Anything over 1 s shows progress and offers cancellation while the rest of the interface stays usable. That needs threads — and separate processes where the library renderer is involved — with clean shutdown. Caching, incremental update and deferred recomputation are yours to design.
What you hand in
The application is not the whole submission.
| Deliverable | Must contain |
|---|---|
| Project report | All implementation steps, design rationale, challenges encountered, and the solutions you developed — documented comprehensively |
| GitLab README | Appropriate usage instructions — how to install, run and operate the tool |
| Application | Stays interactive on the full Kiri Vehera dataset: 60 000 targets, 185 viewpoints |
| Tests | Including the shipped-viewpoint visibility match (requirement 8) |
| Packaging | Part of the marked work — no scaffold application is supplied |
| Attribution | Credit for any external sources used |
Glossary
Every term the brief uses, in plain language. Read it once before the brief.
Knowledge check
Eighteen questions on the things that are easy to get wrong and expensive to discover late. Answer once per question; the explanation appears immediately.